init
This commit is contained in:
21
.eslintrc.cjs
Normal file
21
.eslintrc.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// module.exports = {
|
||||||
|
// root: true,
|
||||||
|
// env: { browser: true, es2020: true },
|
||||||
|
// extends: [
|
||||||
|
// 'eslint:recommended',
|
||||||
|
// 'plugin:react/recommended',
|
||||||
|
// 'plugin:react/jsx-runtime',
|
||||||
|
// 'plugin:react-hooks/recommended',
|
||||||
|
// ],
|
||||||
|
// ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
// parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
// settings: { react: { version: '18.2' } },
|
||||||
|
// plugins: ['react-refresh'],
|
||||||
|
// rules: {
|
||||||
|
// 'react/jsx-no-target-blank': 'off',
|
||||||
|
// 'react-refresh/only-export-components': [
|
||||||
|
// 'warn',
|
||||||
|
// { allowConstantExport: true },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// }
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
10
.prettierrc.json
Normal file
10
.prettierrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
|
|
||||||
22
index.html
Normal file
22
index.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/src/assets/freepik/logo-web.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta name="description" content="COD Maintenance Machine" />
|
||||||
|
<meta name="keywords" content="sypiu, piu, ggcp, permit, cod" />
|
||||||
|
<meta name="author" content="idetama" />
|
||||||
|
<meta property="og:title" content="COD system machine" />
|
||||||
|
<meta property="og:description" content="app for machine system piu" />
|
||||||
|
<meta property="og:image" content="https://www.pi-utilitas.com/img/LOGO_PIU_BnW.ee2ccd30.png" />
|
||||||
|
<meta property="og:url" content="https://www.pi-utilitas.com/" />
|
||||||
|
|
||||||
|
<title>Call of Duty</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "antd-vite-react-sypiu",
|
||||||
|
"homepage": "/dashboard/home",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^6.0.0",
|
||||||
|
"@nivo/line": "^0.88.0",
|
||||||
|
"@nivo/pie": "^0.88.0",
|
||||||
|
"antd": "^5.15.2",
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"browser-image-compression": "^2.0.2",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"jspdf": "^3.0.1",
|
||||||
|
"mqtt": "^5.14.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^4.11.0",
|
||||||
|
"react-router-dom": "^6.22.3",
|
||||||
|
"sweetalert2": "^11.17.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.64",
|
||||||
|
"@types/react-dom": "^18.2.21",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-react": "^7.34.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"vite": "^5.1.6"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92"
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
18
public/web.config
Normal file
18
public/web.config
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<configuration>
|
||||||
|
<system.webServer>
|
||||||
|
<rewrite>
|
||||||
|
<rules>
|
||||||
|
<rule name="reactViteSypiu">
|
||||||
|
<match url=".*" />
|
||||||
|
<conditions logicalGrouping="MatchAll">
|
||||||
|
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||||
|
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||||
|
<add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
|
||||||
|
</conditions>
|
||||||
|
<action type="Rewrite" url="/" />
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</rewrite>
|
||||||
|
</system.webServer>
|
||||||
|
</configuration>
|
||||||
7
readme.md
Normal file
7
readme.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
touch README.md
|
||||||
|
git init
|
||||||
|
git checkout -b main
|
||||||
|
git add README.md
|
||||||
|
git commit -m "first commit"
|
||||||
|
git remote add origin https://gitea.idetama.id/yogiedigital/cod-fe.git
|
||||||
|
git push -u origin main
|
||||||
0
src/App.css
Normal file
0
src/App.css
Normal file
57
src/App.jsx
Normal file
57
src/App.jsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import SignIn from './pages/auth/SignIn';
|
||||||
|
import { ProtectedRoute } from './ProtectedRoute';
|
||||||
|
import NotFound from './pages/blank/NotFound';
|
||||||
|
import { getSessionData } from './components/Global/Formatter';
|
||||||
|
|
||||||
|
// dashboard
|
||||||
|
import Home from './pages/home/Home';
|
||||||
|
import Blank from './pages/blank/Blank';
|
||||||
|
|
||||||
|
// master
|
||||||
|
import IndexDevice from './pages/master/device/IndexDevice';
|
||||||
|
|
||||||
|
// Setting
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const session = getSessionData();
|
||||||
|
// console.log(session);
|
||||||
|
|
||||||
|
const isAdmin =
|
||||||
|
session?.user?.role_id != `${import.meta.env.VITE_ROLE_VENDOR}` &&
|
||||||
|
session?.user?.role_id &&
|
||||||
|
session?.user?.role_id != null &&
|
||||||
|
session?.user?.role_id != 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BrowserRouter
|
||||||
|
future={{
|
||||||
|
v7_startTransition: true,
|
||||||
|
v7_relativeSplatPath: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Route path="/" element={<Navigate to="/dashboard/home" />} />
|
||||||
|
) : (
|
||||||
|
<Route path="/" element={<Navigate to="/dashboard/home-vendor" />} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Route path="/signin" element={<SignIn />} />
|
||||||
|
<Route path="/dashboard" element={<ProtectedRoute />}>
|
||||||
|
<Route path="home" element={<Home />} />
|
||||||
|
<Route path="blank" element={<Blank />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/master" element={<ProtectedRoute />}>
|
||||||
|
<Route path="device" element={<IndexDevice />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
20
src/ProtectedRoute.jsx
Normal file
20
src/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
|
import MainLayout from './layout/MainLayout';
|
||||||
|
|
||||||
|
import { getSessionData } from './components/Global/Formatter';
|
||||||
|
|
||||||
|
export const ProtectedRoute = () => {
|
||||||
|
const session = getSessionData();
|
||||||
|
// console.log(session);
|
||||||
|
|
||||||
|
const isAuthenticated = session?.auth ?? false;
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/signin" replace />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Outlet />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
7
src/Utils/Auth/Logout.jsx
Normal file
7
src/Utils/Auth/Logout.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const handleLogOut = () => {
|
||||||
|
localStorage.removeItem('Auth');
|
||||||
|
localStorage.removeItem('session');
|
||||||
|
window.location.replace('/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handleLogOut;
|
||||||
32
src/Utils/Auth/SignIn.jsx
Normal file
32
src/Utils/Auth/SignIn.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { login } from '../../api/auth';
|
||||||
|
import { encryptData } from '../../components/Global/Formatter';
|
||||||
|
import { NotifAlert } from '../../components/Global/ToastNotif';
|
||||||
|
|
||||||
|
const handleSignIn = async (values) => {
|
||||||
|
|
||||||
|
const response = await login(values);
|
||||||
|
|
||||||
|
// return false
|
||||||
|
if (response?.status == 200) {
|
||||||
|
/* you can change this according to your authentication protocol */
|
||||||
|
let token = JSON.stringify(response.data?.token);
|
||||||
|
let role = JSON.stringify(response.data?.user?.role_id);
|
||||||
|
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
response.data.auth = true;
|
||||||
|
localStorage.setItem('session', encryptData(response?.data));
|
||||||
|
if (role === `${import.meta.env.VITE_ROLE_VENDOR}`) {
|
||||||
|
window.location.replace('/dashboard/home-vendor');
|
||||||
|
} else {
|
||||||
|
window.location.replace('/dashboard/home');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.data?.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleSignIn;
|
||||||
52
src/api/auth.jsx
Normal file
52
src/api/auth.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
import RegistrationRequest from '../components/Global/RegisterRequest';
|
||||||
|
|
||||||
|
const login = async (params) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: `auth/login`,
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
return response || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async (formData) => {
|
||||||
|
const response = await RegistrationRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: 'file-upload',
|
||||||
|
params: formData,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
return response || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (params) => {
|
||||||
|
const response = await RegistrationRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: 'register',
|
||||||
|
params: params,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
return response || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyRedirect = async (params) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: 'auth/verify-redirect',
|
||||||
|
params: params,
|
||||||
|
token: false,
|
||||||
|
});
|
||||||
|
return response || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkUsername = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `register/check-username?${queryParams.toString()}`,
|
||||||
|
});
|
||||||
|
return response || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export { login, uploadFile, register, verifyRedirect, checkUsername };
|
||||||
48
src/api/dashboard-home.jsx
Normal file
48
src/api/dashboard-home.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
|
const getTotal = async (query = '') => {
|
||||||
|
const prefix = `${query ? `?${query}` : ''}`;
|
||||||
|
const fullUrl = `${import.meta.env.VITE_API_SERVER}/dashboard/${prefix}`;
|
||||||
|
try {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `dashboard/${prefix}`,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[API Call] Failed: GET ${fullUrl}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalPermit = async (query = '') => {
|
||||||
|
const prefix = `dashboard/permit-total${query ? `?${query}` : ''}`;
|
||||||
|
const fullUrl = `${import.meta.env.VITE_API_SERVER}/${prefix}`;
|
||||||
|
try {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[API Call] Failed: GET ${fullUrl}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalPermitPerYear = async (query = '') => {
|
||||||
|
const prefix = `dashboard/permit-breakdown${query ? `?${query}` : ''}`;
|
||||||
|
const fullUrl = `${import.meta.env.VITE_API_SERVER}/${prefix}`;
|
||||||
|
try {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[API Call] Failed: GET ${fullUrl}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getTotal, getTotalPermit, getTotalPermitPerYear };
|
||||||
45
src/api/master-apd.jsx
Normal file
45
src/api/master-apd.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
|
const getAllApd = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `apd?${queryParams.toString()}`,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createApd = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: `apd`,
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateApd = async (vendor_id, queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'put',
|
||||||
|
prefix: `apd/${vendor_id}`,
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteApd = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'delete',
|
||||||
|
prefix: `apd/${queryParams}`,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJenisPermit = async () => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `apd/jenis-permit`,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getAllApd, createApd, updateApd, deleteApd, getJenisPermit };
|
||||||
84
src/api/user-admin.jsx
Normal file
84
src/api/user-admin.jsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// user-admin.jsx
|
||||||
|
import axios from 'axios';
|
||||||
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_API_SERVER;
|
||||||
|
|
||||||
|
const getAllUser = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `admin-user?${queryParams.toString()}`,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserDetail = async (id) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `admin-user/${id}`,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = async (id, data) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'put',
|
||||||
|
prefix: `admin-user/${id}`,
|
||||||
|
params: data,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (id) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'delete',
|
||||||
|
prefix: `admin-user/${id}`,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const approvalUser = async (id, queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: `admin-user/approve/${id}`,
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async (formData) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')?.replace(/"/g, '') || '';
|
||||||
|
const url = `${baseURL}/file-upload`;
|
||||||
|
|
||||||
|
const response = await axios.post(url, formData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Accept-Language': 'en_US',
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: response.data?.statusCode ?? 0,
|
||||||
|
message: response.data?.message ?? '',
|
||||||
|
data: response.data?.data ?? {},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ ERROR di uploadFile:', error);
|
||||||
|
return {
|
||||||
|
statusCode: error?.response?.status || 500,
|
||||||
|
message: error?.response?.data?.message || 'Upload gagal',
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export { getAllUser, getUserDetail, updateUser, deleteUser, approvalUser, uploadFile };
|
||||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
64
src/components/Common/BasicButton.jsx
Normal file
64
src/components/Common/BasicButton.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, ConfigProvider } from 'antd';
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
|
||||||
|
const BasicButton = ({
|
||||||
|
// color,
|
||||||
|
text,
|
||||||
|
size,
|
||||||
|
block,
|
||||||
|
clickTheButton
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
// colorBgContainer: '#23A55A',
|
||||||
|
colorText: 'white',
|
||||||
|
colorBgContainer: 'purple',
|
||||||
|
// Seed Token
|
||||||
|
// colorPrimary: '#23A55A',
|
||||||
|
// // colorPrimary: `${color}`,
|
||||||
|
// borderRadius: 8,
|
||||||
|
|
||||||
|
|
||||||
|
// // Alias Token
|
||||||
|
// // colorBgContainer: '#f6ffed',
|
||||||
|
// // colorBgContainer: {color},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23A55A',
|
||||||
|
defaultColor: 'white',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type=""
|
||||||
|
block={block}
|
||||||
|
size={size}
|
||||||
|
onClick={() => clickTheButton(block)}
|
||||||
|
// style={{
|
||||||
|
// background: "#23A55A",
|
||||||
|
// borderColor: "#23A55A",
|
||||||
|
// color: "white",
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicButton.propTypes = {
|
||||||
|
// color: propTypes.string,
|
||||||
|
text: propTypes.string,
|
||||||
|
size: propTypes.string,
|
||||||
|
block: propTypes.bool,
|
||||||
|
clickTheButton: propTypes.any
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BasicButton;
|
||||||
21
src/components/Common/BasicInput.jsx
Normal file
21
src/components/Common/BasicInput.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { theme } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
// import packageJson from "../../../package.json";
|
||||||
|
|
||||||
|
const BasicInput = () => {
|
||||||
|
const {
|
||||||
|
token: { colorPrimary },
|
||||||
|
} = theme.useToken();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="versionContainer"
|
||||||
|
className="cursor-default fixed top-1 right-1 z-20 m-1"
|
||||||
|
style={{ color: colorPrimary }}
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicInput;
|
||||||
58
src/components/Common/NavBar.jsx
Normal file
58
src/components/Common/NavBar.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Layout, theme } from "antd";
|
||||||
|
import React from 'react'
|
||||||
|
import propTypes from 'prop-types';
|
||||||
|
|
||||||
|
const { Header } = Layout;
|
||||||
|
|
||||||
|
const navbarClass =
|
||||||
|
"text-white text-h5 flex flex-col whitespace-nowrap overflow-hidden text-ellipsis";
|
||||||
|
const styles = {
|
||||||
|
navbarTitleFullSize: navbarClass,
|
||||||
|
navbarTitle: navbarClass + " max-w-[calc(100vw-200px)]",
|
||||||
|
};
|
||||||
|
const NavBar = ({ fullSize }) => {
|
||||||
|
const {
|
||||||
|
token: { colorPrimary },
|
||||||
|
} = theme.useToken();
|
||||||
|
|
||||||
|
console.log("import.meta", import.meta)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header
|
||||||
|
data-testid="navbarContainer"
|
||||||
|
className="fixed z-10 flex w-full drop-shadow-md"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colorPrimary,
|
||||||
|
paddingInline: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex h-full w-full items-center gap-4 pl-[5%]">
|
||||||
|
<a href="/">
|
||||||
|
<div
|
||||||
|
data-testid="navbarLogo"
|
||||||
|
className="h-12 w-48 bg-white text-center"
|
||||||
|
>
|
||||||
|
LOGO
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
data-testid="navbarTitle"
|
||||||
|
className={fullSize ? styles.navbarTitleFullSize : styles.navbarTitle}
|
||||||
|
>
|
||||||
|
<div className="text-[2.9vw] font-bold leading-8 sm:text-h5">
|
||||||
|
{import.meta.env.VITE_PROJECT_NAME}
|
||||||
|
</div>
|
||||||
|
<div className="pb-1 text-[2vw] font-bold uppercase leading-3 sm:text-small">
|
||||||
|
{import.meta.env.VITE_PROJECT_DESCRIPTION}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NavBar.propTypes = {
|
||||||
|
fullSize: propTypes.any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavBar;
|
||||||
20
src/components/Common/Version.jsx
Normal file
20
src/components/Common/Version.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { theme } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import packageJson from "../../../package.json";
|
||||||
|
|
||||||
|
const Version = () => {
|
||||||
|
const {
|
||||||
|
token: { colorPrimary },
|
||||||
|
} = theme.useToken();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="versionContainer"
|
||||||
|
className="cursor-default fixed top-1 right-1 z-20 m-1"
|
||||||
|
style={{ color: colorPrimary }}
|
||||||
|
>
|
||||||
|
v{packageJson.version}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Version;
|
||||||
127
src/components/Global/ApiRequest.jsx
Normal file
127
src/components/Global/ApiRequest.jsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
async function ApiRequest(
|
||||||
|
urlParams = { method: 'GET', params: {}, url: '', prefix: '/', token: true }
|
||||||
|
) {
|
||||||
|
const baseURLDef = `${import.meta.env.VITE_API_SERVER}`;
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: urlParams.url ?? baseURLDef,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFormData = urlParams.params instanceof FormData;
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
method: urlParams.method,
|
||||||
|
url: urlParams.prefix ?? '/',
|
||||||
|
data: urlParams.params,
|
||||||
|
// yang lama
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// 'Accept-Language': 'en_US',
|
||||||
|
// },
|
||||||
|
|
||||||
|
// yang baru
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': 'en_US',
|
||||||
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (urlParams.params === 'doc') {
|
||||||
|
request.responseType = 'arraybuffer';
|
||||||
|
request.headers['Content-Type'] = 'blob';
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(request);
|
||||||
|
|
||||||
|
// console.log('prefix', urlParams.prefix);
|
||||||
|
|
||||||
|
const tokenRedirect = sessionStorage.getItem('token_redirect');
|
||||||
|
|
||||||
|
let stringToken = '';
|
||||||
|
|
||||||
|
if (tokenRedirect !== null) {
|
||||||
|
stringToken = tokenRedirect;
|
||||||
|
// console.log(`sessionStorage: ${tokenRedirect}`);
|
||||||
|
} else {
|
||||||
|
stringToken = localStorage.getItem('token');
|
||||||
|
// console.log(`localStorage: ${stringToken}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParams.prefix !== 'auth/login') {
|
||||||
|
const tokenWithQuotes = stringToken;
|
||||||
|
const token = tokenWithQuotes.replace(/"/g, '');
|
||||||
|
const AUTH_TOKEN = `Bearer ${token}`;
|
||||||
|
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
|
||||||
|
} else if (urlParams.token == true) {
|
||||||
|
const tokenWithQuotes = stringToken;
|
||||||
|
const token = tokenWithQuotes.replace(/"/g, '');
|
||||||
|
const AUTH_TOKEN = `Bearer ${token}`;
|
||||||
|
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await instance(request)
|
||||||
|
.then(function (response) {
|
||||||
|
const responseCustom = response;
|
||||||
|
responseCustom.error = false;
|
||||||
|
return responseCustom;
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log('error', error.toJSON());
|
||||||
|
|
||||||
|
const errorData = error.toJSON();
|
||||||
|
|
||||||
|
const respError = error.response ?? {};
|
||||||
|
cekError(
|
||||||
|
errorData.status,
|
||||||
|
error?.response?.data?.message ?? errorData?.message ?? 'Something Wrong'
|
||||||
|
);
|
||||||
|
respError.error = true;
|
||||||
|
return respError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cekError(props, message = '') {
|
||||||
|
console.log('status code', props);
|
||||||
|
if (props === 401) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
text: `${message}, Silahkan login`,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
localStorage.clear();
|
||||||
|
location.replace('/signin');
|
||||||
|
} else if (result.isDenied) {
|
||||||
|
Swal.fire('Changes are not saved', '', 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
text: message,
|
||||||
|
}).then((result) => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendRequest = async (queryParams) => {
|
||||||
|
try {
|
||||||
|
const response = await ApiRequest(queryParams);
|
||||||
|
return response || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
if (error.response) {
|
||||||
|
console.error('Error Status:', error.response.status); // Status error, misal: 401
|
||||||
|
console.error('Error Data:', error.response.data); // Detail pesan error
|
||||||
|
console.error('Error Pesan:', error.response.data.message); //Pesan error
|
||||||
|
} else {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
}
|
||||||
|
Swal.fire({ icon: 'error', text: error });
|
||||||
|
// return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ApiRequest, SendRequest };
|
||||||
7
src/components/Global/DataFilter.jsx
Normal file
7
src/components/Global/DataFilter.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
let filterData = [];
|
||||||
|
|
||||||
|
export const setFilterData = (data) => {
|
||||||
|
filterData = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFilterData = () => filterData;
|
||||||
24
src/components/Global/EmptyData.jsx
Normal file
24
src/components/Global/EmptyData.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Empty, Col, Row, Typography } from "antd";
|
||||||
|
const {Text} = Typography;
|
||||||
|
|
||||||
|
const EmptyData = ({
|
||||||
|
titleButton,
|
||||||
|
titlePositionButton
|
||||||
|
})=>{
|
||||||
|
return(
|
||||||
|
<Empty
|
||||||
|
description={
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||||
|
<Row justify="center" align="middle" style={{ height: "100%" }}>
|
||||||
|
<Text style={{fontSize:'22px', fontWeight:'bold'}}>Data Kosong</Text>
|
||||||
|
</Row>
|
||||||
|
<Row justify="center" align="middle" style={{ height: "100%"}}>
|
||||||
|
<Text style={{width:'20%', fontSize:'20px'}}>Untuk menampilkan data silahkan klik tombol <Text strong style={{fontSize:'20px'}}>{titleButton}</Text> {titlePositionButton}</Text>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyData;
|
||||||
192
src/components/Global/Formatter.jsx
Normal file
192
src/components/Global/Formatter.jsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
const formatIDR = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
return new Intl.NumberFormat('id-ID', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrencyUSD = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const numericValue = value.replace(/[^0-9.]/g, ''); // Hanya angka dan titik
|
||||||
|
const parts = numericValue.split('.'); // Pisahkan bagian desimal
|
||||||
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); // Tambahkan koma
|
||||||
|
return `${parts.join('.')}`; // Gabungkan kembali dengan simbol $
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrencyIDR = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const numericValue = value.replace(/[^0-9]/g, '');
|
||||||
|
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toApiNumberFormatter = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const formattedValue = value.replace(/[.,]/g, ''); // Hapus semua titik
|
||||||
|
return Number(formattedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toAppDateFormatter = (value) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
'Jan',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Apr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Aug',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dec',
|
||||||
|
];
|
||||||
|
const day = date.getUTCDate().toString().padStart(2, '0');
|
||||||
|
const month = months[date.getUTCMonth()];
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
|
||||||
|
const formattedDate = `${day} ${month} ${year}`;
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toAppDateFormatterTwoDigit = (value) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
// Pastikan validitas tanggal
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return 'Invalid Date'; // Handle nilai tidak valid
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = date.getUTCDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); // Tambahkan 1 ke bulan karena bulan dimulai dari 0
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
|
||||||
|
const formattedDate = `${day}-${month}-${year}`;
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toApiDateFormatter = (value) => {
|
||||||
|
const parts = value.split('-');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const day = parts[0];
|
||||||
|
const month = parts[1];
|
||||||
|
const year = parts[2];
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const toAppDateTimezoneFormatter = (value) => {
|
||||||
|
const jakartaTimezone = 'Asia/Jakarta';
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
const formatterDay = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: jakartaTimezone,
|
||||||
|
day: '2-digit',
|
||||||
|
});
|
||||||
|
const formatterMonth = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: jakartaTimezone,
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
const formatterYear = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: jakartaTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const day = formatterDay.format(date);
|
||||||
|
const month = formatterMonth.format(date);
|
||||||
|
const year = formatterYear.format(date);
|
||||||
|
|
||||||
|
return `${day} ${month} ${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toApiDateTimezoneFormatter = (value) => {
|
||||||
|
const jakartaTimezone = 'Asia/Jakarta';
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
const formatterDay = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: jakartaTimezone,
|
||||||
|
day: '2-digit',
|
||||||
|
});
|
||||||
|
const formatterMonth = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: jakartaTimezone,
|
||||||
|
month: '2-digit',
|
||||||
|
});
|
||||||
|
const formatterYear = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: jakartaTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const day = formatterDay.format(date);
|
||||||
|
const month = formatterMonth.format(date);
|
||||||
|
const year = formatterYear.format(date);
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
import { message } from 'antd';
|
||||||
|
// cryptoHelper.js
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
const secretKey = `${import.meta.env.VITE_KEY_SESSION}`; // Ganti dengan kunci rahasia kamu
|
||||||
|
|
||||||
|
// Fungsi untuk mengenkripsi data
|
||||||
|
const encryptData = (data) => {
|
||||||
|
try {
|
||||||
|
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), secretKey).toString();
|
||||||
|
return ciphertext;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Encrypt Error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fungsi untuk mendekripsi data
|
||||||
|
const decryptData = (ciphertext) => {
|
||||||
|
try {
|
||||||
|
const bytes = CryptoJS.AES.decrypt(ciphertext, secretKey);
|
||||||
|
const decrypted = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
|
||||||
|
if (decrypted?.error) {
|
||||||
|
decrypted.error = false;
|
||||||
|
}
|
||||||
|
return decrypted;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error('Decrypt Error:', error);
|
||||||
|
return { error: true, message: `Decrypt Error: ${error}` };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSessionData = () => {
|
||||||
|
try {
|
||||||
|
const ciphertext = localStorage.getItem('session');
|
||||||
|
|
||||||
|
if (!ciphertext) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const result = decryptData(ciphertext);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// console.error('Decrypt Error:', error);
|
||||||
|
return { error: true, message: error };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatIDR,
|
||||||
|
formatCurrencyUSD,
|
||||||
|
formatCurrencyIDR,
|
||||||
|
toApiNumberFormatter,
|
||||||
|
toAppDateFormatter,
|
||||||
|
toApiDateFormatter,
|
||||||
|
toAppDateTimezoneFormatter,
|
||||||
|
toAppDateFormatterTwoDigit,
|
||||||
|
toApiDateTimezoneFormatter,
|
||||||
|
encryptData,
|
||||||
|
getSessionData,
|
||||||
|
decryptData,
|
||||||
|
};
|
||||||
23
src/components/Global/KopReport.jsx
Normal file
23
src/components/Global/KopReport.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const toBase64 = (url) =>
|
||||||
|
fetch(url)
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.then(
|
||||||
|
(blob) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => resolve(reader.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fungsi utama
|
||||||
|
const kopReportPdf = async (logo, title) => {
|
||||||
|
const images = await toBase64(logo);
|
||||||
|
return {
|
||||||
|
images,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { kopReportPdf };
|
||||||
72
src/components/Global/MqttConnection.jsx
Normal file
72
src/components/Global/MqttConnection.jsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// mqttService.js
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
const mqttUrl = 'ws://36.66.16.49:9001';
|
||||||
|
const topics = ['SYPIU_GGCP', 'SYPIU_GGCP/list-permit/changed','SYPIU_GGCP/list-user/changed'];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
keepalive: 30,
|
||||||
|
clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8),
|
||||||
|
protocolId: 'MQTT',
|
||||||
|
protocolVersion: 4,
|
||||||
|
clean: true,
|
||||||
|
reconnectPeriod: 1000,
|
||||||
|
connectTimeout: 30 * 1000,
|
||||||
|
username: 'ide', // jika ada
|
||||||
|
password: 'aremania', // jika ada
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = mqtt.connect(mqttUrl, options);
|
||||||
|
|
||||||
|
// Track connection status
|
||||||
|
let isConnected = false;
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
console.log('MQTT Connected');
|
||||||
|
isConnected = true;
|
||||||
|
|
||||||
|
// Subscribe default topic
|
||||||
|
client.subscribe(topics, (err) => {
|
||||||
|
if (err) console.error('Subscribe error:', err);
|
||||||
|
else console.log(`Subscribed to topics: ${topics.join(', ')}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
console.error('Connection error: ', err);
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('close', () => {
|
||||||
|
console.log('MQTT Disconnected');
|
||||||
|
isConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message to MQTT
|
||||||
|
* @param {string} topic
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
const publishMessage = (topic, message) => {
|
||||||
|
if (client && isConnected && message.trim() !== '') {
|
||||||
|
client.publish(topic, message);
|
||||||
|
} else {
|
||||||
|
console.warn('MQTT not connected or message empty');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to incoming messages
|
||||||
|
* @param {function} callback - Function(topic, message)
|
||||||
|
*/
|
||||||
|
const listenMessage = (callback) => {
|
||||||
|
client.on('message', (topic, message) => {
|
||||||
|
callback(topic, message.toString());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
publishMessage,
|
||||||
|
listenMessage,
|
||||||
|
client,
|
||||||
|
};
|
||||||
109
src/components/Global/QrPermit.jsx
Normal file
109
src/components/Global/QrPermit.jsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import ImgPIU from '../../assets/freepik/LOGOPIU.png';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
function QrPermit() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const selectedRole = searchParams.get('role');
|
||||||
|
const selectedName = searchParams.get('name');
|
||||||
|
const selectedTime = searchParams.get('time');
|
||||||
|
|
||||||
|
const formattedTime = selectedTime
|
||||||
|
? new Date(selectedTime).toLocaleString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
const userApproval = {
|
||||||
|
role: ['Performing Authority', 'Permit Control', 'Site Authority', 'Area Authority'],
|
||||||
|
roleCode: ['PA', 'PC', 'SA', 'AA'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredRoles = userApproval.role.filter((role, index) => {
|
||||||
|
const code = userApproval.roleCode[index];
|
||||||
|
return !selectedRole || selectedRole === code;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
textAlign: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
padding: isMobile ? '1rem' : '3rem',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ImgPIU}
|
||||||
|
alt="Logo PIU"
|
||||||
|
style={{
|
||||||
|
width: isMobile ? '100%' : '100%',
|
||||||
|
maxWidth: isMobile ? '300px' : '30%',
|
||||||
|
height: 'auto',
|
||||||
|
marginBottom: isMobile ? '1.5rem' : '2vh',
|
||||||
|
marginTop: isMobile ? '-1rem' : '-75px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{filteredRoles.map((role, index) => (
|
||||||
|
<div key={index} style={{ width: '100%' }}>
|
||||||
|
<Title
|
||||||
|
strong
|
||||||
|
level={1}
|
||||||
|
style={{
|
||||||
|
marginTop: isMobile ? '-2rem' : '-75px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#004D80',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: isMobile ? '6vw' : '2.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Validasi Dokumen <br /> Pupuk Indonesia Utilitas
|
||||||
|
</Title>
|
||||||
|
<Title
|
||||||
|
level={2}
|
||||||
|
style={{
|
||||||
|
marginTop: isMobile ? '1rem' : '-5px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#141414',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: isMobile ? '6vw' : '2.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{role}
|
||||||
|
</Title>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: isMobile ? '4.5vw' : '25px',
|
||||||
|
lineHeight: isMobile ? 1.5 : 1.8,
|
||||||
|
marginTop: isMobile ? '1rem' : '1.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text strong style={{ fontSize: isMobile ? '4.5vw' : 25 }}>
|
||||||
|
Nama:
|
||||||
|
</Text>{' '}
|
||||||
|
{selectedName ?? '-'}
|
||||||
|
<br />
|
||||||
|
<Text strong style={{ fontSize: isMobile ? '4.5vw' : 25 }}>
|
||||||
|
{formattedTime}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QrPermit;
|
||||||
25
src/components/Global/RegisterRequest.jsx
Normal file
25
src/components/Global/RegisterRequest.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const RegistrationRequest = async ({ method, prefix, params, headers = {} }) => {
|
||||||
|
const baseURL = `${import.meta.env.VITE_API_SERVER}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method: method,
|
||||||
|
url: `${baseURL}/${prefix}`,
|
||||||
|
data: params,
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': 'en_US',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data || {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error saat ${prefix}:`, error.response?.data || error.message);
|
||||||
|
throw error.response?.data || { message: 'Terjadi kesalahan pada server' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegistrationRequest;
|
||||||
499
src/components/Global/StatusButton.jsx
Normal file
499
src/components/Global/StatusButton.jsx
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Divider,
|
||||||
|
Card,
|
||||||
|
Tag,
|
||||||
|
ConfigProvider,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
Input,
|
||||||
|
Radio,
|
||||||
|
} from 'antd';
|
||||||
|
import { getStatusHistory, approvalPermit } from '../../api/status-history';
|
||||||
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from './ToastNotif';
|
||||||
|
import { getSessionData } from './Formatter';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const StatusButton = (props) => {
|
||||||
|
const {
|
||||||
|
color,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
canApprove = true,
|
||||||
|
canReject = true,
|
||||||
|
refreshData = (e) => {},
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const session = getSessionData();
|
||||||
|
const isVendor = session?.user?.role_id == `${import.meta.env.VITE_ROLE_VENDOR}`;
|
||||||
|
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const [historyData, setHistoryData] = useState([]);
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||||
|
const [showPermitSelesai, setShowPermitSelesai] = useState(false);
|
||||||
|
const [actionType, setActionType] = useState(null);
|
||||||
|
const [deskripsi, setDeskripsi] = useState('');
|
||||||
|
const [closeType, setCloseType] = useState('1');
|
||||||
|
|
||||||
|
const pengajuanPermitSelesai = 4;
|
||||||
|
const permitSelesai = 7;
|
||||||
|
|
||||||
|
const fetchHistory = async () => {
|
||||||
|
const permitId = props.permitId;
|
||||||
|
const id = parseInt(permitId);
|
||||||
|
|
||||||
|
if (!permitId || isNaN(id)) {
|
||||||
|
console.error('Permit ID tidak valid:', permitId);
|
||||||
|
message.error('Permit ID tidak valid atau kosong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getStatusHistory(id);
|
||||||
|
|
||||||
|
const mapped =
|
||||||
|
res?.data?.data?.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
color: item.status_permit_color,
|
||||||
|
text: item.status_permit_name,
|
||||||
|
deskripsi: item.deskripsi,
|
||||||
|
date: item.created_at,
|
||||||
|
closed: item.close_type !== null ? item.close_type_name : null,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
setHistoryData(mapped);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API ERROR:', err);
|
||||||
|
message.error('Gagal mengambil riwayat status dari server');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
fetchHistory();
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelesai = () => {
|
||||||
|
setShowPermitSelesai(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApprove = () => {
|
||||||
|
setActionType('approve');
|
||||||
|
setShowConfirmModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = () => {
|
||||||
|
setActionType('reject');
|
||||||
|
setShowConfirmModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitSelesai = async () => {
|
||||||
|
const payload = {
|
||||||
|
status_permit: true,
|
||||||
|
close_type: closeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
setConfirmLoading(true);
|
||||||
|
const response = await approvalPermit(props.permitId, payload);
|
||||||
|
|
||||||
|
if (response?.status === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Pengajuan Selesai',
|
||||||
|
message: `Permit berhasil diajukan sebagai ${
|
||||||
|
closeType === '1' ? 'selesai' : 'belum selesai'
|
||||||
|
}.`,
|
||||||
|
});
|
||||||
|
setIsModalVisible(false);
|
||||||
|
setShowPermitSelesai(false);
|
||||||
|
setCloseType('');
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshData();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.data?.message || 'Proses gagal');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saat mengajukan permit:', err);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: err.message || 'Terjadi kesalahan saat memproses permit.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
setShowPermitSelesai(false);
|
||||||
|
setCloseType('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitApproval = async () => {
|
||||||
|
const payload = {
|
||||||
|
status_permit: actionType === 'approve' ? true : false,
|
||||||
|
deskripsi: deskripsi.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
setConfirmLoading(true);
|
||||||
|
const response = await approvalPermit(props.permitId, payload);
|
||||||
|
|
||||||
|
if (response?.status === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: actionType === 'approve' ? 'Disetujui' : 'Ditolak',
|
||||||
|
message:
|
||||||
|
actionType === 'approve'
|
||||||
|
? 'Permit berhasil disetujui.'
|
||||||
|
: 'Permit berhasil ditolak.',
|
||||||
|
});
|
||||||
|
setIsModalVisible(false);
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setDeskripsi('');
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshData();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.data?.message || 'Proses gagal');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saat menyetujui permit:', err);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: err.message || 'Terjadi kesalahan saat memproses permit.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setDeskripsi('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
onClick={showModal}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: color,
|
||||||
|
borderColor: color,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '5px 16px',
|
||||||
|
fontWeight: 500,
|
||||||
|
...style?.button,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name || 'N/A'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: color || '#999',
|
||||||
|
border: `1px solid ${color || '#ddd'}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: '4px 12px',
|
||||||
|
fontSize: 16,
|
||||||
|
display: 'inline-block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name ?? 'Belum ada status'}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
open={isModalVisible}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleCancel}>Batal</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
{props.status_permit === pengajuanPermitSelesai &&
|
||||||
|
historyData.length >= 0 &&
|
||||||
|
isVendor && (
|
||||||
|
<>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#23a55ade' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button loading={confirmLoading} onClick={handleSelesai}>
|
||||||
|
Selesai
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{props.status_permit !== 0 &&
|
||||||
|
props.status_permit !== pengajuanPermitSelesai &&
|
||||||
|
props.status_permit !== permitSelesai &&
|
||||||
|
historyData.length >= 0 && (
|
||||||
|
<>
|
||||||
|
{canReject && (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#FF4D4Fde' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#FF4D4F',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#FF4D4F',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#FF4D4F',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleReject}>Reject</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
)}
|
||||||
|
{canApprove && (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#23a55ade' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
loading={confirmLoading}
|
||||||
|
onClick={handleApprove}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Divider style={{ margin: '16px 0', borderTop: '2px solid #D9D9D9' }} />
|
||||||
|
|
||||||
|
{historyData.length > 0 ? (
|
||||||
|
[...historyData]
|
||||||
|
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||||
|
.map((item, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
color: item.color,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
border: `1.5px solid ${item.color}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
color: item.color,
|
||||||
|
border: `1.5px solid ${item.color}`,
|
||||||
|
fontSize: 13,
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: 6,
|
||||||
|
fontWeight: '600',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</Tag>
|
||||||
|
{item.date != null && (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{dayjs.utc(item.date).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 13, color: '#333' }}>{item.name}</div>
|
||||||
|
|
||||||
|
{item.closed && (
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#f9f9f9',
|
||||||
|
color: '#000',
|
||||||
|
border: '1px dashed #999',
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
marginRight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Closed: {item.closed}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.deskripsi && (
|
||||||
|
<p style={{ fontSize: 12, color: '#333', margin: 0 }}>
|
||||||
|
{item.deskripsi}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">Belum ada riwayat status.</Text>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={showConfirmModal}
|
||||||
|
title={actionType === 'approve' ? 'Konfirmasi Persetujuan' : 'Konfirmasi Penolakan'}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setDeskripsi('');
|
||||||
|
}}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setDeskripsi('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>,
|
||||||
|
|
||||||
|
<ConfigProvider
|
||||||
|
key="action"
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer:
|
||||||
|
actionType === 'approve' ? '#23a55ade' : '#FF4D4Fde',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: actionType === 'approve' ? '#23a55a' : '#FF4D4F',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor:
|
||||||
|
actionType === 'approve' ? '#23a55a' : '#FF4D4F',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor:
|
||||||
|
actionType === 'approve' ? '#23a55a' : '#FF4D4F',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button key="submit" loading={confirmLoading} onClick={submitApproval}>
|
||||||
|
{actionType === 'approve' ? 'Approve' : 'Reject'}
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p>Silakan isi deskripsi:</p>
|
||||||
|
<TextArea
|
||||||
|
rows={4}
|
||||||
|
value={deskripsi}
|
||||||
|
onChange={(e) => setDeskripsi(e.target.value)}
|
||||||
|
placeholder="Contoh: Disetujui karena dokumen lengkap atau ditolak karena dokumen tidak lengkap."
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={showPermitSelesai}
|
||||||
|
title={'Konfirmasi Pengajuan Selesai'}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowPermitSelesai(false);
|
||||||
|
setCloseType('');
|
||||||
|
}}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
setShowPermitSelesai(false);
|
||||||
|
setCloseType('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>,
|
||||||
|
<ConfigProvider
|
||||||
|
key="action"
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#23a55ade' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button key="submit" loading={confirmLoading} onClick={submitSelesai}>
|
||||||
|
Ajukan Permit Selesai
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p>Status Permit saat ini :</p>
|
||||||
|
<Radio.Group onChange={(e) => setCloseType(e.target.value)} value={closeType}>
|
||||||
|
<Radio value="0">Belum Selesai</Radio>
|
||||||
|
<Radio value="1">Selesai</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusButton;
|
||||||
294
src/components/Global/StatusUserButton.jsx
Normal file
294
src/components/Global/StatusUserButton.jsx
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button, Modal, Divider, Card, Tag, ConfigProvider, Typography } from 'antd';
|
||||||
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from './ToastNotif';
|
||||||
|
import { approvalUser } from '../../api/user-admin';
|
||||||
|
import { toAppDateFormatter } from './Formatter';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const StatusUserButton = ({ color, name, data, readOnly, style }) => {
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColor = data?.warna || color || '#999';
|
||||||
|
const statusName = data?.status_name || name || 'N/A';
|
||||||
|
const userCreated = data?.user_add_name || 'Pengguna tidak dikenal';
|
||||||
|
const userUpdated = data?.user_upd_name || 'Pengguna tidak dikenal';
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
approve_user: true,
|
||||||
|
};
|
||||||
|
const response = await approvalUser(data?.id_register, payload);
|
||||||
|
|
||||||
|
if (response?.data?.statusCode === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'User berhasil di-approve.',
|
||||||
|
});
|
||||||
|
setIsModalVisible(false);
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.data?.message || 'Gagal approve user.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saat approve user:', err);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Terjadi kesalahan pada server.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
NotifConfirmDialog({
|
||||||
|
icon: 'question',
|
||||||
|
title: 'Konfirmasi Penolakan',
|
||||||
|
message: 'Apakah kamu yakin ingin menolak permintaan ini?',
|
||||||
|
confirmButtonText: 'Reject',
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
approve_user: false,
|
||||||
|
};
|
||||||
|
const response = await approvalUser(data.id_register, payload);
|
||||||
|
|
||||||
|
if (response?.data?.statusCode === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Ditolak',
|
||||||
|
message: 'User berhasil ditolak.',
|
||||||
|
});
|
||||||
|
setIsModalVisible(false);
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.message || 'Gagal reject user.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Terjadi kesalahan pada server.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
onClick={showModal}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: statusColor,
|
||||||
|
borderColor: statusColor,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '4px 16px',
|
||||||
|
fontWeight: 500,
|
||||||
|
...style?.button,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statusName}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: statusColor,
|
||||||
|
border: `1px solid ${statusColor}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: '4px 12px',
|
||||||
|
fontSize: 16,
|
||||||
|
display: 'inline-block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statusName}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
open={isModalVisible}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleCancel}>Batal</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
{data?.status_register === 1 && (
|
||||||
|
<>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer: '#FF4D4Fde',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#FF4D4F',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#FF4D4F',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#FF4D4F',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleReject}>Reject</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer: '#23a55ade',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button loading={confirmLoading} onClick={handleApprove}>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Divider style={{ margin: '16px 0', borderTop: '2px solid #D9D9D9' }} />
|
||||||
|
|
||||||
|
{data ? (
|
||||||
|
<>
|
||||||
|
{data.updated_at !== data.created_at && (
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
color: '#FF6E35',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
border: `1.5px solid #FF6E35`,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
color: '#FF6E35',
|
||||||
|
border: `1.5px solid #FF6E35`,
|
||||||
|
fontSize: 13,
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: 6,
|
||||||
|
fontWeight: '500',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Updated at
|
||||||
|
</Tag>
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, fontWeight: '500' }}
|
||||||
|
>
|
||||||
|
{toAppDateFormatter(data.updated_at)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: '#333' }}>
|
||||||
|
Diubah terakhir oleh <b>{userUpdated}</b>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
color: '#3498DB',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
border: `1.5px solid #3498DB`,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
color: '#3498DB',
|
||||||
|
border: `1.5px solid #3498DB`,
|
||||||
|
fontSize: 13,
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: 6,
|
||||||
|
fontWeight: '500',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created at
|
||||||
|
</Tag>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12, fontWeight: '500' }}>
|
||||||
|
{toAppDateFormatter(data.created_at)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: '#333' }}>
|
||||||
|
Dibuat oleh <b>{userCreated}</b>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">Belum ada riwayat status.</Text>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusUserButton;
|
||||||
281
src/components/Global/TableList.jsx
Normal file
281
src/components/Global/TableList.jsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import React, { memo, useState, useEffect, useRef } from 'react';
|
||||||
|
import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { setFilterData } from './DataFilter';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const defCard = {
|
||||||
|
r1: {
|
||||||
|
style: { fontWeight: 'bold', fontSize: 13 },
|
||||||
|
type: 'primary',
|
||||||
|
color: '',
|
||||||
|
text: 'Cold Work Permit',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
r2: {
|
||||||
|
style: { marginLeft: 8, fontSize: 13 },
|
||||||
|
type: 'primary',
|
||||||
|
color: 'success',
|
||||||
|
text: 'Pengajuan',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
r3: {
|
||||||
|
style: { fontSize: 12 },
|
||||||
|
type: 'secondary',
|
||||||
|
color: '',
|
||||||
|
text: 'No. IVR/20250203/XXV/III',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
r4: {
|
||||||
|
style: { fontSize: 12 },
|
||||||
|
type: 'primary',
|
||||||
|
color: '',
|
||||||
|
text: '3 Feb 2025',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
r5: {
|
||||||
|
style: { fontSize: 12 },
|
||||||
|
type: 'primary',
|
||||||
|
color: '',
|
||||||
|
text: 'Lokasi Gudang Robang',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
r6: {
|
||||||
|
style: { fontSize: 12 },
|
||||||
|
type: 'primary',
|
||||||
|
color: '',
|
||||||
|
text: 'maka tambahkan user tersebut dalam user_partner dengan partner baru yang ditambahkan diatas',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
action: (e) => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableList = memo(function TableList({
|
||||||
|
getData,
|
||||||
|
queryParams,
|
||||||
|
columns,
|
||||||
|
triger,
|
||||||
|
mobile,
|
||||||
|
rowSelection = null,
|
||||||
|
}) {
|
||||||
|
const [gridLoading, setGridLoading] = useState(false);
|
||||||
|
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [pagingResponse, setPagingResponse] = useState({
|
||||||
|
totalData: '',
|
||||||
|
perPage: '',
|
||||||
|
totalPage: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filter(1, 10);
|
||||||
|
}, [triger]);
|
||||||
|
|
||||||
|
const filter = async (currentPage, pageSize) => {
|
||||||
|
setGridLoading(true);
|
||||||
|
|
||||||
|
const paging = {
|
||||||
|
page: currentPage,
|
||||||
|
limit: pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const param = new URLSearchParams({ ...paging, ...queryParams });
|
||||||
|
|
||||||
|
const resData = await getData(param);
|
||||||
|
if (resData) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setGridLoading(false);
|
||||||
|
}, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(resData.data.data ?? []);
|
||||||
|
setFilterData(resData.data.data ?? []);
|
||||||
|
|
||||||
|
if (resData.status == 200) {
|
||||||
|
setPagingResponse({
|
||||||
|
totalData: resData.data.total,
|
||||||
|
perPage: resData.data.paging.page_total,
|
||||||
|
totalPage: resData.data.paging.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
setPagination((prev) => ({
|
||||||
|
...prev,
|
||||||
|
current: resData.data.paging.page,
|
||||||
|
limit: resData.data.paging.limit,
|
||||||
|
total: resData.data.paging.total,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaginationChange = (page, pageSize) => {
|
||||||
|
setPagination((prev) => ({
|
||||||
|
...prev,
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
}));
|
||||||
|
filter(page, pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
|
||||||
|
const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isMobile && mobile ? (
|
||||||
|
<Row gutter={24}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
|
{data.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
title={
|
||||||
|
(mobile.r1 || mobile.r2) && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mobile.r1 && (
|
||||||
|
<span style={mobile.r1.style ?? {}}>
|
||||||
|
{item[mobile.r1.name] ?? mobile.r1.text ?? ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{mobile.r2 && (
|
||||||
|
<Tag
|
||||||
|
color={mobile.r2.color ?? ''}
|
||||||
|
style={mobile.r2.style ?? {}}
|
||||||
|
>
|
||||||
|
{item[mobile.r2.name] ?? mobile.r2.text ?? ''}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{mobile.r3 && mobile.r4 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
type={mobile.r3 ? mobile.r3.type ?? 'primary' : ''}
|
||||||
|
style={mobile.r3 ? mobile.r3.style ?? {} : {}}
|
||||||
|
>
|
||||||
|
{item[mobile.r3 ? mobile.r3.name : ''] ?? ''}
|
||||||
|
</Text>
|
||||||
|
<Text style={mobile.r4 ? mobile.r4.style ?? {} : {}}>
|
||||||
|
{item[mobile.r4 ? mobile.r4.name : ''] ?? ''}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{mobile.r5 && (
|
||||||
|
<Text
|
||||||
|
type={mobile.r5 ? mobile.r5.type ?? 'secondary' : ''}
|
||||||
|
style={mobile.r5 ? mobile.r5.style ?? {} : {}}
|
||||||
|
>
|
||||||
|
{item[mobile.r5 ? mobile.r5.name : ''] ?? ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{mobile.r6 && (
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
type={mobile.r6 ? mobile.r6.type ?? 'primary' : ''}
|
||||||
|
style={mobile.r6 ? mobile.r6.style ?? {} : {}}
|
||||||
|
>
|
||||||
|
{item[mobile.r6 ? mobile.r6.name : ''] ?? ''}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
borderTop: '1px solid #f0f0f0',
|
||||||
|
paddingTop: 8,
|
||||||
|
textAlign: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
mobile.action(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<Row gutter={24}>
|
||||||
|
{/* TABLE */}
|
||||||
|
<Table
|
||||||
|
rowSelection={rowSelection || null}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data.map((item, index) => ({ ...item, key: index }))}
|
||||||
|
pagination={false}
|
||||||
|
loading={gridLoading}
|
||||||
|
scroll={{
|
||||||
|
y: 520,
|
||||||
|
x: 1300,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* PAGINATION */}
|
||||||
|
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||||
|
<Row justify="space-between" align="middle">
|
||||||
|
<Col>
|
||||||
|
<div>
|
||||||
|
Menampilkan {pagingResponse.totalData} Data dari{' '}
|
||||||
|
{pagingResponse.perPage} Halaman
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Pagination
|
||||||
|
showSizeChanger
|
||||||
|
onChange={handlePaginationChange}
|
||||||
|
onShowSizeChange={handlePaginationChange}
|
||||||
|
current={pagination.current}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
total={pagination.total}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TableList;
|
||||||
69
src/components/Global/ToastNotif.jsx
Normal file
69
src/components/Global/ToastNotif.jsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
const NotifAlert = ({ icon, title, message }) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: icon,
|
||||||
|
title: title,
|
||||||
|
text: message,
|
||||||
|
showConfirmButton: false,
|
||||||
|
position: 'center',
|
||||||
|
timer: 2000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotifOk = ({ icon, title, message }) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: icon,
|
||||||
|
title: title,
|
||||||
|
text: message,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotifConfirmDialog = ({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
confirmButtonText = 'Hapus',
|
||||||
|
}) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: icon,
|
||||||
|
title: title,
|
||||||
|
text: message,
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: '#23A55A',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
confirmButtonColor: '#d33000',
|
||||||
|
confirmButtonText: confirmButtonText,
|
||||||
|
reverseButtons: true,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
onConfirm();
|
||||||
|
} else if (result.dismiss) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const QuestionConfirmSubmit = ({ icon, title, message, onConfirm, onCancel }) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: icon,
|
||||||
|
title: title,
|
||||||
|
text: message,
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: '#23A55A',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
confirmButtonColor: '#d33000',
|
||||||
|
confirmButtonText: 'Proses',
|
||||||
|
reverseButtons: true,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
onConfirm();
|
||||||
|
} else if (result.dismiss) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotifAlert, NotifOk, NotifConfirmDialog, QuestionConfirmSubmit };
|
||||||
81
src/components/Global/headerReport.jsx
Normal file
81
src/components/Global/headerReport.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Row, Col, Button, ConfigProvider, Divider, Typography } from "antd";
|
||||||
|
import { ArrowLeftOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const {Text} = Typography;
|
||||||
|
|
||||||
|
const HeaderReport = ({
|
||||||
|
title,
|
||||||
|
loadingPilihDataStep1,
|
||||||
|
handleSelectData,
|
||||||
|
step
|
||||||
|
})=>{
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<Row style={{ justifyContent: "space-between" }}>
|
||||||
|
<Col>
|
||||||
|
<Row>
|
||||||
|
<Text style={{fontSize:'18px'}} strong>{title}</Text>
|
||||||
|
<div style={{width:'20px'}}></div>
|
||||||
|
<div style={{
|
||||||
|
width:'95px',
|
||||||
|
border:'1px solid #e9f6ef',
|
||||||
|
borderRadius:'5px',
|
||||||
|
backgroundColor:'#e9f6ef',
|
||||||
|
color:'#23a55a',
|
||||||
|
padding:'3px',
|
||||||
|
fontSize:'16px',
|
||||||
|
fontWeight:'bold'
|
||||||
|
}}>
|
||||||
|
{step}
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer: "#eff0f5",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: "white",
|
||||||
|
defaultColor: "#000000",
|
||||||
|
defaultBorderColor: "#000000",
|
||||||
|
defaultHoverColor: "#000000",
|
||||||
|
defaultHoverBorderColor: "#000000",
|
||||||
|
defaultHoverColor: "#000000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button icon={<ArrowLeftOutlined />}>Batal</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Divider/>
|
||||||
|
<Row style={{ justifyContent: "space-between" }}>
|
||||||
|
<Col>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer: "#209652",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: "#23a55a",
|
||||||
|
defaultColor: "#FFFFFF",
|
||||||
|
defaultBorderColor: "#23a55a",
|
||||||
|
defaultHoverColor: "#FFFFFF",
|
||||||
|
defaultHoverBorderColor: "#209652",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button loading={loadingPilihDataStep1} onClick={handleSelectData}><SearchOutlined /> Pilih Data</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default HeaderReport;
|
||||||
28
src/components/loading/Loading.jsx
Normal file
28
src/components/loading/Loading.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { memo } from 'react'
|
||||||
|
import './loading.css'
|
||||||
|
|
||||||
|
|
||||||
|
const Loading = memo(function Loading() {
|
||||||
|
return(
|
||||||
|
<div className='mask'>
|
||||||
|
<div className="main">
|
||||||
|
<div className="loader">
|
||||||
|
<p className="text">
|
||||||
|
<span className="letter letter1">L</span>
|
||||||
|
<span className="letter letter2">o</span>
|
||||||
|
<span className="letter letter3">a</span>
|
||||||
|
<span className="letter letter4">d</span>
|
||||||
|
<span className="letter letter5">i</span>
|
||||||
|
<span className="letter letter6">n</span>
|
||||||
|
<span className="letter letter7">g</span>
|
||||||
|
<span className="letter letter8">.</span>
|
||||||
|
<span className="letter letter9">.</span>
|
||||||
|
<span className="letter letter10">.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Loading
|
||||||
80
src/components/loading/loading.css
Normal file
80
src/components/loading/loading.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
.mask {
|
||||||
|
/* background-color: rgba(0, 0, 0, .6); */
|
||||||
|
background-color: rgba(255, 255, 255, .6);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes letter {
|
||||||
|
0% {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter {
|
||||||
|
animation: letter 1s infinite;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter1 {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter2 {
|
||||||
|
animation-delay: -0.9s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter3 {
|
||||||
|
animation-delay: -0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter4 {
|
||||||
|
animation-delay: -0.7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter5 {
|
||||||
|
animation-delay: -0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter6 {
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter7 {
|
||||||
|
animation-delay: -0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter8 {
|
||||||
|
animation-delay: -0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter9 {
|
||||||
|
animation-delay: -0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter10 {
|
||||||
|
animation-delay: -0.1s;
|
||||||
|
}
|
||||||
21
src/index.css
Normal file
21
src/index.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
/* color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424; */
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
15
src/layout/LayoutBreadcrumb.jsx
Normal file
15
src/layout/LayoutBreadcrumb.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
const BreadcrumbContext = createContext();
|
||||||
|
|
||||||
|
export const BreadcrumbProvider = ({ children }) => {
|
||||||
|
const [breadcrumbItems, setBreadcrumbItems] = useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BreadcrumbContext.Provider value={{ breadcrumbItems, setBreadcrumbItems }}>
|
||||||
|
{children}
|
||||||
|
</BreadcrumbContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBreadcrumb = () => useContext(BreadcrumbContext);
|
||||||
18
src/layout/LayoutFooter.jsx
Normal file
18
src/layout/LayoutFooter.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Layout } from 'antd'
|
||||||
|
import Link from 'antd/es/typography/Link';
|
||||||
|
|
||||||
|
const { Footer } = Layout;
|
||||||
|
const LayoutFooter = () => {
|
||||||
|
return (
|
||||||
|
<Footer
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* SYPIU GGCP */}
|
||||||
|
</Footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutFooter
|
||||||
119
src/layout/LayoutHeader.jsx
Normal file
119
src/layout/LayoutHeader.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Layout, theme, Space, Typography, Breadcrumb, Button } from 'antd';
|
||||||
|
import { UserOutlined } from '@ant-design/icons';
|
||||||
|
import handleLogOut from '../Utils/Auth/Logout';
|
||||||
|
import { useBreadcrumb } from './LayoutBreadcrumb';
|
||||||
|
import { decryptData } from '../components/Global/Formatter';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const { Link, Text } = Typography;
|
||||||
|
const { Header } = Layout;
|
||||||
|
|
||||||
|
const LayoutHeader = () => {
|
||||||
|
const { breadcrumbItems } = useBreadcrumb();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
token: { colorBgContainer, colorBorder, colorText },
|
||||||
|
} = theme.useToken();
|
||||||
|
|
||||||
|
// Ambil data user dari localStorage dan dekripsi
|
||||||
|
const sessionData = localStorage.getItem('session');
|
||||||
|
const userData = sessionData ? decryptData(sessionData) : null;
|
||||||
|
// console.log(userData);
|
||||||
|
|
||||||
|
const roleName = userData?.user?.approval || userData?.user?.partner_name || 'Guest';
|
||||||
|
|
||||||
|
const userName = userData?.user?.name || userData?.user?.username || 'User';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header
|
||||||
|
style={{
|
||||||
|
background: colorBgContainer,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
rowGap: 10,
|
||||||
|
paddingTop:15,
|
||||||
|
paddingBottom: 20,
|
||||||
|
paddingLeft: 24,
|
||||||
|
paddingRight: 24,
|
||||||
|
minHeight: 100,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: colorText,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Login AS {roleName}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: '#f5f5f5',
|
||||||
|
border: `1px solid ${colorBorder}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '4px 12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserOutlined style={{ fontSize: 16, color: colorText }} />
|
||||||
|
<Text style={{ marginLeft: 8, color: colorText }} strong>
|
||||||
|
{userName}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
handleLogOut();
|
||||||
|
navigate('/signin');
|
||||||
|
}}
|
||||||
|
aria-label="Log out from the application"
|
||||||
|
style={{
|
||||||
|
color: colorText,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<div style={{ width: '100%', maxWidth: '50%', textAlign: 'left' }}>
|
||||||
|
<Breadcrumb
|
||||||
|
style={{
|
||||||
|
marginLeft: '20px',
|
||||||
|
marginTop: '20px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}
|
||||||
|
items={breadcrumbItems || []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutHeader;
|
||||||
69
src/layout/LayoutLogo.jsx
Normal file
69
src/layout/LayoutLogo.jsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Image } from 'antd';
|
||||||
|
import logoPiu from '../assets/freepik/LOGOPIU.png';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const LayoutLogo = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: '1rem auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#001529',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle at center, rgba(255,255,255,0.95), rgba(220,220,220,0.5))',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 160,
|
||||||
|
height: 160,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
boxShadow: '0 6px 20px rgba(0, 0, 0, 0.2)',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Ring sebelum logo utama */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: '5px solid #ffffffff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 160,
|
||||||
|
height: 160,
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: '10px',
|
||||||
|
padding: '4px',
|
||||||
|
mixBlendMode: 'normal',
|
||||||
|
opacity: 1,
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={logoPiu}
|
||||||
|
alt="logo"
|
||||||
|
width={140}
|
||||||
|
height={100}
|
||||||
|
preview={false}
|
||||||
|
style={{
|
||||||
|
filter: 'drop-shadow(0 0 3px rgba(0, 0, 0, 0.2))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutLogo;
|
||||||
110
src/layout/LayoutMenu.jsx
Normal file
110
src/layout/LayoutMenu.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Menu, Typography, Image } from 'antd';
|
||||||
|
import { getSessionData } from '../components/Global/Formatter';
|
||||||
|
import { HomeOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
AntDesignOutlined,
|
||||||
|
ShoppingCartOutlined,
|
||||||
|
ShoppingOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
RollbackOutlined,
|
||||||
|
ProductOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const allItems = [
|
||||||
|
{
|
||||||
|
key: 'home',
|
||||||
|
icon: <HomeOutlined style={{fontSize:'19px'}} />,
|
||||||
|
label: <Link to="/dashboard/home" className='fontMenus'>Home</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'master',
|
||||||
|
icon: <DatabaseOutlined style={{fontSize:'19px'}} />,
|
||||||
|
label: 'Master',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'master-device',
|
||||||
|
icon: <DatabaseOutlined style={{fontSize:'19px'}} />,
|
||||||
|
label: <Link to="/master/device">Device</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LayoutMenu = () => {
|
||||||
|
const [stateOpenKeys, setStateOpenKeys] = useState(['home']);
|
||||||
|
|
||||||
|
const getLevelKeys = items1 => {
|
||||||
|
const key = {};
|
||||||
|
const func = (items2, level = 1) => {
|
||||||
|
items2.forEach(item => {
|
||||||
|
if (item.key) {
|
||||||
|
key[item.key] = level;
|
||||||
|
}
|
||||||
|
if (item.children) {
|
||||||
|
func(item.children, level + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
func(items1);
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelKeys = getLevelKeys(allItems);
|
||||||
|
|
||||||
|
const onOpenChange = openKeys => {
|
||||||
|
const currentOpenKey = openKeys.find(key => stateOpenKeys.indexOf(key) === -1);
|
||||||
|
if (currentOpenKey !== undefined) {
|
||||||
|
const repeatIndex = openKeys.filter(key => key !== currentOpenKey).findIndex(key => levelKeys[key] === levelKeys[currentOpenKey]);
|
||||||
|
setStateOpenKeys(
|
||||||
|
openKeys.filter((_, index) => index !== repeatIndex).filter(key => levelKeys[key] <= levelKeys[currentOpenKey]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setStateOpenKeys(openKeys);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const session = getSessionData();
|
||||||
|
const isAdmin = session?.user?.user_id;
|
||||||
|
|
||||||
|
const karyawan = ()=>{
|
||||||
|
return allItems.filter(
|
||||||
|
item => item.key !== 'setting'
|
||||||
|
// tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan
|
||||||
|
// && item.key !== 'master'
|
||||||
|
// && item.key !== 'master'
|
||||||
|
).map(item=>{
|
||||||
|
if(item.key === 'master'){
|
||||||
|
return{
|
||||||
|
...item,
|
||||||
|
// buka command dibawah jika terdapat sub menu yang di sembunyikan
|
||||||
|
// children: item.children.filter(
|
||||||
|
// child => child.key !== 'master-product'
|
||||||
|
// tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan
|
||||||
|
// && child.key !== 'master-service'
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const items = isAdmin === 1 ? allItems : karyawan();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
theme="dark"
|
||||||
|
mode="inline"
|
||||||
|
items={items}
|
||||||
|
defaultSelectedKeys={['home']}
|
||||||
|
openKeys={stateOpenKeys}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default LayoutMenu;
|
||||||
25
src/layout/LayoutSidebar.jsx
Normal file
25
src/layout/LayoutSidebar.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Layout } from 'antd';
|
||||||
|
import LayoutLogo from './LayoutLogo';
|
||||||
|
import LayoutMenu from './LayoutMenu';
|
||||||
|
|
||||||
|
const { Sider } = Layout;
|
||||||
|
const LayoutSidebar = () => {
|
||||||
|
return (
|
||||||
|
<Sider width={300}
|
||||||
|
breakpoint="lg"
|
||||||
|
collapsedWidth="0"
|
||||||
|
onBreakpoint={(broken) => {
|
||||||
|
// console.log(broken);
|
||||||
|
}}
|
||||||
|
onCollapse={(collapsed, type) => {
|
||||||
|
// console.log(collapsed, type);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LayoutLogo />
|
||||||
|
<LayoutMenu />
|
||||||
|
</Sider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutSidebar
|
||||||
46
src/layout/MainLayout.jsx
Normal file
46
src/layout/MainLayout.jsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Layout, theme } from 'antd';
|
||||||
|
import LayoutFooter from './LayoutFooter';
|
||||||
|
import LayoutHeader from './LayoutHeader';
|
||||||
|
import LayoutSidebar from './LayoutSidebar';
|
||||||
|
|
||||||
|
|
||||||
|
const { Content } = Layout;
|
||||||
|
|
||||||
|
const MainLayout = ({ children }) => {
|
||||||
|
const {
|
||||||
|
token: { colorBgContainer, borderRadiusLG },
|
||||||
|
} = theme.useToken();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ height: '100vh' }}>
|
||||||
|
<LayoutSidebar />
|
||||||
|
<Layout
|
||||||
|
style={{
|
||||||
|
overflow: 'auto',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<LayoutHeader />
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
margin: '24px 16px 0',
|
||||||
|
flex: '1 0 auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
minHeight: '100%',
|
||||||
|
background: colorBgContainer,
|
||||||
|
borderRadius: borderRadiusLG,
|
||||||
|
}}
|
||||||
|
> */}
|
||||||
|
{children}
|
||||||
|
{/* </div> */}
|
||||||
|
</Content>
|
||||||
|
{/* <LayoutFooter /> */}
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MainLayout;
|
||||||
13
src/main.jsx
Normal file
13
src/main.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
import { BreadcrumbProvider } from './layout/LayoutBreadcrumb.jsx';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BreadcrumbProvider>
|
||||||
|
<App />
|
||||||
|
</BreadcrumbProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
461
src/pages/auth/Registration.jsx
Normal file
461
src/pages/auth/Registration.jsx
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Form,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Space,
|
||||||
|
Upload,
|
||||||
|
Divider,
|
||||||
|
Tooltip,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
UploadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
IdcardOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
MailOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
const { Item } = Form;
|
||||||
|
const { Option } = Select;
|
||||||
|
import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { register, uploadFile, checkUsername } from '../../api/auth';
|
||||||
|
import { NotifAlert } from '../../components/Global/ToastNotif';
|
||||||
|
|
||||||
|
const Registration = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [fileListKontrak, setFileListKontrak] = useState([]);
|
||||||
|
const [fileListHsse, setFileListHsse] = useState([]);
|
||||||
|
const [fileListIcon, setFileListIcon] = useState([]);
|
||||||
|
|
||||||
|
// Daftar jenis vendor
|
||||||
|
const vendorTypes = [
|
||||||
|
{ vendor_type: 1, vendor_type_name: 'One-Time' },
|
||||||
|
{ vendor_type: 2, vendor_type_name: 'Rutin' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onFinish = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (!fileListKontrak.length || !fileListHsse.length) {
|
||||||
|
message.error('Harap unggah Lampiran Kontrak Kerja dan HSSE Plan!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('path_kontrak', fileListKontrak[0].originFileObj);
|
||||||
|
formData.append('path_hse_plant', fileListHsse[0].originFileObj);
|
||||||
|
if (fileListIcon.length) {
|
||||||
|
formData.append('path_icon', fileListIcon[0].originFileObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadResponse = await uploadFile(formData);
|
||||||
|
|
||||||
|
if (!uploadResponse.data?.pathKontrak && !uploadResponse.data?.pathHsePlant) {
|
||||||
|
message.error(uploadResponse.message || 'Gagal mengunggah file.');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ username: values.username });
|
||||||
|
const usernameCheck = await checkUsername(params);
|
||||||
|
if (usernameCheck.data.data && usernameCheck.data.data.available === false) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: usernameCheck.data.message || 'Terjadi kesalahan, silakan coba lagi',
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerData = {
|
||||||
|
nama_perusahaan: values.namaPerusahaan,
|
||||||
|
no_kontak_wo: values.noKontakWo,
|
||||||
|
path_kontrak: uploadResponse.data.pathKontrak || '',
|
||||||
|
durasi: values.durasiPekerjaan,
|
||||||
|
nilai_csms: values.nilaiCsms.toString(),
|
||||||
|
vendor_type: values.jenisVendor, // Tambahkan jenis vendor ke registerData
|
||||||
|
path_hse_plant: uploadResponse.data.pathHsePlant || '',
|
||||||
|
nama_leader: values.penanggungJawab,
|
||||||
|
no_identitas: values.noIdentitas,
|
||||||
|
no_hp: values.noHandphone,
|
||||||
|
email_register: values.username,
|
||||||
|
password_register: values.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await register(registerData);
|
||||||
|
|
||||||
|
if (response.data?.id_register) {
|
||||||
|
message.success('Data berhasil disimpan!');
|
||||||
|
|
||||||
|
try {
|
||||||
|
form.resetFields();
|
||||||
|
setFileListKontrak([]);
|
||||||
|
setFileListHsse([]);
|
||||||
|
setFileListIcon([]);
|
||||||
|
|
||||||
|
navigate('/registration-submitted');
|
||||||
|
} catch (postSuccessError) {
|
||||||
|
message.warning(
|
||||||
|
'Registrasi berhasil, tetapi ada masalah setelahnya. Silakan ke halaman login secara manual.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(response.message || 'Pendaftaran gagal, silakan coba lagi.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saat registrasi:', error);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: error.message || 'Terjadi kesalahan, silakan coba lagi',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
setFileListKontrak([]);
|
||||||
|
setFileListHsse([]);
|
||||||
|
setFileListIcon([]);
|
||||||
|
navigate('/signin');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeKontrak = ({ fileList }) => {
|
||||||
|
setFileListKontrak(fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeHsse = ({ fileList }) => {
|
||||||
|
setFileListHsse(fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeIcon = ({ fileList }) => {
|
||||||
|
setFileListIcon(fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file, fieldname) => {
|
||||||
|
const isValidType = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/png',
|
||||||
|
fieldname !== 'path_icon' ? 'application/pdf' : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.includes(file.type);
|
||||||
|
const isNotEmpty = file.size > 0;
|
||||||
|
const isSizeValid = file.size / 1024 / 1024 < 10;
|
||||||
|
|
||||||
|
if (!isValidType) {
|
||||||
|
message.error(
|
||||||
|
`Hanya file ${
|
||||||
|
fieldname === 'path_icon' ? 'JPG/PNG' : 'PDF/JPG/PNG'
|
||||||
|
} yang diperbolehkan!`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isNotEmpty) {
|
||||||
|
message.error('File tidak boleh kosong!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isSizeValid) {
|
||||||
|
message.error('Ukuran file maksimal 10MB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
backgroundImage: `url(${sypiu_ggcp})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
padding: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 800,
|
||||||
|
background: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)',
|
||||||
|
padding: '24px',
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<h2 style={{ margin: 0, color: '#1a3c34' }}>Formulir Pendaftaran</h2>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<InfoCircleOutlined />}
|
||||||
|
onClick={() => navigate('/signin')}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={onFinish}
|
||||||
|
layout="horizontal"
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
style={{ maxWidth: 800 }}
|
||||||
|
>
|
||||||
|
{/* Informasi Perusahaan */}
|
||||||
|
<Divider
|
||||||
|
orientation="left"
|
||||||
|
orientationMargin={0}
|
||||||
|
style={{
|
||||||
|
color: '#23A55A',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginLeft: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Informasi Perusahaan
|
||||||
|
</Divider>
|
||||||
|
<Item
|
||||||
|
label="Nama Perusahaan"
|
||||||
|
name="namaPerusahaan"
|
||||||
|
rules={[{ required: true, message: 'Masukkan Nama Perusahaan!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="Masukkan Nama Perusahaan"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="Durasi Pekerjaan (Hari)"
|
||||||
|
name="durasiPekerjaan"
|
||||||
|
rules={[{ required: true, message: 'Masukkan Durasi Pekerjaan!' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Masukkan Durasi Pekerjaan"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="No Kontrak Kerja / Agreement"
|
||||||
|
name="noKontakWo"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Masukkan No Kontrak Kerja / Agreement!' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
placeholder="Masukkan No Kontrak Kerja / Agreement"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="Lampiran Kontrak Kerja"
|
||||||
|
name="lampiranKontrak"
|
||||||
|
rules={[{ required: true, message: 'Unggah Lampiran Kontrak Kerja!' }]}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
beforeUpload={(file) => beforeUpload(file, 'path_kontrak')}
|
||||||
|
fileList={fileListKontrak}
|
||||||
|
onChange={handleChangeKontrak}
|
||||||
|
maxCount={1}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />} size="large">
|
||||||
|
Unggah PDF/JPG
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="HSSE Plan"
|
||||||
|
name="hssePlan"
|
||||||
|
rules={[{ required: true, message: 'Unggah HSSE Plan!' }]}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
beforeUpload={(file) => beforeUpload(file, 'path_hse_plant')}
|
||||||
|
fileList={fileListHsse}
|
||||||
|
onChange={handleChangeHsse}
|
||||||
|
maxCount={1}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />} size="large">
|
||||||
|
Unggah PDF/JPG
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="Nilai CSMS"
|
||||||
|
name="nilaiCsms"
|
||||||
|
rules={[{ required: true, message: 'Masukkan Nilai CSMS!' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Masukkan Nilai CSMS"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="Jenis Vendor"
|
||||||
|
name="jenisVendor"
|
||||||
|
rules={[{ required: true, message: 'Pilih Jenis Vendor!' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih Jenis Vendor"
|
||||||
|
size="large"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{vendorTypes.map((vendor) => (
|
||||||
|
<Option key={vendor.vendor_type} value={vendor.vendor_type}>
|
||||||
|
{vendor.vendor_type_name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Item>
|
||||||
|
|
||||||
|
{/* Informasi Penanggung Jawab */}
|
||||||
|
<Divider
|
||||||
|
orientation="left"
|
||||||
|
orientationMargin={0}
|
||||||
|
style={{
|
||||||
|
color: '#23A55A',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginLeft: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Informasi Penanggung Jawab
|
||||||
|
</Divider>
|
||||||
|
<Item
|
||||||
|
label="Nama Penanggung Jawab"
|
||||||
|
name="penanggungJawab"
|
||||||
|
rules={[{ required: true, message: 'Masukkan Nama Penanggung Jawab!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="Masukkan Nama Penanggung Jawab"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="No Handphone"
|
||||||
|
name="noHandphone"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Masukkan No Handphone!' },
|
||||||
|
{
|
||||||
|
pattern: /^(\+62|0)[0-9]{9,12}$/,
|
||||||
|
message:
|
||||||
|
'Format nomor telepon tidak valid! (Contoh: +62.... atau 0....)',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<PhoneOutlined />}
|
||||||
|
placeholder="Masukkan No Handphone (+62)"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="No Identitas"
|
||||||
|
name="noIdentitas"
|
||||||
|
rules={[{ required: true, message: 'Masukkan No Identitas!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<IdcardOutlined />}
|
||||||
|
placeholder="Masukkan No Identitas"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
|
||||||
|
{/* Akun Pengguna */}
|
||||||
|
<Divider
|
||||||
|
orientation="left"
|
||||||
|
orientationMargin={0}
|
||||||
|
style={{
|
||||||
|
color: '#23A55A',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginLeft: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Akun Pengguna (digunakan sebagai user login SYPIU)
|
||||||
|
</Divider>
|
||||||
|
<Item
|
||||||
|
label="Email"
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Masukkan Email!' },
|
||||||
|
{ type: 'email', message: 'Format email tidak valid!' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MailOutlined />}
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Masukkan Password!' },
|
||||||
|
{ min: 6, message: 'Password minimal 6 karakter!' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="Masukkan Password"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
|
||||||
|
{/* Tombol */}
|
||||||
|
<Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||||
|
<Space style={{ marginTop: '24px', width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
size="large"
|
||||||
|
loading={loading}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#23A55A',
|
||||||
|
borderColor: '#23A55A',
|
||||||
|
width: 120,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCancel} size="large" style={{ width: 120 }}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Registration;
|
||||||
187
src/pages/auth/SignIn.jsx
Normal file
187
src/pages/auth/SignIn.jsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { Flex, Input, Form, Button, Card, Space, Image } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import handleSignIn from '../../Utils/Auth/SignIn';
|
||||||
|
import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
|
||||||
|
import logo from 'assets/freepik/LOGOPIU.png';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { NotifAlert } from '../../components/Global/ToastNotif';
|
||||||
|
import { decryptData } from '../../components/Global/Formatter';
|
||||||
|
|
||||||
|
const SignIn = () => {
|
||||||
|
const [captchaSvg, setCaptchaSvg] = React.useState('');
|
||||||
|
const [userInput, setUserInput] = React.useState('');
|
||||||
|
const [message, setMessage] = React.useState('');
|
||||||
|
const [captchaText, setcaptchaText] = React.useState('');
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// let url = `${import.meta.env.VITE_API_SERVER}/users`;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchCaptcha();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch the CAPTCHA SVG from the backend
|
||||||
|
const fetchCaptcha = async () => {
|
||||||
|
try {
|
||||||
|
// let url = `${import.meta.env.VITE_API_SERVER}/operation`
|
||||||
|
// const response = await fetch('http://localhost:9528/generate-captcha');
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_SERVER}/auth/generate-captcha`,
|
||||||
|
{
|
||||||
|
credentials: 'include', // Wajib untuk mengirim cookie
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ambil header
|
||||||
|
const captchaToken = response.headers.get('X-Captcha-Token');
|
||||||
|
|
||||||
|
// console.log('Captcha Token:', decryptData(captchaToken));
|
||||||
|
|
||||||
|
setcaptchaText(decryptData(captchaToken));
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
setCaptchaSvg(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching CAPTCHA:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnSubmit = async (e) => {
|
||||||
|
// console.log('Received values of form: ', e);
|
||||||
|
// e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// const response = await fetch(`${import.meta.env.VITE_API_SERVER}/auth/verify-captcha`, {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// credentials: 'include', // WAJIB: Agar cookie CAPTCHA dikirim
|
||||||
|
// body: JSON.stringify({ captcha: userInput }),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const data = await response.json();
|
||||||
|
// console.log(data);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
success: captchaText === userInput ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('CAPTCHA verified successfully!');
|
||||||
|
await handleSignIn(e);
|
||||||
|
} else {
|
||||||
|
setMessage('CAPTCHA verification failed. Try again.');
|
||||||
|
fetchCaptcha(); // Refresh CAPTCHA on failure
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: data.message || 'CAPTCHA verification failed. Try again.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying CAPTCHA:', error);
|
||||||
|
setMessage('An error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserInput(''); // Clear the input field
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveToRegistration = (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// navigate("/signup")
|
||||||
|
navigate('/registration');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
// vertical
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
// marginTop: '10vh',
|
||||||
|
// backgroundImage: `url('https://via.placeholder.com/300')`,
|
||||||
|
backgroundImage: `url(${sypiu_ggcp})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="center">
|
||||||
|
<Image
|
||||||
|
// src="/src/assets/freepik/LOGOPIU.png"
|
||||||
|
src={logo}
|
||||||
|
height={150}
|
||||||
|
width={220}
|
||||||
|
preview={false}
|
||||||
|
alt="signin"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<br />
|
||||||
|
<Form onFinish={handleOnSubmit} layout="vertical" style={{ width: '250px' }}>
|
||||||
|
<Form.Item
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
// type: "email",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="username" size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please input your password!',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="password" size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<div
|
||||||
|
style={{ marginLeft: 45 }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: captchaSvg }}
|
||||||
|
/>
|
||||||
|
{/* {message} */}
|
||||||
|
<Input
|
||||||
|
style={{ marginTop: 10, marginBottom: 15 }}
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter CAPTCHA text"
|
||||||
|
size="large"
|
||||||
|
value={userInput}
|
||||||
|
onChange={(e) => setUserInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Button type="primary" htmlType="submit" style={{ width: '100%' }}>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={() => moveToRegistration()}
|
||||||
|
>
|
||||||
|
Registration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignIn;
|
||||||
158
src/pages/auth/Signup.jsx
Normal file
158
src/pages/auth/Signup.jsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { Flex, Input, Form, Button, Card, Space, Image } from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
// import handleSignIn from '../../Utils/Auth/SignIn';
|
||||||
|
import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
const SignUp = () => {
|
||||||
|
|
||||||
|
const [captchaSvg, setCaptchaSvg] = React.useState('');
|
||||||
|
const [userInput, setUserInput] = React.useState('');
|
||||||
|
const [message, setMessage] = React.useState('');
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// let url = `${import.meta.env.VITE_API_SERVER}/users`;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchCaptcha();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch the CAPTCHA SVG from the backend
|
||||||
|
const fetchCaptcha = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:9528/generate-captcha');
|
||||||
|
const data = await response.text();
|
||||||
|
setCaptchaSvg(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching CAPTCHA:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnSubmt = async (e) => {
|
||||||
|
// console.log('Received values of form: ', e);
|
||||||
|
// await handleSignIn(e);
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5000/verify-captcha', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userInput }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('CAPTCHA verified successfully!');
|
||||||
|
} else {
|
||||||
|
setMessage('CAPTCHA verification failed. Try again.');
|
||||||
|
fetchCaptcha(); // Refresh CAPTCHA on failure
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying CAPTCHA:', error);
|
||||||
|
setMessage('An error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserInput(''); // Clear the input field
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveToSignin = (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
navigate("/signin")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
justify='center'
|
||||||
|
// vertical
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
// marginTop: '10vh',
|
||||||
|
// backgroundImage: `url('https://via.placeholder.com/300')`,
|
||||||
|
backgroundImage: `url(${sypiu_ggcp})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align='center' justify='center'>
|
||||||
|
<Image src='/vite.svg' height={150} width={150} preview={false} alt='signin' />
|
||||||
|
</Flex>
|
||||||
|
<br />
|
||||||
|
<Form
|
||||||
|
onFinish={handleOnSubmt}
|
||||||
|
layout='vertical'
|
||||||
|
style={{ width: '250px' }}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Form.Item label="Email" name="email"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
type: 'email'
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder='email' size='large' />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Password" name="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please input your password!'
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder='password' size='large' />
|
||||||
|
</Form.Item>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: captchaSvg }} />
|
||||||
|
|
||||||
|
<Form.Item label="Captcha" name="captcha"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder='Enter CAPTCHA text' size='large' />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* <input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter CAPTCHA text"
|
||||||
|
value={userInput}
|
||||||
|
onChange={(e) => setUserInput(e.target.value)}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space direction='vertical' style={{ width: '100%' }}>
|
||||||
|
<Button type="primary" htmlType="submit" style={{ width: '100%' }}>
|
||||||
|
Registrasi
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={() => moveToSignin()}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignUp
|
||||||
11
src/pages/blank/Blank.jsx
Normal file
11
src/pages/blank/Blank.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Blank = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Hi From BlankPage
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blank
|
||||||
85
src/pages/blank/NotFound.jsx
Normal file
85
src/pages/blank/NotFound.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import ImgRobot from '../../assets/freepik/404.png';
|
||||||
|
|
||||||
|
const { Title, Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
padding: '5vh 16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Title
|
||||||
|
level={2}
|
||||||
|
style={{
|
||||||
|
fontWeight: 800,
|
||||||
|
marginBottom: 12,
|
||||||
|
fontSize: 'clamp(28px, 5vw, 42px)',
|
||||||
|
color: '#1f1f1f',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Oops... You seem lost.
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
fontSize: 'clamp(14px, 2vw, 18px)',
|
||||||
|
maxWidth: '90%',
|
||||||
|
color: '#595959',
|
||||||
|
marginBottom: '3vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
We couldn't find the page you were looking for. Let us take you back to the main
|
||||||
|
page.
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={ImgRobot}
|
||||||
|
alt="404 Not Found"
|
||||||
|
style={{
|
||||||
|
maxWidth: '90%',
|
||||||
|
width: '480px',
|
||||||
|
marginBottom: '4vh',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link to="/">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#2f2f2f',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
padding: '10px 20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px', marginTop: '5vh' }}>
|
||||||
|
Illustration by{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.freepik.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: '#1890ff' }}
|
||||||
|
>
|
||||||
|
Freepik
|
||||||
|
</a>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
58
src/pages/blank/Waiting.jsx
Normal file
58
src/pages/blank/Waiting.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Button, Spin, Typography } from 'antd';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import ImgPIU from '../../assets/freepik/LOGOPIU.png';
|
||||||
|
|
||||||
|
const { Title, Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
const Waiting = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
padding: '5vh 16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ImgPIU}
|
||||||
|
alt="Loading"
|
||||||
|
style={{
|
||||||
|
maxWidth: '30%',
|
||||||
|
// width: '400px',
|
||||||
|
marginBottom: '4vh',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Spin size="large" style={{ marginBottom: '4vh' }} />
|
||||||
|
|
||||||
|
<Title
|
||||||
|
level={2}
|
||||||
|
style={{
|
||||||
|
fontWeight: 800,
|
||||||
|
marginBottom: 12,
|
||||||
|
fontSize: 'clamp(28px, 5vw, 42px)',
|
||||||
|
color: '#1f1f1f',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Please wait...
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
fontSize: 'clamp(14px, 2vw, 18px)',
|
||||||
|
maxWidth: '90%',
|
||||||
|
color: '#595959',
|
||||||
|
marginBottom: '3vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
We are loading your content. This won’t take long.
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Waiting;
|
||||||
52
src/pages/home/Home.jsx
Normal file
52
src/pages/home/Home.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Card, Typography, Flex } from 'antd';
|
||||||
|
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const { setBreadcrumbItems } = useBreadcrumb();
|
||||||
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setIsMobile(window.innerWidth <= 768);
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
setBreadcrumbItems([
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<Text strong style={{ fontSize: '14px' }}>
|
||||||
|
• Dashboard
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<Text strong style={{ fontSize: '14px' }}>
|
||||||
|
Home
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Flex align="center" justify="center">
|
||||||
|
<Text strong style={{fontSize:'30px'}}>Wellcome Call Of Duty App</Text>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
71
src/pages/home/component/StatCard.jsx
Normal file
71
src/pages/home/component/StatCard.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Card } from 'antd';
|
||||||
|
|
||||||
|
const StatCard = ({ title, data }) => {
|
||||||
|
const totalCount = data.reduce((sum, item) => sum + item.count, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={title} style={{ borderRadius: 12 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.map((item, idx) => (
|
||||||
|
<div key={idx} style={{ textAlign: 'center', flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 'clamp(12px, 1.5vw, 16px)', fontWeight: 500 }}>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 'clamp(20px, 3vw, 28px)',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: item.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.percent}%
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 'clamp(12px, 1.5vw, 16px)', color: item.color }}>
|
||||||
|
{item.count.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ position: 'relative', height: 28, marginTop: 8 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
width: `${item.percent}%`,
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Total: {totalCount.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatCard;
|
||||||
86
src/pages/master/device/IndexDevice.jsx
Normal file
86
src/pages/master/device/IndexDevice.jsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ListDevice from './component/ListDevice';
|
||||||
|
import DetailDevice from './component/DetailDevice';
|
||||||
|
import GeneratePdf from './component/GeneratePdf';
|
||||||
|
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const IndexDevice = memo(function IndexDevice() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { setBreadcrumbItems } = useBreadcrumb();
|
||||||
|
|
||||||
|
const [actionMode, setActionMode] = useState('list');
|
||||||
|
const [selectedData, setSelectedData] = useState(null);
|
||||||
|
const [readOnly, setReadOnly] = useState(false);
|
||||||
|
const [showModal, setShowmodal] = useState(false);
|
||||||
|
|
||||||
|
const setMode = (param) => {
|
||||||
|
setShowmodal(true);
|
||||||
|
switch (param) {
|
||||||
|
case 'add':
|
||||||
|
setReadOnly(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
setReadOnly(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'preview':
|
||||||
|
setReadOnly(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
setShowmodal(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setActionMode(param);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
setBreadcrumbItems([
|
||||||
|
{ title: <Text strong style={{ fontSize: '14px' }}>• Master</Text> },
|
||||||
|
{ title: <Text strong style={{ fontSize: '14px' }}>Device</Text> }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ListDevice
|
||||||
|
actionMode={actionMode}
|
||||||
|
setActionMode={setMode}
|
||||||
|
selectedData={selectedData}
|
||||||
|
setSelectedData={setSelectedData}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
<DetailDevice
|
||||||
|
setActionMode={setMode}
|
||||||
|
selectedData={selectedData}
|
||||||
|
setSelectedData={setSelectedData}
|
||||||
|
readOnly={readOnly}
|
||||||
|
showModal={showModal}
|
||||||
|
permitDefault={true}
|
||||||
|
actionMode={actionMode}
|
||||||
|
/>
|
||||||
|
{actionMode == 'generatepdf' && (
|
||||||
|
<GeneratePdf
|
||||||
|
setActionMode={setMode}
|
||||||
|
selectedData={selectedData}
|
||||||
|
setSelectedData={setSelectedData}
|
||||||
|
readOnly={readOnly}
|
||||||
|
showPdf={true}
|
||||||
|
permitDefault={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IndexDevice;
|
||||||
308
src/pages/master/device/component/DetailDevice.jsx
Normal file
308
src/pages/master/device/component/DetailDevice.jsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Radio } from 'antd';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
|
import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd';
|
||||||
|
import { Checkbox } from 'antd';
|
||||||
|
const CheckboxGroup = Checkbox.Group;
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const DetailDevice = (props) => {
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
id_apd: '',
|
||||||
|
nama_apd: '',
|
||||||
|
type_input: 1,
|
||||||
|
is_active: true,
|
||||||
|
jenis_permit_default: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [FormData, setFormData] = useState(defaultData);
|
||||||
|
|
||||||
|
const [jenisPermit, setJenisPermit] = useState([]);
|
||||||
|
const [checkedList, setCheckedList] = useState([]);
|
||||||
|
|
||||||
|
const onChange = (list) => {
|
||||||
|
setCheckedList(list);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeRadio = (e) => {
|
||||||
|
setFormData({
|
||||||
|
...FormData,
|
||||||
|
type_input: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataJenisPermit = async () => {
|
||||||
|
setCheckedList([]);
|
||||||
|
const result = await getJenisPermit();
|
||||||
|
const data = result.data ?? [];
|
||||||
|
const names = data.map((item) => ({
|
||||||
|
value: item.id_jenis_permit,
|
||||||
|
label: item.nama_jenis_permit,
|
||||||
|
}));
|
||||||
|
setJenisPermit(names);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
props.setSelectedData(null);
|
||||||
|
props.setActionMode('list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
|
||||||
|
if (!FormData.nama_apd) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
message: 'Kolom Nama APD Tidak Boleh Kosong',
|
||||||
|
});
|
||||||
|
|
||||||
|
setConfirmLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.permitDefault && checkedList.length === 0) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
message: 'Kolom Jenis Permit Tidak Boleh Kosong',
|
||||||
|
});
|
||||||
|
|
||||||
|
setConfirmLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_apd: FormData.nama_apd,
|
||||||
|
is_active: FormData.is_active,
|
||||||
|
type_input: FormData.type_input,
|
||||||
|
jenis_permit_default: checkedList,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.permitDefault) {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
if (!FormData.id_apd) {
|
||||||
|
response = await createApd(payload);
|
||||||
|
} else {
|
||||||
|
response = await updateApd(FormData.id_apd, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: `Data "${response.data.nama_apd}" berhasil ${
|
||||||
|
FormData.id_apd ? 'diubah' : 'ditambahkan'
|
||||||
|
}.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
props.setActionMode('list');
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Terjadi kesalahan pada server. Coba lagi nanti.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
props.setData((prevData) => [
|
||||||
|
...prevData,
|
||||||
|
{ nama_apd: payload.nama_apd, type_input: payload.type_input },
|
||||||
|
]);
|
||||||
|
|
||||||
|
props.setActionMode('list');
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({
|
||||||
|
...FormData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusToggle = (event) => {
|
||||||
|
const isChecked = event;
|
||||||
|
setFormData({
|
||||||
|
...FormData,
|
||||||
|
is_active: isChecked ? true : false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
getDataJenisPermit();
|
||||||
|
if (props.selectedData != null) {
|
||||||
|
setFormData(props.selectedData);
|
||||||
|
setCheckedList(props.selectedData.jenis_permit_default_arr);
|
||||||
|
} else {
|
||||||
|
setFormData(defaultData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, [props.showModal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
// title={`${FormData.id_apd === '' ? 'Tambah' : 'Edit'} APD`}
|
||||||
|
title={`${
|
||||||
|
props.actionMode === 'add'
|
||||||
|
? 'Tambah'
|
||||||
|
: props.actionMode === 'preview'
|
||||||
|
? 'Preview'
|
||||||
|
: 'Edit'
|
||||||
|
} Device`}
|
||||||
|
open={props.showModal}
|
||||||
|
// open={true}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<React.Fragment key="modal-footer">
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleCancel}>Batal</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorBgContainer: '#209652',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!props.readOnly && (
|
||||||
|
<Button loading={confirmLoading} onClick={handleSave}>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ConfigProvider>
|
||||||
|
</React.Fragment>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{FormData && (
|
||||||
|
<div>
|
||||||
|
{props.permitDefault && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Aktif</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginRight: '8px' }}>
|
||||||
|
<Switch
|
||||||
|
disabled={props.readOnly}
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
FormData.is_active == 1 ? '#23A55A' : '#bfbfbf',
|
||||||
|
}}
|
||||||
|
checked={FormData.is_active == 1 ? true : false}
|
||||||
|
onChange={handleStatusToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text>
|
||||||
|
{FormData.is_active == 1 ? 'Aktif' : 'Non Aktif'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ margin: '5px 0' }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div hidden>
|
||||||
|
<Text strong>Device ID</Text>
|
||||||
|
<Input
|
||||||
|
name="id_apd"
|
||||||
|
value={FormData.id_apd}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Device Name</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Input
|
||||||
|
name="nama_apd"
|
||||||
|
value={FormData.nama_apd}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 4, marginBottom: 4 }}>
|
||||||
|
<Text strong>Tipe Input</Text>
|
||||||
|
<Text style={{ color: 'red', marginLeft: 4 }}>*</Text>
|
||||||
|
<div>
|
||||||
|
<Radio.Group
|
||||||
|
value={FormData.type_input}
|
||||||
|
options={[
|
||||||
|
{ value: 1, label: 'Check' },
|
||||||
|
{ value: 2, label: 'Text' },
|
||||||
|
{ value: 3, label: 'Number' },
|
||||||
|
]}
|
||||||
|
onChange={onChangeRadio}
|
||||||
|
disabled={props.readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{props.permitDefault && (
|
||||||
|
<div>
|
||||||
|
<Text strong>Jenis Permit</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
|
||||||
|
<CheckboxGroup
|
||||||
|
options={jenisPermit}
|
||||||
|
value={checkedList}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={props.readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailDevice;
|
||||||
127
src/pages/master/device/component/GeneratePdf.jsx
Normal file
127
src/pages/master/device/component/GeneratePdf.jsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import React, {useEffect, useState } from 'react';
|
||||||
|
import { Modal, Button, ConfigProvider } from 'antd';
|
||||||
|
import { jsPDF } from 'jspdf';
|
||||||
|
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
|
||||||
|
import { kopReportPdf } from '../../../../components/Global/KopReport';
|
||||||
|
|
||||||
|
const GeneratePdf = (props) => {
|
||||||
|
const [pdfUrl, setPdfUrl] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
generatePdf();
|
||||||
|
} else {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
props.setSelectedData(null);
|
||||||
|
props.setActionMode('list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePdf = async () => {
|
||||||
|
const {images, title} = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
|
||||||
|
|
||||||
|
const doc = new jsPDF({
|
||||||
|
orientation: "portrait",
|
||||||
|
unit: "mm",
|
||||||
|
format: "a4"
|
||||||
|
});
|
||||||
|
|
||||||
|
const width = 45;
|
||||||
|
const height = 23;
|
||||||
|
const marginTop = 6;
|
||||||
|
const marginLeft = 10;
|
||||||
|
doc.addImage(images, 'PNG', marginLeft, marginTop, width, height);
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(25);
|
||||||
|
doc.setTextColor(35, 165, 90);
|
||||||
|
doc.setTextColor('#00b0f0');
|
||||||
|
doc.text(title, 100, 25);
|
||||||
|
doc.setTextColor('#000000');
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
|
doc.setLineWidth(0.2);
|
||||||
|
doc.line(10, 32, 200, 32);
|
||||||
|
doc.setLineWidth(0.6);
|
||||||
|
doc.line(10, 32.8, 200, 32.8);
|
||||||
|
|
||||||
|
doc.text("Tanggal Pengajuan", 10, 42);
|
||||||
|
doc.text(":", 59, 42);
|
||||||
|
|
||||||
|
doc.text("Deskripsi Pekerjaan", 10, 48);
|
||||||
|
doc.text(":", 59, 48);
|
||||||
|
|
||||||
|
doc.text("No. Permit", 10, 54);
|
||||||
|
doc.text(":", 59, 54);
|
||||||
|
doc.text("Spesifik Lokasi", 120, 54);
|
||||||
|
doc.text(":", 160, 54);
|
||||||
|
|
||||||
|
doc.text("No. Order", 10, 60);
|
||||||
|
doc.text(":", 59, 60);
|
||||||
|
doc.text("Jum. Personil Terlihat", 120, 60);
|
||||||
|
doc.text(":", 160, 60);
|
||||||
|
|
||||||
|
doc.text("Peralatan yang digunakan", 10, 66);
|
||||||
|
doc.text(":", 59, 66);
|
||||||
|
|
||||||
|
doc.text("Jenis APD yang digunakan", 10, 72);
|
||||||
|
doc.text(":", 59, 72);
|
||||||
|
|
||||||
|
const blob = doc.output('blob');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
setPdfUrl(url);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
width='60%'
|
||||||
|
title="Preview PDF"
|
||||||
|
open={props.showPdf}
|
||||||
|
// open={true}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleCancel}>Batal</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{pdfUrl && (
|
||||||
|
<iframe
|
||||||
|
src={`${pdfUrl}#zoom=100`}
|
||||||
|
title="PDF Viewer"
|
||||||
|
width="100%"
|
||||||
|
height="600px"
|
||||||
|
style={{ marginTop: '20px', border: '1px solid #ccc' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneratePdf;
|
||||||
408
src/pages/master/device/component/ListDevice.jsx
Normal file
408
src/pages/master/device/component/ListDevice.jsx
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
ConfigProvider,
|
||||||
|
Button, Row, Col, Card, Divider, Form, Input, Dropdown } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
FileExcelOutlined,
|
||||||
|
EllipsisOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { deleteApd, getAllApd } from '../../../../api/master-apd';
|
||||||
|
import TableList from '../../../../components/Global/TableList';
|
||||||
|
import { getFilterData } from '../../../../components/Global/DataFilter';
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
|
||||||
|
|
||||||
|
const columns = (items, handleClickMenu) => [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id_apd',
|
||||||
|
key: 'id_apd',
|
||||||
|
width: '5%',
|
||||||
|
hidden: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Device Name',
|
||||||
|
dataIndex: 'nama_apd',
|
||||||
|
key: 'nama_apd',
|
||||||
|
width: '55%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Aktif',
|
||||||
|
dataIndex: 'is_active',
|
||||||
|
key: 'is_active',
|
||||||
|
width: '10%',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, { is_active }) => (
|
||||||
|
<>
|
||||||
|
{is_active === true ? (
|
||||||
|
<Tag color={'green'} key={'aaa'}>
|
||||||
|
Aktif
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag color={'red'} key={'aaa'}>
|
||||||
|
Non-Aktif
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Aksi',
|
||||||
|
key: 'aksi',
|
||||||
|
align: 'center',
|
||||||
|
width: '5%',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items,
|
||||||
|
onClick: ({key})=>handleClickMenu(key, record)
|
||||||
|
}}
|
||||||
|
trigger={['click']}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
shape="default"
|
||||||
|
icon={<EllipsisOutlined />}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ListDevice = memo(function ListDevice(props) {
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
|
|
||||||
|
const defaultFilter = { nama_apd: '' };
|
||||||
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
if (props.actionMode == 'list') {
|
||||||
|
setFormDataFilter(defaultFilter);
|
||||||
|
doFilter();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, [props.actionMode]);
|
||||||
|
|
||||||
|
const toggleFilter = () => {
|
||||||
|
setFormDataFilter(defaultFilter);
|
||||||
|
setShowFilter((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const doFilter = () => {
|
||||||
|
setTrigerFilter((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChangeFilter = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormDataFilter((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPreviewModal = (param) => {
|
||||||
|
props.setSelectedData(param);
|
||||||
|
props.setActionMode('preview');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showEditModal = (param = null) => {
|
||||||
|
props.setSelectedData(param);
|
||||||
|
props.setActionMode('edit');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAddModal = (param = null) => {
|
||||||
|
props.setSelectedData(param);
|
||||||
|
props.setActionMode('add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDeleteDialog = (param) => {
|
||||||
|
NotifConfirmDialog({
|
||||||
|
icon: 'question',
|
||||||
|
title: 'Konfirmasi',
|
||||||
|
message: 'Apakah anda yakin hapus data "' + param.nama_apd + '" ?',
|
||||||
|
onConfirm: () => handleDelete(param.id_apd),
|
||||||
|
onCancel: () => props.setSelectedData(null),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id_apd) => {
|
||||||
|
const response = await deleteApd(id_apd);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'Data Data "' + response.data[0].nama_apd + '" berhasil dihapus.',
|
||||||
|
});
|
||||||
|
doFilter();
|
||||||
|
} else {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: 'Gagal Menghapus Data Data "' + response.data[0].nama_apd + '"',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePdf = () => {
|
||||||
|
props.setActionMode('generatepdf');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportExcel = async()=>{
|
||||||
|
const data = getFilterData();
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const sheet = workbook.addWorksheet('Data APD');
|
||||||
|
let rowCursor = 1;
|
||||||
|
// Kop Logo PIE
|
||||||
|
if (logoPiEnergi) {
|
||||||
|
const response = await fetch(logoPiEnergi);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const buffer = await blob.arrayBuffer();
|
||||||
|
|
||||||
|
const imageId = workbook.addImage({
|
||||||
|
buffer,
|
||||||
|
extension: 'png',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tempatkan gambar di pojok atas
|
||||||
|
sheet.addImage(imageId, {
|
||||||
|
tl: { col: 0.2, row: 0.8 },
|
||||||
|
ext: { width: 163, height: 80 },
|
||||||
|
});
|
||||||
|
|
||||||
|
sheet.getRow(5).height = 15; // biar ada jarak ke tabel
|
||||||
|
rowCursor = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambah Judul
|
||||||
|
const titleCell = sheet.getCell(`C${rowCursor}`);
|
||||||
|
titleCell.value = 'Data APD';
|
||||||
|
titleCell.font = { size: 20, bold: true, color: { argb: 'FF00AEEF' } };
|
||||||
|
titleCell.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||||
|
sheet.mergeCells(`C${rowCursor}:F${rowCursor}`);
|
||||||
|
|
||||||
|
// Header tabel
|
||||||
|
const headers = [
|
||||||
|
'ID APD',
|
||||||
|
'Nama APD',
|
||||||
|
'Deskripsi',
|
||||||
|
'Jenis Permit Default',
|
||||||
|
'Aktif',
|
||||||
|
'Dibuat',
|
||||||
|
'Diubah',
|
||||||
|
];
|
||||||
|
sheet.addRow(headers);
|
||||||
|
const headerRow = sheet.getRow(6);
|
||||||
|
headerRow.font = { bold: true, size: 12 };
|
||||||
|
headerRow.eachCell((cell) => {
|
||||||
|
cell.alignment = {
|
||||||
|
horizontal: 'center', // rata tengah kiri-kanan
|
||||||
|
vertical: 'middle', // rata tengah atas-bawah
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tambahkan data
|
||||||
|
data.forEach((item) => {
|
||||||
|
sheet.addRow([
|
||||||
|
item.id_apd,
|
||||||
|
item.nama_apd,
|
||||||
|
item.deskripsi_apd ?? '',
|
||||||
|
item.jenis_permit_default ?? '',
|
||||||
|
item.is_active ? 'Ya' : 'Tidak',
|
||||||
|
new Date(item.created_at).toLocaleString(),
|
||||||
|
new Date(item.updated_at).toLocaleString(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto width
|
||||||
|
sheet.columns.forEach((col) => {
|
||||||
|
let maxLength = 10;
|
||||||
|
col.eachCell({ includeEmpty: true }, (cell) => {
|
||||||
|
const len = cell.value?.toString().length || 0;
|
||||||
|
if (len > maxLength) maxLength = len;
|
||||||
|
});
|
||||||
|
col.width = maxLength + 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
saveAs(new Blob([buffer]), 'Data_APD.xlsx');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickMenu = (key, record) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'preview':
|
||||||
|
showPreviewModal(record);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
showEditModal(record);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
showDeleteDialog(record);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
|
key: 'preview',
|
||||||
|
label: <span style={{fontSize:'17px'}}>Preview</span>,
|
||||||
|
icon: <EyeOutlined style={{fontSize:'17px', marginTop:'5px'}} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: <span style={{fontSize:'17px'}}>Edit</span>,
|
||||||
|
icon: <EditOutlined style={{fontSize:'17px'}} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: <span style={{fontSize:'17px'}}>Delete</span>,
|
||||||
|
icon: <DeleteOutlined style={{fontSize:'17px'}} />,
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Card>
|
||||||
|
<Row>
|
||||||
|
<Col xs={24}>
|
||||||
|
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||||
|
<Col>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={toggleFilter} icon={<FilterOutlined />}>
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Space wrap size="small">
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => showAddModal()}
|
||||||
|
>
|
||||||
|
Tambah Data
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* filter */}
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||||
|
{showFilter && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="Nama APD">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'center',
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="nama_apd"
|
||||||
|
value={formDataFilter.nama_apd}
|
||||||
|
onChange={handleInputChangeFilter}
|
||||||
|
placeholder="Enter Nama APD"
|
||||||
|
style={{ height: '40px' }}
|
||||||
|
/>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorText: 'white',
|
||||||
|
colorBgContainer: 'purple',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
onClick={doFilter}
|
||||||
|
style={{ height: '40px' }}
|
||||||
|
>
|
||||||
|
Cari
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||||
|
<TableList
|
||||||
|
getData={getAllApd}
|
||||||
|
queryParams={formDataFilter}
|
||||||
|
columns={columns(menu, handleClickMenu)}
|
||||||
|
triger={trigerFilter}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ListDevice;
|
||||||
50
vite.config.js
Normal file
50
vite.config.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// '@': resolve(__dirname, './src'),
|
||||||
|
// apis: resolve(__dirname, "./src/apis"),
|
||||||
|
assets: resolve(__dirname, "./src/assets"),
|
||||||
|
// components: resolve(__dirname, "./src/components"),
|
||||||
|
// layouts: resolve(__dirname, "./src/layouts"),
|
||||||
|
// pages: resolve(__dirname, "./src/pages"),
|
||||||
|
// utils: resolve(__dirname, "./src/utils"),
|
||||||
|
// store: resolve(__dirname, "./src/store"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
// port: 3036
|
||||||
|
port: 8592
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
port: 8592
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// export default defineConfig({
|
||||||
|
// resolve: {
|
||||||
|
// alias: {
|
||||||
|
// // '@': resolve(__dirname, './src'),
|
||||||
|
// apis: resolve(__dirname, "./src/apis"),
|
||||||
|
// assets: resolve(__dirname, "./src/assets"),
|
||||||
|
// components: resolve(__dirname, "./src/components"),
|
||||||
|
// layouts: resolve(__dirname, "./src/layouts"),
|
||||||
|
// pages: resolve(__dirname, "./src/pages"),
|
||||||
|
// utils: resolve(__dirname, "./src/utils"),
|
||||||
|
// store: resolve(__dirname, "./src/store"),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// plugins: [react()],
|
||||||
|
// server: {
|
||||||
|
// port: 3036
|
||||||
|
// },
|
||||||
|
// preview: {
|
||||||
|
// port: 9525
|
||||||
|
// }
|
||||||
|
// })
|
||||||
Reference in New Issue
Block a user