commit b1083f3d4ae314f50a6919d449fcea82cf3f09cb Author: yogiedigital Date: Mon Sep 22 10:37:41 2025 +0700 init 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/.gitignore b/.gitignore new file mode 100644 index 0000000..b338238 --- /dev/null +++ b/.gitignore @@ -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? 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/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/readme.md b/readme.md new file mode 100644 index 0000000..a8edd40 --- /dev/null +++ b/readme.md @@ -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 \ 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/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/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 ( + <> + + + + + ); +} + +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 ( +
+ Logo PIU + {filteredRoles.map((role, index) => ( +
+ + Validasi Dokumen <br /> 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 ?? 'Belum ada status'} + + } + open={isModalVisible} + onCancel={handleCancel} + footer={[ + <> + + + + {props.status_permit === pengajuanPermitSelesai && + historyData.length >= 0 && + isVendor && ( + <> + + + + + )} + {props.status_permit !== 0 && + props.status_permit !== pengajuanPermitSelesai && + props.status_permit !== permitSelesai && + historyData.length >= 0 && ( + <> + {canReject && ( + + + + )} + {canApprove && ( + + + + )} + + )} + , + ]} + > + + + {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={[ + , + + + + , + ]} + > +

Silakan isi deskripsi:

+