diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..bea6a2b
--- /dev/null
+++ b/.eslintrc.cjs
@@ -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 },
+// ],
+// },
+// }
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
index 54f07af..b338238 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
+.env
+package-lock.json
# Editor directories and files
.vscode/*
@@ -21,4 +23,4 @@ dist-ssr
*.ntvs*
*.njsproj
*.sln
-*.sw?
\ No newline at end of file
+*.sw?
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..92eb704
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "printWidth": 100,
+ "tabWidth": 4,
+ "useTabs": false,
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "bracketSpacing": true
+ }
+
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..4a6eee7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Call of Duty
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6167a07
--- /dev/null
+++ b/package.json
@@ -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"
+}
diff --git a/public/assets/contohpdf.pdf b/public/assets/contohpdf.pdf
new file mode 100644
index 0000000..4a790cf
Binary files /dev/null and b/public/assets/contohpdf.pdf differ
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/web.config b/public/web.config
new file mode 100644
index 0000000..dbd0490
--- /dev/null
+++ b/public/web.config
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..4872856
--- /dev/null
+++ b/src/App.jsx
@@ -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 (
+
+
+ {isAdmin ? (
+ } />
+ ) : (
+ } />
+ )}
+
+ } />
+ }>
+ } />
+ } />
+
+
+ }>
+ } />
+
+
+ } />
+
+
+ );
+};
+
+export default App;
diff --git a/src/ProtectedRoute.jsx b/src/ProtectedRoute.jsx
new file mode 100644
index 0000000..1015c1f
--- /dev/null
+++ b/src/ProtectedRoute.jsx
@@ -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 ;
+ }
+ return (
+
+
+
+ );
+};
diff --git a/src/Utils/Auth/Logout.jsx b/src/Utils/Auth/Logout.jsx
new file mode 100644
index 0000000..957d1aa
--- /dev/null
+++ b/src/Utils/Auth/Logout.jsx
@@ -0,0 +1,7 @@
+const handleLogOut = () => {
+ localStorage.removeItem('Auth');
+ localStorage.removeItem('session');
+ window.location.replace('/signin');
+}
+
+export default handleLogOut;
\ No newline at end of file
diff --git a/src/Utils/Auth/SignIn.jsx b/src/Utils/Auth/SignIn.jsx
new file mode 100644
index 0000000..e038f6a
--- /dev/null
+++ b/src/Utils/Auth/SignIn.jsx
@@ -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;
diff --git a/src/api/auth.jsx b/src/api/auth.jsx
new file mode 100644
index 0000000..468e938
--- /dev/null
+++ b/src/api/auth.jsx
@@ -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 };
diff --git a/src/api/dashboard-home.jsx b/src/api/dashboard-home.jsx
new file mode 100644
index 0000000..6f14ea0
--- /dev/null
+++ b/src/api/dashboard-home.jsx
@@ -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 };
\ No newline at end of file
diff --git a/src/api/master-apd.jsx b/src/api/master-apd.jsx
new file mode 100644
index 0000000..c39c14a
--- /dev/null
+++ b/src/api/master-apd.jsx
@@ -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 };
diff --git a/src/api/user-admin.jsx b/src/api/user-admin.jsx
new file mode 100644
index 0000000..72dba82
--- /dev/null
+++ b/src/api/user-admin.jsx
@@ -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 };
\ No newline at end of file
diff --git a/src/assets/freepik/404.png b/src/assets/freepik/404.png
new file mode 100644
index 0000000..1b7a7c0
Binary files /dev/null and b/src/assets/freepik/404.png differ
diff --git a/src/assets/freepik/LOGOPIU.png b/src/assets/freepik/LOGOPIU.png
new file mode 100644
index 0000000..5e35863
Binary files /dev/null and b/src/assets/freepik/LOGOPIU.png differ
diff --git a/src/assets/freepik/logo-web.png b/src/assets/freepik/logo-web.png
new file mode 100644
index 0000000..dbc846f
Binary files /dev/null and b/src/assets/freepik/logo-web.png differ
diff --git a/src/assets/freepik/waiting.png b/src/assets/freepik/waiting.png
new file mode 100644
index 0000000..da3512a
Binary files /dev/null and b/src/assets/freepik/waiting.png differ
diff --git a/src/assets/images/bahayaMudahMeledak.webp b/src/assets/images/bahayaMudahMeledak.webp
new file mode 100644
index 0000000..99bb9f8
Binary files /dev/null and b/src/assets/images/bahayaMudahMeledak.webp differ
diff --git a/src/assets/images/logo/pi-energi.png b/src/assets/images/logo/pi-energi.png
new file mode 100644
index 0000000..2549ec4
Binary files /dev/null and b/src/assets/images/logo/pi-energi.png differ
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/sypiu_ggcp.jpg b/src/assets/sypiu_ggcp.jpg
new file mode 100644
index 0000000..5c8fec4
Binary files /dev/null and b/src/assets/sypiu_ggcp.jpg differ
diff --git a/src/components/Common/BasicButton.jsx b/src/components/Common/BasicButton.jsx
new file mode 100644
index 0000000..398c945
--- /dev/null
+++ b/src/components/Common/BasicButton.jsx
@@ -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 (
+ <>
+
+ clickTheButton(block)}
+ // style={{
+ // background: "#23A55A",
+ // borderColor: "#23A55A",
+ // color: "white",
+ // }}
+ >
+ {text}
+
+
+ >
+ );
+}
+
+BasicButton.propTypes = {
+ // color: propTypes.string,
+ text: propTypes.string,
+ size: propTypes.string,
+ block: propTypes.bool,
+ clickTheButton: propTypes.any
+}
+
+export default BasicButton;
\ No newline at end of file
diff --git a/src/components/Common/BasicInput.jsx b/src/components/Common/BasicInput.jsx
new file mode 100644
index 0000000..62b584d
--- /dev/null
+++ b/src/components/Common/BasicInput.jsx
@@ -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 (
+
+
+
+ );
+};
+
+export default BasicInput;
diff --git a/src/components/Common/NavBar.jsx b/src/components/Common/NavBar.jsx
new file mode 100644
index 0000000..34d2c71
--- /dev/null
+++ b/src/components/Common/NavBar.jsx
@@ -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 (
+
+
+
+
+ LOGO
+
+
+
+
+ {import.meta.env.VITE_PROJECT_NAME}
+
+
+ {import.meta.env.VITE_PROJECT_DESCRIPTION}
+
+
+
+
+ );
+};
+
+NavBar.propTypes = {
+ fullSize: propTypes.any,
+}
+
+export default NavBar;
diff --git a/src/components/Common/Version.jsx b/src/components/Common/Version.jsx
new file mode 100644
index 0000000..4aea058
--- /dev/null
+++ b/src/components/Common/Version.jsx
@@ -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 (
+
+ v{packageJson.version}
+
+ );
+};
+
+export default Version;
diff --git a/src/components/Global/ApiRequest.jsx b/src/components/Global/ApiRequest.jsx
new file mode 100644
index 0000000..8811919
--- /dev/null
+++ b/src/components/Global/ApiRequest.jsx
@@ -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 };
diff --git a/src/components/Global/DataFilter.jsx b/src/components/Global/DataFilter.jsx
new file mode 100644
index 0000000..7a96638
--- /dev/null
+++ b/src/components/Global/DataFilter.jsx
@@ -0,0 +1,7 @@
+let filterData = [];
+
+export const setFilterData = (data) => {
+ filterData = data;
+};
+
+export const getFilterData = () => filterData;
\ No newline at end of file
diff --git a/src/components/Global/EmptyData.jsx b/src/components/Global/EmptyData.jsx
new file mode 100644
index 0000000..d1b3ab4
--- /dev/null
+++ b/src/components/Global/EmptyData.jsx
@@ -0,0 +1,24 @@
+import { Empty, Col, Row, Typography } from "antd";
+const {Text} = Typography;
+
+const EmptyData = ({
+ titleButton,
+ titlePositionButton
+})=>{
+ return(
+
+
+ Data Kosong
+
+
+ Untuk menampilkan data silahkan klik tombol {titleButton} {titlePositionButton}
+
+
+ }
+ />
+ );
+};
+
+export default EmptyData;
\ No newline at end of file
diff --git a/src/components/Global/Formatter.jsx b/src/components/Global/Formatter.jsx
new file mode 100644
index 0000000..9aef87c
--- /dev/null
+++ b/src/components/Global/Formatter.jsx
@@ -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,
+};
diff --git a/src/components/Global/KopReport.jsx b/src/components/Global/KopReport.jsx
new file mode 100644
index 0000000..b01d1f3
--- /dev/null
+++ b/src/components/Global/KopReport.jsx
@@ -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 };
\ No newline at end of file
diff --git a/src/components/Global/MqttConnection.jsx b/src/components/Global/MqttConnection.jsx
new file mode 100644
index 0000000..1a9551b
--- /dev/null
+++ b/src/components/Global/MqttConnection.jsx
@@ -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,
+};
diff --git a/src/components/Global/QrPermit.jsx b/src/components/Global/QrPermit.jsx
new file mode 100644
index 0000000..cb42ad7
--- /dev/null
+++ b/src/components/Global/QrPermit.jsx
@@ -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 (
+
+
+ {filteredRoles.map((role, index) => (
+
+
+ Validasi Dokumen Pupuk Indonesia Utilitas
+
+
+ {role}
+
+
+
+ Nama:
+ {' '}
+ {selectedName ?? '-'}
+
+
+ {formattedTime}
+
+
+
+ ))}
+
+ );
+}
+
+export default QrPermit;
diff --git a/src/components/Global/RegisterRequest.jsx b/src/components/Global/RegisterRequest.jsx
new file mode 100644
index 0000000..4bd767c
--- /dev/null
+++ b/src/components/Global/RegisterRequest.jsx
@@ -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;
\ No newline at end of file
diff --git a/src/components/Global/StatusButton.jsx b/src/components/Global/StatusButton.jsx
new file mode 100644
index 0000000..9c7b531
--- /dev/null
+++ b/src/components/Global/StatusButton.jsx
@@ -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 (
+ <>
+
+ {name || 'N/A'}
+
+
+
+ {name ?? 'Belum ada status'}
+
+ }
+ open={isModalVisible}
+ onCancel={handleCancel}
+ footer={[
+ <>
+
+ Batal
+
+ {props.status_permit === pengajuanPermitSelesai &&
+ historyData.length >= 0 &&
+ isVendor && (
+ <>
+
+
+ Selesai
+
+
+ >
+ )}
+ {props.status_permit !== 0 &&
+ props.status_permit !== pengajuanPermitSelesai &&
+ props.status_permit !== permitSelesai &&
+ historyData.length >= 0 && (
+ <>
+ {canReject && (
+
+ Reject
+
+ )}
+ {canApprove && (
+
+
+ Approve
+
+
+ )}
+ >
+ )}
+ >,
+ ]}
+ >
+
+
+ {historyData.length > 0 ? (
+ [...historyData]
+ .sort((a, b) => new Date(b.date) - new Date(a.date))
+ .map((item, index) => (
+
+
+
+ {item.text}
+
+ {item.date != null && (
+
+ {dayjs.utc(item.date).format('YYYY-MM-DD HH:mm:ss')}
+
+ )}
+
+
+
{item.name}
+
+ {item.closed && (
+
+ Closed: {item.closed}
+
+ )}
+
+
+ {item.deskripsi && (
+
+ {item.deskripsi}
+
+ )}
+
+ ))
+ ) : (
+ Belum ada riwayat status.
+ )}
+
+
+ {
+ setShowConfirmModal(false);
+ setDeskripsi('');
+ }}
+ confirmLoading={confirmLoading}
+ footer={[
+ {
+ setShowConfirmModal(false);
+ setDeskripsi('');
+ }}
+ >
+ Batal
+ ,
+
+
+
+ {actionType === 'approve' ? 'Approve' : 'Reject'}
+
+ ,
+ ]}
+ >
+ Silakan isi deskripsi:
+
+
+ {
+ setShowPermitSelesai(false);
+ setCloseType('');
+ }}
+ confirmLoading={confirmLoading}
+ footer={[
+ {
+ setShowPermitSelesai(false);
+ setCloseType('');
+ }}
+ >
+ Batal
+ ,
+
+
+ Ajukan Permit Selesai
+
+ ,
+ ]}
+ >
+ Status Permit saat ini :
+ setCloseType(e.target.value)} value={closeType}>
+ Belum Selesai
+ Selesai
+
+
+ >
+ );
+};
+
+export default StatusButton;
diff --git a/src/components/Global/StatusUserButton.jsx b/src/components/Global/StatusUserButton.jsx
new file mode 100644
index 0000000..2a3034f
--- /dev/null
+++ b/src/components/Global/StatusUserButton.jsx
@@ -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 (
+ <>
+
+ {statusName}
+
+
+
+ {statusName}
+
+ }
+ open={isModalVisible}
+ onCancel={handleCancel}
+ footer={[
+ <>
+
+ Batal
+
+ {data?.status_register === 1 && (
+ <>
+
+ Reject
+
+
+
+ Approve
+
+
+ >
+ )}
+ >,
+ ]}
+ >
+
+
+ {data ? (
+ <>
+ {data.updated_at !== data.created_at && (
+
+
+
+ Updated at
+
+
+ {toAppDateFormatter(data.updated_at)}
+
+
+
+ Diubah terakhir oleh {userUpdated}
+
+
+ )}
+
+
+
+ Created at
+
+
+ {toAppDateFormatter(data.created_at)}
+
+
+
+ Dibuat oleh {userCreated}
+
+
+ >
+ ) : (
+ Belum ada riwayat status.
+ )}
+
+ >
+ );
+};
+
+export default StatusUserButton;
diff --git a/src/components/Global/TableList.jsx b/src/components/Global/TableList.jsx
new file mode 100644
index 0000000..7e4d221
--- /dev/null
+++ b/src/components/Global/TableList.jsx
@@ -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 (
+
+ {isMobile && mobile ? (
+
+
+ {data.map((item) => (
+
+ {mobile.r1 && (
+
+ {item[mobile.r1.name] ?? mobile.r1.text ?? ''}
+
+ )}
+ {mobile.r2 && (
+
+ {item[mobile.r2.name] ?? mobile.r2.text ?? ''}
+
+ )}
+
+ )
+ }
+ style={{ width: '100%' }}
+ >
+
+ {mobile.r3 && mobile.r4 && (
+
+
+ {item[mobile.r3 ? mobile.r3.name : ''] ?? ''}
+
+
+ {item[mobile.r4 ? mobile.r4.name : ''] ?? ''}
+
+
+ )}
+ {mobile.r5 && (
+
+ {item[mobile.r5 ? mobile.r5.name : ''] ?? ''}
+
+ )}
+
+ {mobile.r6 && (
+
+
+ {item[mobile.r6 ? mobile.r6.name : ''] ?? ''}
+
+
+ )}
+
+ }
+ onClick={(e) => {
+ mobile.action(item);
+ }}
+ >
+ Detail
+
+
+
+ ))}
+
+
+ ) : (
+
+ {/* TABLE */}
+ ({ ...item, key: index }))}
+ pagination={false}
+ loading={gridLoading}
+ scroll={{
+ y: 520,
+ x: 1300,
+ }}
+ />
+
+ {/* PAGINATION */}
+
+
+
+
+ Menampilkan {pagingResponse.totalData} Data dari{' '}
+ {pagingResponse.perPage} Halaman
+
+
+
+
+
+
+
+
+ )}
+
+ );
+});
+
+export default TableList;
diff --git a/src/components/Global/ToastNotif.jsx b/src/components/Global/ToastNotif.jsx
new file mode 100644
index 0000000..cf9d8ad
--- /dev/null
+++ b/src/components/Global/ToastNotif.jsx
@@ -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 };
diff --git a/src/components/Global/headerReport.jsx b/src/components/Global/headerReport.jsx
new file mode 100644
index 0000000..47b8a30
--- /dev/null
+++ b/src/components/Global/headerReport.jsx
@@ -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(
+ <>
+
+
+
+ {title}
+
+
+ {step}
+
+
+
+
+
+ }>Batal
+
+
+
+
+
+
+
+ Pilih Data
+
+
+
+ >
+ );
+};
+export default HeaderReport;
\ No newline at end of file
diff --git a/src/components/loading/Loading.jsx b/src/components/loading/Loading.jsx
new file mode 100644
index 0000000..21fd37c
--- /dev/null
+++ b/src/components/loading/Loading.jsx
@@ -0,0 +1,28 @@
+import React, { memo } from 'react'
+import './loading.css'
+
+
+const Loading = memo(function Loading() {
+ return(
+
+
+
+
+ L
+ o
+ a
+ d
+ i
+ n
+ g
+ .
+ .
+ .
+
+
+
+
+ );
+})
+
+export default Loading
\ No newline at end of file
diff --git a/src/components/loading/loading.css b/src/components/loading/loading.css
new file mode 100644
index 0000000..763b9bb
--- /dev/null
+++ b/src/components/loading/loading.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..30d122f
--- /dev/null
+++ b/src/index.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/layout/LayoutBreadcrumb.jsx b/src/layout/LayoutBreadcrumb.jsx
new file mode 100644
index 0000000..5889c90
--- /dev/null
+++ b/src/layout/LayoutBreadcrumb.jsx
@@ -0,0 +1,15 @@
+import React, { createContext, useContext, useState } from 'react';
+
+const BreadcrumbContext = createContext();
+
+export const BreadcrumbProvider = ({ children }) => {
+ const [breadcrumbItems, setBreadcrumbItems] = useState([]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useBreadcrumb = () => useContext(BreadcrumbContext);
\ No newline at end of file
diff --git a/src/layout/LayoutFooter.jsx b/src/layout/LayoutFooter.jsx
new file mode 100644
index 0000000..be22b02
--- /dev/null
+++ b/src/layout/LayoutFooter.jsx
@@ -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 (
+
+ )
+}
+
+export default LayoutFooter
diff --git a/src/layout/LayoutHeader.jsx b/src/layout/LayoutHeader.jsx
new file mode 100644
index 0000000..21a7717
--- /dev/null
+++ b/src/layout/LayoutHeader.jsx
@@ -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 (
+ <>
+
+
+
+ Login AS {roleName}
+
+
+
+
+
+
+
+ {userName}
+
+
+ {
+ handleLogOut();
+ navigate('/signin');
+ }}
+ aria-label="Log out from the application"
+ style={{
+ color: colorText,
+ whiteSpace: 'nowrap',
+ }}
+ >
+ Logout
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default LayoutHeader;
diff --git a/src/layout/LayoutLogo.jsx b/src/layout/LayoutLogo.jsx
new file mode 100644
index 0000000..086db57
--- /dev/null
+++ b/src/layout/LayoutLogo.jsx
@@ -0,0 +1,69 @@
+import { Image } from 'antd';
+import logoPiu from '../assets/freepik/LOGOPIU.png';
+import React from 'react';
+
+const LayoutLogo = () => {
+ return (
+
+
+ {/* Ring sebelum logo utama */}
+
+
+
+
+
+
+
+ );
+};
+
+export default LayoutLogo;
\ No newline at end of file
diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx
new file mode 100644
index 0000000..33fb21f
--- /dev/null
+++ b/src/layout/LayoutMenu.jsx
@@ -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: ,
+ label: Home,
+ },
+ {
+ key: 'master',
+ icon: ,
+ label: 'Master',
+ children: [
+ {
+ key: 'master-device',
+ icon: ,
+ label: Device,
+ },
+ ],
+ },
+];
+
+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 (
+
+ );
+};
+export default LayoutMenu;
\ No newline at end of file
diff --git a/src/layout/LayoutSidebar.jsx b/src/layout/LayoutSidebar.jsx
new file mode 100644
index 0000000..4589344
--- /dev/null
+++ b/src/layout/LayoutSidebar.jsx
@@ -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 (
+ {
+ // console.log(broken);
+ }}
+ onCollapse={(collapsed, type) => {
+ // console.log(collapsed, type);
+ }}
+ >
+
+
+
+ )
+}
+
+export default LayoutSidebar
diff --git a/src/layout/MainLayout.jsx b/src/layout/MainLayout.jsx
new file mode 100644
index 0000000..6d035f9
--- /dev/null
+++ b/src/layout/MainLayout.jsx
@@ -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 (
+
+
+
+
+
+ {/* */}
+ {children}
+ {/*
*/}
+
+ {/* */}
+
+
+ );
+};
+export default MainLayout;
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..a76e718
--- /dev/null
+++ b/src/main.jsx
@@ -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(
+
+
+
+
+ ,
+)
diff --git a/src/pages/auth/Registration.jsx b/src/pages/auth/Registration.jsx
new file mode 100644
index 0000000..093c2b1
--- /dev/null
+++ b/src/pages/auth/Registration.jsx
@@ -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 (
+
+
+ Formulir Pendaftaran
+ }
+ onClick={() => navigate('/signin')}
+ >
+ Kembali
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default Registration;
diff --git a/src/pages/auth/SignIn.jsx b/src/pages/auth/SignIn.jsx
new file mode 100644
index 0000000..d2f0d33
--- /dev/null
+++ b/src/pages/auth/SignIn.jsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* {message} */}
+ setUserInput(e.target.value)}
+ />
+
+
+
+
+ Sign In
+
+
+
+
+ moveToRegistration()}
+ >
+ Registration
+
+
+
+ >
+ );
+};
+
+export default SignIn;
diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx
new file mode 100644
index 0000000..8bd1d14
--- /dev/null
+++ b/src/pages/auth/Signup.jsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* setUserInput(e.target.value)}
+ /> */}
+
+
+
+
+ Registrasi
+
+
+
+
+
+ moveToSignin()}
+ >
+ Sign In
+
+
+
+ >
+ )
+}
+
+export default SignUp
diff --git a/src/pages/blank/Blank.jsx b/src/pages/blank/Blank.jsx
new file mode 100644
index 0000000..54f06cf
--- /dev/null
+++ b/src/pages/blank/Blank.jsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const Blank = () => {
+ return (
+
+ Hi From BlankPage
+
+ )
+}
+
+export default Blank
diff --git a/src/pages/blank/NotFound.jsx b/src/pages/blank/NotFound.jsx
new file mode 100644
index 0000000..3cda2b0
--- /dev/null
+++ b/src/pages/blank/NotFound.jsx
@@ -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 (
+
+
+ Oops... You seem lost.
+
+
+
+ We couldn't find the page you were looking for. Let us take you back to the main
+ page.
+
+
+
+
+
+
+ Go back
+
+
+
+
+ Illustration by{' '}
+
+ Freepik
+
+
+
+ );
+};
+
+export default NotFound;
diff --git a/src/pages/blank/Waiting.jsx b/src/pages/blank/Waiting.jsx
new file mode 100644
index 0000000..d327574
--- /dev/null
+++ b/src/pages/blank/Waiting.jsx
@@ -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 (
+
+
+
+
+
+ Please wait...
+
+
+
+ We are loading your content. This won’t take long.
+
+
+ );
+};
+
+export default Waiting;
diff --git a/src/pages/home/Home.jsx b/src/pages/home/Home.jsx
new file mode 100644
index 0000000..38bd5ab
--- /dev/null
+++ b/src/pages/home/Home.jsx
@@ -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: (
+
+ • Dashboard
+
+ ),
+ },
+ {
+ title: (
+
+ Home
+
+ ),
+ },
+ ]);
+ } else {
+ navigate('/signin');
+ }
+ }, []);
+
+ return (
+
+
+ Wellcome Call Of Duty App
+
+
+ );
+};
+
+export default Home;
diff --git a/src/pages/home/component/StatCard.jsx b/src/pages/home/component/StatCard.jsx
new file mode 100644
index 0000000..bfbf068
--- /dev/null
+++ b/src/pages/home/component/StatCard.jsx
@@ -0,0 +1,71 @@
+import { Card } from 'antd';
+
+const StatCard = ({ title, data }) => {
+ const totalCount = data.reduce((sum, item) => sum + item.count, 0);
+
+ return (
+
+
+ {data.map((item, idx) => (
+
+
+ {item.label}
+
+
+ {item.percent}%
+
+
+ {item.count.toLocaleString()}
+
+
+ ))}
+
+
+
+
+ {data.map((item, idx) => (
+
+ ))}
+
+
+
+
+ Total: {totalCount.toLocaleString()}
+
+
+ );
+};
+
+export default StatCard;
diff --git a/src/pages/master/device/IndexDevice.jsx b/src/pages/master/device/IndexDevice.jsx
new file mode 100644
index 0000000..0ea060c
--- /dev/null
+++ b/src/pages/master/device/IndexDevice.jsx
@@ -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: • Master },
+ { title: Device }
+ ]);
+ } else {
+ navigate('/signin');
+ }
+ }, []);
+
+ return (
+
+
+
+ {actionMode == 'generatepdf' && (
+
+ )}
+
+ );
+});
+
+export default IndexDevice;
diff --git a/src/pages/master/device/component/DetailDevice.jsx b/src/pages/master/device/component/DetailDevice.jsx
new file mode 100644
index 0000000..77ab15f
--- /dev/null
+++ b/src/pages/master/device/component/DetailDevice.jsx
@@ -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 (
+
+
+ Batal
+
+
+ {!props.readOnly && (
+
+ Simpan
+
+ )}
+
+ ,
+ ]}
+ >
+ {FormData && (
+
+ {props.permitDefault && (
+ <>
+
+
+ Aktif
+
+
+
+
+
+
+
+ {FormData.is_active == 1 ? 'Aktif' : 'Non Aktif'}
+
+
+
+
+
+ >
+ )}
+
+ Device ID
+
+
+
+ Device Name
+ *
+
+
+
+ {props.permitDefault && (
+
+ Jenis Permit
+ *
+
+
+
+ )}
+
+ )}
+
+ );
+};
+
+export default DetailDevice;
\ No newline at end of file
diff --git a/src/pages/master/device/component/GeneratePdf.jsx b/src/pages/master/device/component/GeneratePdf.jsx
new file mode 100644
index 0000000..02ea2a0
--- /dev/null
+++ b/src/pages/master/device/component/GeneratePdf.jsx
@@ -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 (
+
+
+ Batal
+
+ >,
+ ]}
+ >
+ {pdfUrl && (
+
+ )}
+
+ );
+};
+
+export default GeneratePdf;
diff --git a/src/pages/master/device/component/ListDevice.jsx b/src/pages/master/device/component/ListDevice.jsx
new file mode 100644
index 0000000..e18f30d
--- /dev/null
+++ b/src/pages/master/device/component/ListDevice.jsx
@@ -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 ? (
+
+ Aktif
+
+ ) : (
+
+ Non-Aktif
+
+ )}
+ >
+ ),
+ },
+ {
+ title: 'Aksi',
+ key: 'aksi',
+ align: 'center',
+ width: '5%',
+ render: (_, record) => (
+ handleClickMenu(key, record)
+ }}
+ trigger={['click']}
+ placement="bottomRight"
+ >
+ }
+ />
+
+ ),
+ },
+];
+
+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: Preview ,
+ icon: ,
+ },
+ {
+ key: 'edit',
+ label: Edit ,
+ icon: ,
+ },
+ {
+ key: 'delete',
+ label: Delete ,
+ icon: ,
+ danger: true,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ }>
+ Filter
+
+
+
+
+
+
+ }
+ onClick={() => showAddModal()}
+ >
+ Tambah Data
+
+
+
+
+
+
+
+ {/* filter */}
+
+ {showFilter && (
+ <>
+
+
+
+
+
+ }
+ onClick={doFilter}
+ style={{ height: '40px' }}
+ >
+ Cari
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+});
+
+export default ListDevice;
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..4552433
--- /dev/null
+++ b/vite.config.js
@@ -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
+// }
+// })
\ No newline at end of file