From c64b7b3490fe999362772f0a5f2c4fcbe2009f2e Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 26 Sep 2025 11:07:03 +0700 Subject: [PATCH 001/114] update: route --- src/App.jsx | 36 +- src/ProtectedRoute.jsx | 31 +- src/Utils/Auth/Logout.jsx | 13 +- src/Utils/Auth/SignIn.jsx | 10 +- src/api/auth.jsx | 14 +- src/components/Global/ApiRequest.jsx | 203 +++---- src/layout/LayoutHeader.jsx | 23 +- src/pages/auth/Registration.jsx | 876 +++++++++++++-------------- src/pages/auth/SignIn.jsx | 272 +++------ src/pages/auth/Signup.jsx | 313 +++++----- src/pages/home/Home.jsx | 47 +- 11 files changed, 868 insertions(+), 970 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 4872856..659b307 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,44 +1,27 @@ import React from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import SignIn from './pages/auth/SignIn'; +import SignUp from './pages/auth/Signup'; import { ProtectedRoute } from './ProtectedRoute'; import NotFound from './pages/blank/NotFound'; -import { getSessionData } from './components/Global/Formatter'; -// dashboard +// Dashboard import Home from './pages/home/Home'; import Blank from './pages/blank/Blank'; -// master +// 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 ? ( - } /> - ) : ( - } /> - )} - + {/* Public Routes */} + } /> } /> + } /> + + {/* Protected Routes */} }> } /> } /> @@ -48,6 +31,7 @@ const App = () => { } /> + {/* Catch-all */} } /> diff --git a/src/ProtectedRoute.jsx b/src/ProtectedRoute.jsx index 1015c1f..f2e4811 100644 --- a/src/ProtectedRoute.jsx +++ b/src/ProtectedRoute.jsx @@ -1,20 +1,25 @@ import React from 'react'; import { Navigate, Outlet } from 'react-router-dom'; import MainLayout from './layout/MainLayout'; - -import { getSessionData } from './components/Global/Formatter'; +import { NotifAlert } from './components/Global/ToastNotif'; export const ProtectedRoute = () => { - const session = getSessionData(); - // console.log(session); + // cek token di localStorage + const token = localStorage.getItem('token'); + const isAuthenticated = !!token; - const isAuthenticated = session?.auth ?? false; - if (!isAuthenticated) { - return ; - } - return ( - - - - ); + if (!isAuthenticated) { + NotifAlert({ + icon: 'warning', + title: 'Session Habis', + message: 'Silahkan login terlebih dahulu', + }); + return ; + } + + return ( + + + + ); }; diff --git a/src/Utils/Auth/Logout.jsx b/src/Utils/Auth/Logout.jsx index 957d1aa..f610b52 100644 --- a/src/Utils/Auth/Logout.jsx +++ b/src/Utils/Auth/Logout.jsx @@ -1,7 +1,12 @@ -const handleLogOut = () => { - localStorage.removeItem('Auth'); - localStorage.removeItem('session'); +const handleLogOut = (navigate) => { + localStorage.removeItem('Auth'); + localStorage.removeItem('session'); + + if (navigate) { + navigate('/signin', { replace: true }); + } else { 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 index e038f6a..d67b81d 100644 --- a/src/Utils/Auth/SignIn.jsx +++ b/src/Utils/Auth/SignIn.jsx @@ -8,18 +8,14 @@ const handleSignIn = async (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'); - } + + // langsung redirect ke dashboard utama + window.location.replace('/dashboard/home'); } else { NotifAlert({ icon: 'error', diff --git a/src/api/auth.jsx b/src/api/auth.jsx index 468e938..fd96460 100644 --- a/src/api/auth.jsx +++ b/src/api/auth.jsx @@ -10,20 +10,10 @@ const login = async (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', + prefix: 'auth/register', params: params, headers: { 'Content-Type': 'application/json' }, }); @@ -49,4 +39,4 @@ const checkUsername = async (queryParams) => { }; -export { login, uploadFile, register, verifyRedirect, checkUsername }; +export { login, register, verifyRedirect, checkUsername }; diff --git a/src/components/Global/ApiRequest.jsx b/src/components/Global/ApiRequest.jsx index 8811919..20cf2ae 100644 --- a/src/components/Global/ApiRequest.jsx +++ b/src/components/Global/ApiRequest.jsx @@ -1,127 +1,92 @@ -import axios from 'axios'; -import Swal from 'sweetalert2'; +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, +async function ApiRequest({ + method = "GET", + params = {}, + url = "", + prefix = "/", + token = true, +} = {}) { + const baseURL = url || import.meta.env.VITE_API_SERVER; + const instance = axios.create({ baseURL }); + + const isFormData = params instanceof FormData; + + // request config + const request = { + method, + url: prefix, + data: params, + headers: { + "Accept-Language": "en_US", + ...(isFormData ? {} : { "Content-Type": "application/json" }), + }, + }; + + // handle download (doc) + if (params === "doc") { + request.responseType = "blob"; + } + + // ambil token + const rawToken = + sessionStorage.getItem("token_redirect") || localStorage.getItem("token"); + + if (token && rawToken) { + const cleanToken = rawToken.replace(/"/g, ""); + instance.defaults.headers.common[ + "Authorization" + ] = `Bearer ${cleanToken}`; + } + + try { + const response = await instance(request); + return { ...response, error: false }; + } catch (error) { + const status = error?.response?.status || 500; + const message = + error?.response?.data?.message || error.message || "Something Wrong"; + + cekError(status, message); + return { ...error.response, error: true }; + } +} + +// global error handler +async function cekError(status, message = "") { + console.log("status code", status); + + if (status === 401) { + Swal.fire({ + icon: "warning", + title: "Peringatan", + text: `${message}, Silahkan login`, + }).then((result) => { + if (result.isConfirmed) { + localStorage.clear(); + location.replace("/signin"); + } }); - - 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) => {}); - } + } else { + Swal.fire({ + icon: "warning", + title: "Peringatan", + text: message, + }); + } } +// wrapper biar frontend lebih simple 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; - } + try { + const response = await ApiRequest(queryParams); + return response?.data || []; + } catch (error) { + console.error("Request error:", error); + Swal.fire({ icon: "error", text: error.message || "Something went wrong" }); + return []; + } }; export { ApiRequest, SendRequest }; diff --git a/src/layout/LayoutHeader.jsx b/src/layout/LayoutHeader.jsx index 21a7717..457db57 100644 --- a/src/layout/LayoutHeader.jsx +++ b/src/layout/LayoutHeader.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import { Layout, theme, Space, Typography, Breadcrumb, Button } from 'antd'; +import { Layout, Typography, Breadcrumb, Button, theme } 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'; +import { replace, useNavigate } from 'react-router-dom'; const { Link, Text } = Typography; const { Header } = Layout; @@ -12,17 +12,18 @@ const { Header } = Layout; const LayoutHeader = () => { const { breadcrumbItems } = useBreadcrumb(); const navigate = useNavigate(); - const { - token: { colorBgContainer, colorBorder, colorText }, - } = theme.useToken(); + + // Ambil token warna dari theme Ant Design, dengan fallback default + const { token } = theme.useToken() || {}; + const colorBgContainer = token?.colorBgContainer || '#fff'; + const colorBorder = token?.colorBorder || '#d9d9d9'; + const colorText = token?.colorText || '#000'; // 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 ( @@ -35,11 +36,11 @@ const LayoutHeader = () => { alignItems: 'center', flexWrap: 'wrap', rowGap: 10, - paddingTop:15, + paddingTop: 15, paddingBottom: 20, paddingLeft: 24, paddingRight: 24, - minHeight: 100, + minHeight: 100, boxSizing: 'border-box', }} > @@ -88,8 +89,7 @@ const LayoutHeader = () => { { - handleLogOut(); - navigate('/signin'); + handleLogOut(navigate); }} aria-label="Log out from the application" style={{ @@ -117,3 +117,4 @@ const LayoutHeader = () => { }; export default LayoutHeader; + \ No newline at end of file diff --git a/src/pages/auth/Registration.jsx b/src/pages/auth/Registration.jsx index 093c2b1..a836ffd 100644 --- a/src/pages/auth/Registration.jsx +++ b/src/pages/auth/Registration.jsx @@ -1,461 +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'; +// 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([]); +// 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' }, - ]; +// // 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 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 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); +// const uploadResponse = await uploadFile(formData); - if (!uploadResponse.data?.pathKontrak && !uploadResponse.data?.pathHsePlant) { - message.error(uploadResponse.message || 'Gagal mengunggah file.'); - setLoading(false); - return; - } +// 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 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 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); +// const response = await register(registerData); - if (response.data?.id_register) { - message.success('Data berhasil disimpan!'); +// if (response.data?.id_register) { +// message.success('Data berhasil disimpan!'); - try { - form.resetFields(); - setFileListKontrak([]); - setFileListHsse([]); - setFileListIcon([]); +// 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); - } - }; +// 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 onCancel = () => { +// form.resetFields(); +// setFileListKontrak([]); +// setFileListHsse([]); +// setFileListIcon([]); +// navigate('/signin'); +// }; - const handleChangeKontrak = ({ fileList }) => { - setFileListKontrak(fileList); - }; +// const handleChangeKontrak = ({ fileList }) => { +// setFileListKontrak(fileList); +// }; - const handleChangeHsse = ({ fileList }) => { - setFileListHsse(fileList); - }; +// const handleChangeHsse = ({ fileList }) => { +// setFileListHsse(fileList); +// }; - const handleChangeIcon = ({ fileList }) => { - setFileListIcon(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; +// 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; - }; +// 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

- -
- } - > -
- {/* Informasi Perusahaan */} - - Informasi Perusahaan - - - } - placeholder="Masukkan Nama Perusahaan" - size="large" - /> - - - - - - - - - beforeUpload(file, 'path_kontrak')} - fileList={fileListKontrak} - onChange={handleChangeKontrak} - maxCount={1} - > - - - - - beforeUpload(file, 'path_hse_plant')} - fileList={fileListHsse} - onChange={handleChangeHsse} - maxCount={1} - > - - - - - - - - - +// return ( +// +// +//

Formulir Pendaftaran

+// +//
+// } +// > +// +// {/* Informasi Perusahaan */} +// +// Informasi Perusahaan +// +// +// } +// placeholder="Masukkan Nama Perusahaan" +// size="large" +// /> +// +// +// +// +// +// +// +// +// beforeUpload(file, 'path_kontrak')} +// fileList={fileListKontrak} +// onChange={handleChangeKontrak} +// maxCount={1} +// > +// +// +// +// +// beforeUpload(file, 'path_hse_plant')} +// fileList={fileListHsse} +// onChange={handleChangeHsse} +// maxCount={1} +// > +// +// +// +// +// +// +// +// +// - {/* Informasi Penanggung Jawab */} - - Informasi Penanggung Jawab - - - } - placeholder="Masukkan Nama Penanggung Jawab" - size="large" - /> - - - } - placeholder="Masukkan No Handphone (+62)" - size="large" - /> - - - } - placeholder="Masukkan No Identitas" - size="large" - /> - +// {/* Informasi Penanggung Jawab */} +// +// Informasi Penanggung Jawab +// +// +// } +// placeholder="Masukkan Nama Penanggung Jawab" +// size="large" +// /> +// +// +// } +// placeholder="Masukkan No Handphone (+62)" +// size="large" +// /> +// +// +// } +// placeholder="Masukkan No Identitas" +// size="large" +// /> +// - {/* Akun Pengguna */} - - Akun Pengguna (digunakan sebagai user login SYPIU) - - - } - placeholder="Masukkan Email" - size="large" - /> - - - } - placeholder="Masukkan Password" - size="large" - /> - +// {/* Akun Pengguna */} +// +// Akun Pengguna (digunakan sebagai user login SYPIU) +// +// +// } +// placeholder="Masukkan Email" +// size="large" +// /> +// +// +// } +// placeholder="Masukkan Password" +// size="large" +// /> +// - {/* Tombol */} - - - - - - -
- - - ); -}; +// {/* Tombol */} +// +// +// +// +// +// +// +// +// +// ); +// }; -export default Registration; +// export default Registration; diff --git a/src/pages/auth/SignIn.jsx b/src/pages/auth/SignIn.jsx index d2f0d33..5f982e3 100644 --- a/src/pages/auth/SignIn.jsx +++ b/src/pages/auth/SignIn.jsx @@ -1,187 +1,117 @@ +import React, { useEffect, useState } from 'react'; import { Flex, Input, Form, Button, Card, Space, Image } from 'antd'; -import React from 'react'; -import handleSignIn from '../../Utils/Auth/SignIn'; +import { useNavigate } from 'react-router-dom'; +import { NotifAlert, NotifOk } from '../../components/Global/ToastNotif'; +import { SendRequest } from '../../components/Global/ApiRequest'; 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 [captchaSvg, setCaptchaSvg] = useState(''); + const [captchaText, setCaptchaText] = useState(''); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); - const navigate = useNavigate(); - // let url = `${import.meta.env.VITE_API_SERVER}/users`; + const moveToSignUp = () => { + navigate("/signup"); + }; - React.useEffect(() => { + // ambil captcha + const fetchCaptcha = async () => { + try { + const res = await SendRequest({ method: 'get', prefix: 'auth/generate-captcha', token: false }); + setCaptchaSvg(res.data.svg || ''); + setCaptchaText(res.data.text || ''); + } catch (err) { + console.error('Error fetching captcha:', err); + } + }; + + useEffect(() => { fetchCaptcha(); }, []); + + const handleOnSubmit = async (values) => { + setLoading(true); + try { + const res = await SendRequest({ + method: 'post', + prefix: 'auth/login', + params: { + email: values.email, + password: values.password, + captcha: values.captcha, + captchaText: captchaText, + } + }); + + const user = res?.data?.user || res?.user; + const tokens = res?.data?.tokens || res?.tokens; + + if (user && tokens?.accessToken) { + localStorage.setItem('token', tokens.accessToken); + localStorage.setItem('refreshToken', tokens.refreshToken); + localStorage.setItem('user', JSON.stringify(user)); + + NotifOk({ + icon: 'success', + title: 'Login Berhasil', + message: res?.message || 'Selamat datang!', + }); + + navigate('/dashboard/home'); + } else { + NotifAlert({ + icon: 'error', + title: 'Login Gagal', + message: res?.message || 'Terjadi kesalahan', + }); fetchCaptcha(); - }, []); + } + } catch (err) { + console.error(err); + NotifAlert({ icon: 'error', title: 'Login Gagal', message: err?.message || 'Terjadi kesalahan' }); + fetchCaptcha(); + } finally { setLoading(false); } + }; - // 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 - } - ); + return ( + + + + logo + +
+
+ + + - // 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 ( - <> - - - - signin - -
- - - - - - - - -
- {/* {message} */} - setUserInput(e.target.value)} - /> - - - - - - - - - - - - ); + + + + + + + + + + ); }; export default SignIn; diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx index 8bd1d14..42ecbac 100644 --- a/src/pages/auth/Signup.jsx +++ b/src/pages/auth/Signup.jsx @@ -1,158 +1,185 @@ -import { Flex, Input, Form, Button, Card, Space, Image } from 'antd' -import React from 'react' -// import handleSignIn from '../../Utils/Auth/SignIn'; +import React, { useState } from 'react'; +import { Flex, Input, Form, Button, Card, Space, Image, Row, Col, Typography } from 'antd'; +import { useNavigate } from 'react-router-dom'; import sypiu_ggcp from 'assets/sypiu_ggcp.jpg'; -import {useNavigate} from "react-router-dom"; +import logo from 'assets/freepik/LOGOPIU.png'; +import { register } from '../../api/auth'; +import { NotifOk, NotifAlert } from '../../components/Global/ToastNotif'; const SignUp = () => { - - const [captchaSvg, setCaptchaSvg] = React.useState(''); - const [userInput, setUserInput] = React.useState(''); - const [message, setMessage] = React.useState(''); - + const [loading, setLoading] = useState(false); const navigate = useNavigate(); - // let url = `${import.meta.env.VITE_API_SERVER}/users`; - React.useEffect(() => { - fetchCaptcha(); - }, []); + const moveToSignin = () => { + navigate('/signin'); + }; - // Fetch the CAPTCHA SVG from the backend - const fetchCaptcha = async () => { + const handleSignUp = async (values) => { + const { fullname, username, email, phone, password, confirmPassword } = values; + + if (password !== confirmPassword) { + NotifAlert({ + icon: 'error', + title: 'Password Tidak Sama', + message: 'Password dan confirm password harus sama', + }); + return; + } + + const phoneRegex = /^(?:\+62|62|0)8[1-9][0-9]{6,11}$/; + if (!phoneRegex.test(phone)) { + NotifAlert({ + icon: 'error', + title: 'Format Telepon Salah', + message: 'Nomor telepon tidak valid (harus nomor Indonesia)', + }); + return; + } + + const passwordErrors = []; + if (password.length < 8) passwordErrors.push('Minimal 8 karakter'); + if (!/[A-Z]/.test(password)) passwordErrors.push('Harus ada huruf kapital'); + if (!/[0-9]/.test(password)) passwordErrors.push('Harus ada angka'); + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) passwordErrors.push('Harus ada karakter spesial'); + if (passwordErrors.length) { + NotifAlert({ + icon: 'error', + title: 'Password Tidak Valid', + message: passwordErrors.join(', '), + }); + return; + } + + setLoading(true); 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 res = await register({ fullname, username, email, phone, password }); + // const user = res?.data?.user; + // // const tokens = res?.data?.tokens; + + + NotifOk({ + icon: 'success', + title: 'Registrasi Berhasil', + message: res?.data?.message || 'Berhasil menambahkan user.', + }); + + + // if (user) { + // // localStorage.setItem('token', tokens.accessToken); + // // localStorage.setItem('refreshToken', tokens.refreshToken); + // // localStorage.setItem('user', JSON.stringify(user)); + + // NotifOk({ + // icon: 'success', + // title: 'Registrasi Berhasil', + // message: res?.data?.message || 'Selamat datang! Anda akan diarahkan ke dashboard.', + // }); + + // navigate('/signin'); + // } else { + // NotifAlert({ + // icon: 'error', + // title: 'Registrasi Gagal', + // message: res?.data?.message || 'Terjadi kesalahan', + // }); + // } + } catch (err) { + console.error('Register error:', err); + NotifAlert({ + icon: 'error', + title: 'Registrasi Gagal', + message: err?.response?.data?.message || err.message || 'Terjadi kesalahan', + }); + } finally { + setLoading(false); } }; - 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 ( - <> - - + - + +

Registration

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ ); +}; - - - - - - - -
- - - - - - {/* setUserInput(e.target.value)} - /> */} - - - - - - - - - - - - - ) -} - -export default SignUp +export default SignUp; diff --git a/src/pages/home/Home.jsx b/src/pages/home/Home.jsx index 38bd5ab..b080d27 100644 --- a/src/pages/home/Home.jsx +++ b/src/pages/home/Home.jsx @@ -9,44 +9,39 @@ const Home = () => { const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth <= 768); - }; + 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'); - } + setBreadcrumbItems([ + { + title: ( + + Dashboard + + ), + }, + { + title: ( + + Home + + ), + }, + ]); }, []); return ( - Wellcome Call Of Duty App + + Welcome to Call Of Duty App + ); }; -export default Home; +export default Home; \ No newline at end of file From 42fff789e3e9dc68b427979b4f6e47027cab2c93 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 29 Sep 2025 11:48:36 +0700 Subject: [PATCH 002/114] fix register request --- src/components/Global/RegisterRequest.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Global/RegisterRequest.jsx b/src/components/Global/RegisterRequest.jsx index 4bd767c..46deb42 100644 --- a/src/components/Global/RegisterRequest.jsx +++ b/src/components/Global/RegisterRequest.jsx @@ -2,7 +2,7 @@ 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, @@ -12,7 +12,7 @@ const RegistrationRequest = async ({ method, prefix, params, headers = {} }) => 'Accept-Language': 'en_US', ...headers, }, - withCredentials: true, + // withCredentials: true, }); return response.data || {}; @@ -22,4 +22,4 @@ const RegistrationRequest = async ({ method, prefix, params, headers = {} }) => } }; -export default RegistrationRequest; \ No newline at end of file +export default RegistrationRequest; From b9b5704232ad0648f6e5aaaacf3340c28bd5b0c4 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 29 Sep 2025 11:48:54 +0700 Subject: [PATCH 003/114] update signup --- src/pages/auth/Signup.jsx | 337 ++++++++++++++++++++------------------ 1 file changed, 175 insertions(+), 162 deletions(-) diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx index 42ecbac..3ecdd15 100644 --- a/src/pages/auth/Signup.jsx +++ b/src/pages/auth/Signup.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Flex, Input, Form, Button, Card, Space, Image, Row, Col, Typography } from 'antd'; +import { Flex, Input, Form, Button, Card, Space, Image, Row, Col } from 'antd'; import { useNavigate } from 'react-router-dom'; import sypiu_ggcp from 'assets/sypiu_ggcp.jpg'; import logo from 'assets/freepik/LOGOPIU.png'; @@ -7,179 +7,192 @@ import { register } from '../../api/auth'; import { NotifOk, NotifAlert } from '../../components/Global/ToastNotif'; const SignUp = () => { - const [loading, setLoading] = useState(false); - const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const navigate = useNavigate(); - const moveToSignin = () => { - navigate('/signin'); - }; + const [isRegistered, setIsRegistered] = useState(false); - const handleSignUp = async (values) => { - const { fullname, username, email, phone, password, confirmPassword } = values; + const moveToSignin = () => { + navigate('/signin'); + }; - if (password !== confirmPassword) { - NotifAlert({ - icon: 'error', - title: 'Password Tidak Sama', - message: 'Password dan confirm password harus sama', - }); - return; - } + const handleSignUp = async (values) => { + const { fullname, username, email, phone, password, confirmPassword } = values; - const phoneRegex = /^(?:\+62|62|0)8[1-9][0-9]{6,11}$/; - if (!phoneRegex.test(phone)) { - NotifAlert({ - icon: 'error', - title: 'Format Telepon Salah', - message: 'Nomor telepon tidak valid (harus nomor Indonesia)', - }); - return; - } + // Validasi confirm password + if (password !== confirmPassword) { + NotifAlert({ + icon: 'error', + title: 'Password Tidak Sama', + message: 'Password dan confirm password harus sama', + }); + form.resetFields(['password', 'confirmPassword']); + return; + } - const passwordErrors = []; - if (password.length < 8) passwordErrors.push('Minimal 8 karakter'); - if (!/[A-Z]/.test(password)) passwordErrors.push('Harus ada huruf kapital'); - if (!/[0-9]/.test(password)) passwordErrors.push('Harus ada angka'); - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) passwordErrors.push('Harus ada karakter spesial'); - if (passwordErrors.length) { - NotifAlert({ - icon: 'error', - title: 'Password Tidak Valid', - message: passwordErrors.join(', '), - }); - return; - } + // Validasi nomor telepon Indonesia + const phoneRegex = /^(?:\+62|62|0)8[1-9][0-9]{6,11}$/; + if (!phoneRegex.test(phone)) { + NotifAlert({ + icon: 'error', + title: 'Format Telepon Salah', + message: 'Nomor telepon tidak valid (harus nomor Indonesia)', + }); + form.resetFields(['phone']); + return; + } - setLoading(true); - try { - const res = await register({ fullname, username, email, phone, password }); - // const user = res?.data?.user; - // // const tokens = res?.data?.tokens; + // Validasi password kompleks + const passwordErrors = []; + if (password.length < 8) passwordErrors.push('Minimal 8 karakter'); + if (!/[A-Z]/.test(password)) passwordErrors.push('Harus ada huruf kapital'); + if (!/[0-9]/.test(password)) passwordErrors.push('Harus ada angka'); + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) + passwordErrors.push('Harus ada karakter spesial'); + if (passwordErrors.length) { + NotifAlert({ + icon: 'error', + title: 'Password Tidak Valid', + message: passwordErrors.join(', '), + }); + form.resetFields(['password', 'confirmPassword']); + return; + } + setLoading(true); + try { + const res = await register({ fullname, username, email, phone, password }); - NotifOk({ - icon: 'success', - title: 'Registrasi Berhasil', - message: res?.data?.message || 'Berhasil menambahkan user.', - }); + NotifOk({ + icon: 'success', + title: 'Registrasi Berhasil', + message: res?.data?.message || 'Berhasil menambahkan user.', + }); + form.resetFields(); + setIsRegistered(true); + // navigate('/signin'); + } catch (err) { + console.error('Register error:', err); + const errorMessage = err?.response?.data?.message || err.message || 'Terjadi kesalahan'; - // if (user) { - // // localStorage.setItem('token', tokens.accessToken); - // // localStorage.setItem('refreshToken', tokens.refreshToken); - // // localStorage.setItem('user', JSON.stringify(user)); + NotifAlert({ + icon: 'error', + title: 'Registrasi Gagal', + message: errorMessage || 'Terjadi kesalahan', + }); + if (errorMessage.toLowerCase().includes('already')) { + form.resetFields(); + } + } finally { + setLoading(false); + } + }; - // NotifOk({ - // icon: 'success', - // title: 'Registrasi Berhasil', - // message: res?.data?.message || 'Selamat datang! Anda akan diarahkan ke dashboard.', - // }); - - // navigate('/signin'); - // } else { - // NotifAlert({ - // icon: 'error', - // title: 'Registrasi Gagal', - // message: res?.data?.message || 'Terjadi kesalahan', - // }); - // } - } catch (err) { - console.error('Register error:', err); - NotifAlert({ - icon: 'error', - title: 'Registrasi Gagal', - message: err?.response?.data?.message || err.message || 'Terjadi kesalahan', - }); - } finally { - setLoading(false); - } - }; - - return ( - - - {/* Logo di dalam Card */} - logo - -

Registration

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- ); + + logo + +

Registration

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + ); }; export default SignUp; From 6de6d35a6bc39cb20b6b7c99887084702d1c6087 Mon Sep 17 00:00:00 2001 From: Fachba Date: Mon, 29 Sep 2025 16:23:20 +0700 Subject: [PATCH 004/114] example dashboard svg mqtt --- .env.example | 5 + package.json | 1 + src/App.jsx | 2 + src/components/Global/MqttConnection.jsx | 101 ++-- src/pages/home/SvgTest.jsx | 37 ++ svg/test-new.svg | 566 +++++++++++++++++++++++ 6 files changed, 675 insertions(+), 37 deletions(-) create mode 100644 .env.example create mode 100644 src/pages/home/SvgTest.jsx create mode 100644 svg/test-new.svg diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..62bd0b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +VITE_API_SERVER=http://36.66.16.49:9528/api +VITE_MQTT_SERVER=ws://localhost:1884 +VITE_MQTT_USERNAME= +VITE_MQTT_PASSWORD= +VITE_KEY_SESSION=PetekRombonganPetekMorekMorakMarek \ No newline at end of file diff --git a/package.json b/package.json index 6167a07..21bd8b0 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-router-dom": "^6.22.3", + "react-svg": "^16.3.0", "sweetalert2": "^11.17.2" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx index 659b307..8d2c595 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,6 +11,7 @@ import Blank from './pages/blank/Blank'; // Master import IndexDevice from './pages/master/device/IndexDevice'; +import SvgTest from './pages/home/SvgTest'; const App = () => { return ( @@ -20,6 +21,7 @@ const App = () => { } /> } /> } /> + } /> {/* Protected Routes */} }> diff --git a/src/components/Global/MqttConnection.jsx b/src/components/Global/MqttConnection.jsx index 1a9551b..731a3fe 100644 --- a/src/components/Global/MqttConnection.jsx +++ b/src/components/Global/MqttConnection.jsx @@ -1,19 +1,18 @@ // 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 mqttUrl = `${import.meta.env.VITE_MQTT_SERVER ?? 'ws://localhost:1884'}`; +const topics = ['PIU_GGCP/Devices/PB']; 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 + keepalive: 30, + clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8), + protocolId: 'MQTT', + protocolVersion: 4, + clean: true, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + username: `${import.meta.env.VITE_MQTT_USERNAME ?? ''}`, // jika ada + password: `${import.meta.env.VITE_MQTT_PASSWORD ?? ''}`, // jika ada }; const client = mqtt.connect(mqttUrl, options); @@ -22,37 +21,37 @@ const client = mqtt.connect(mqttUrl, options); let isConnected = false; client.on('connect', () => { - console.log('MQTT Connected'); - isConnected = true; + 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(', ')}`); - }); + // 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(); + console.error('Connection error: ', err); + client.end(); }); client.on('close', () => { - console.log('MQTT Disconnected'); - isConnected = false; + console.log('MQTT Disconnected'); + isConnected = false; }); /** * Publish message to MQTT - * @param {string} topic - * @param {string} message + * @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'); - } + if (client && isConnected && message.trim() !== '') { + client.publish(topic, message); + } else { + console.warn('MQTT not connected or message empty'); + } }; /** @@ -60,13 +59,41 @@ const publishMessage = (topic, message) => { * @param {function} callback - Function(topic, message) */ const listenMessage = (callback) => { - client.on('message', (topic, message) => { - callback(topic, message.toString()); - }); + client.on('message', (topic, message) => { + callback(topic, message.toString()); + }); }; -export { - publishMessage, - listenMessage, - client, +const listenMessageState = (setState) => { + client.on('message', (topic, message) => { + const msgObj = { + date: new Date().toLocaleString(), + topic: topic, + msg: JSON.parse(message), + }; + setState(msgObj); + }); }; + +const setValSvg = (msgValue, topic, svg) => { + if (msgValue.topic == topic) { + const objChanel = msgValue?.msg; + Object.entries(objChanel).forEach(([key, value]) => { + // console.log(key, value); + const el = svg.getElementById(key); + if (el) { + if (value === true) { + el.style.display = ''; // sembunyikan + } else if (value === false) { + el.style.display = 'none'; + } else if (!isNaN(value)) { + el.textContent = Number(value ?? 0.0); + } else { + el.textContent = value; + } + } + }); + } +}; + +export { publishMessage, listenMessage, listenMessageState, setValSvg }; diff --git a/src/pages/home/SvgTest.jsx b/src/pages/home/SvgTest.jsx new file mode 100644 index 0000000..976fc4b --- /dev/null +++ b/src/pages/home/SvgTest.jsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { ReactSVG } from 'react-svg'; +import { listenMessageState, setValSvg } from '../../components/Global/MqttConnection'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgTest = () => { + const [received, setReceived] = useState([]); + + listenMessageState(setReceived) + + useEffect(() => {}, []); + + return ( + <> + + + + Example SVG Value By Mqtt + + + + { + setValSvg(received, topicMqtt, svg); + }} + /> + + ); +}; + +export default SvgTest; diff --git a/svg/test-new.svg b/svg/test-new.svg new file mode 100644 index 0000000..8628689 --- /dev/null +++ b/svg/test-new.svg @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + STARTER + GENERATOR + + + + + + + + + + + + + + + + + ###.## + 01-PT008-A + + ###.## + 01-ZT002 + + ###.## + 01-TE014-A + + ###.## + 01-TE014-B + + ###.## + 01-TE015-A + + ###.## + 01-TE015-B + 01-PT008-B + + ###.## + 01-PT008-C + + ###.## + + ###.## + 01-KT001 + + ###.## + 01-PDT007 + + ###.## + 01-TE019 + + + + + + \ No newline at end of file From b73abf2fa2865cd82ed1f51b9deb21efd23d7198 Mon Sep 17 00:00:00 2001 From: Fachba Date: Mon, 29 Sep 2025 16:38:00 +0700 Subject: [PATCH 005/114] simplyfy code svg value mqtt --- src/components/Global/MqttConnection.jsx | 50 ++++++++++-------------- src/pages/home/SvgTest.jsx | 10 +---- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/components/Global/MqttConnection.jsx b/src/components/Global/MqttConnection.jsx index 731a3fe..ab8287c 100644 --- a/src/components/Global/MqttConnection.jsx +++ b/src/components/Global/MqttConnection.jsx @@ -64,36 +64,28 @@ const listenMessage = (callback) => { }); }; -const listenMessageState = (setState) => { +const setValSvg = (listenTopic, svg) => { client.on('message', (topic, message) => { - const msgObj = { - date: new Date().toLocaleString(), - topic: topic, - msg: JSON.parse(message), - }; - setState(msgObj); + if (topic == listenTopic) { + const objChanel = JSON.parse(message); + + Object.entries(objChanel).forEach(([key, value]) => { + // console.log(key, value); + const el = svg.getElementById(key); + if (el) { + if (value === true) { + el.style.display = ''; // sembunyikan + } else if (value === false) { + el.style.display = 'none'; + } else if (!isNaN(value)) { + el.textContent = Number(value ?? 0.0); + } else { + el.textContent = value; + } + } + }); + } }); }; -const setValSvg = (msgValue, topic, svg) => { - if (msgValue.topic == topic) { - const objChanel = msgValue?.msg; - Object.entries(objChanel).forEach(([key, value]) => { - // console.log(key, value); - const el = svg.getElementById(key); - if (el) { - if (value === true) { - el.style.display = ''; // sembunyikan - } else if (value === false) { - el.style.display = 'none'; - } else if (!isNaN(value)) { - el.textContent = Number(value ?? 0.0); - } else { - el.textContent = value; - } - } - }); - } -}; - -export { publishMessage, listenMessage, listenMessageState, setValSvg }; +export { publishMessage, listenMessage, setValSvg }; diff --git a/src/pages/home/SvgTest.jsx b/src/pages/home/SvgTest.jsx index 976fc4b..3ee33f8 100644 --- a/src/pages/home/SvgTest.jsx +++ b/src/pages/home/SvgTest.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Card, Typography, Flex } from 'antd'; import { ReactSVG } from 'react-svg'; -import { listenMessageState, setValSvg } from '../../components/Global/MqttConnection'; +import { setValSvg } from '../../components/Global/MqttConnection'; const { Text } = Typography; @@ -9,12 +9,6 @@ const filePathSvg = '/svg/test-new.svg'; const topicMqtt = 'PIU_GGCP/Devices/PB'; const SvgTest = () => { - const [received, setReceived] = useState([]); - - listenMessageState(setReceived) - - useEffect(() => {}, []); - return ( <> @@ -27,7 +21,7 @@ const SvgTest = () => { { - setValSvg(received, topicMqtt, svg); + setValSvg(topicMqtt, svg); }} /> From ba0b145bda4acb57d73cc664b119ca7c6a09f1f7 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:11:20 +0700 Subject: [PATCH 006/114] update: credentials true --- src/components/Global/RegisterRequest.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Global/RegisterRequest.jsx b/src/components/Global/RegisterRequest.jsx index 46deb42..e1ca092 100644 --- a/src/components/Global/RegisterRequest.jsx +++ b/src/components/Global/RegisterRequest.jsx @@ -12,7 +12,7 @@ const RegistrationRequest = async ({ method, prefix, params, headers = {} }) => 'Accept-Language': 'en_US', ...headers, }, - // withCredentials: true, + withCredentials: true, }); return response.data || {}; From 9dabbd60eab97ab77c5b0845bb13102ed8e1aedb Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:11:30 +0700 Subject: [PATCH 007/114] fix: handle submit --- src/pages/auth/SignIn.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/auth/SignIn.jsx b/src/pages/auth/SignIn.jsx index 5f982e3..cbac2a3 100644 --- a/src/pages/auth/SignIn.jsx +++ b/src/pages/auth/SignIn.jsx @@ -40,15 +40,15 @@ const SignIn = () => { password: values.password, captcha: values.captcha, captchaText: captchaText, - } + }, + withCredentials: true }); const user = res?.data?.user || res?.user; - const tokens = res?.data?.tokens || res?.tokens; + const accessToken = res?.data?.accessToken || res?.tokens?.accessToken; - if (user && tokens?.accessToken) { - localStorage.setItem('token', tokens.accessToken); - localStorage.setItem('refreshToken', tokens.refreshToken); + if (user && accessToken) { + localStorage.setItem('token', accessToken); localStorage.setItem('user', JSON.stringify(user)); NotifOk({ From 6b4b511d66a085c15dcc2bf63a502df2c2c71088 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:11:56 +0700 Subject: [PATCH 008/114] fix: error --- src/pages/home/SvgTest.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/SvgTest.jsx b/src/pages/home/SvgTest.jsx index 3ee33f8..1b65a69 100644 --- a/src/pages/home/SvgTest.jsx +++ b/src/pages/home/SvgTest.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { Card, Typography, Flex } from 'antd'; -import { ReactSVG } from 'react-svg'; +// import { ReactSVG } from 'react-svg'; import { setValSvg } from '../../components/Global/MqttConnection'; const { Text } = Typography; From ef67cdd91cac671d71945d8c3026fe463c097b56 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Wed, 1 Oct 2025 10:27:58 +0700 Subject: [PATCH 009/114] update layout header --- src/layout/LayoutHeader.jsx | 48 ++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/layout/LayoutHeader.jsx b/src/layout/LayoutHeader.jsx index 457db57..0cb0a8a 100644 --- a/src/layout/LayoutHeader.jsx +++ b/src/layout/LayoutHeader.jsx @@ -4,7 +4,7 @@ import { UserOutlined } from '@ant-design/icons'; import handleLogOut from '../Utils/Auth/Logout'; import { useBreadcrumb } from './LayoutBreadcrumb'; import { decryptData } from '../components/Global/Formatter'; -import { replace, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; const { Link, Text } = Typography; const { Header } = Layout; @@ -13,18 +13,51 @@ const LayoutHeader = () => { const { breadcrumbItems } = useBreadcrumb(); const navigate = useNavigate(); - // Ambil token warna dari theme Ant Design, dengan fallback default + // Ambil token warna dari theme Ant Design const { token } = theme.useToken() || {}; const colorBgContainer = token?.colorBgContainer || '#fff'; const colorBorder = token?.colorBorder || '#d9d9d9'; const colorText = token?.colorText || '#000'; - // Ambil data user dari localStorage dan dekripsi - const sessionData = localStorage.getItem('session'); - const userData = sessionData ? decryptData(sessionData) : null; + // Ambil data user dari localStorage + let userData = null; - const roleName = userData?.user?.approval || userData?.user?.partner_name || 'Guest'; - const userName = userData?.user?.name || userData?.user?.username || 'User'; + const sessionData = localStorage.getItem('session'); + if (sessionData) { + userData = decryptData(sessionData); + } else { + const userRaw = localStorage.getItem('user'); + if (userRaw) { + try { + // bungkus biar konsisten { user: {...} } + userData = { user: JSON.parse(userRaw) }; + } catch (e) { + console.error('Gagal parse user dari localStorage:', e); + } + } + } + + // console.log('User data di header:', userData?.user); + + // Role handling + const roleNameDefault = + userData?.user?.approval || + userData?.user?.partner_name || + userData?.user?.role_name || + 'Guest'; + + let roleName = roleNameDefault; + const userName = + userData?.user?.name || userData?.user?.username || userData?.user?.user_name || 'User'; + + // Override jika Super Admin + if ( + userData?.user?.is_sa === true || + userData?.user?.is_sa === 'true' || + userData?.user?.is_sa === 1 + ) { + roleName = 'Super Admin'; + } return ( <> @@ -117,4 +150,3 @@ const LayoutHeader = () => { }; export default LayoutHeader; - \ No newline at end of file From d362c041ac7a748181e057f94667d044e33b621f Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 10:49:05 +0700 Subject: [PATCH 010/114] add: credential true --- src/components/Global/ApiRequest.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Global/ApiRequest.jsx b/src/components/Global/ApiRequest.jsx index 20cf2ae..c0f8c67 100644 --- a/src/components/Global/ApiRequest.jsx +++ b/src/components/Global/ApiRequest.jsx @@ -22,6 +22,7 @@ async function ApiRequest({ "Accept-Language": "en_US", ...(isFormData ? {} : { "Content-Type": "application/json" }), }, + withCredentials: true, }; // handle download (doc) From 6a5b01bf0c490d78e0134f7fc64975af2f3f7071 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 11:02:59 +0700 Subject: [PATCH 011/114] add: auth context --- src/context/AuthContext.jsx | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/context/AuthContext.jsx diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx new file mode 100644 index 0000000..fb4d9b5 --- /dev/null +++ b/src/context/AuthContext.jsx @@ -0,0 +1,42 @@ +import { createContext, useContext, useState, useEffect } from "react"; +import { SendRequest } from "../utils/api"; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // fetch user info saat mount + useEffect(() => { + const fetchUser = async () => { + try { + const res = await SendRequest({ prefix: "/auth/me", method: "GET" }); + setUser(res); + } catch (err) { + setUser(null); + } finally { + setLoading(false); + } + }; + fetchUser(); + }, []); + + const login = (accessToken) => { + localStorage.setItem("token", accessToken); + }; + + const logout = () => { + localStorage.clear(); + setUser(null); + window.location.href = "/signin"; + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); From fb5528616c9c7c0865cbec1c45d623140ce133c5 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Wed, 1 Oct 2025 11:03:11 +0700 Subject: [PATCH 012/114] update: api request --- src/components/Global/ApiRequest.jsx | 87 ++++++++++++++++++---------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/src/components/Global/ApiRequest.jsx b/src/components/Global/ApiRequest.jsx index c0f8c67..ad37baa 100644 --- a/src/components/Global/ApiRequest.jsx +++ b/src/components/Global/ApiRequest.jsx @@ -1,19 +1,54 @@ import axios from "axios"; import Swal from "sweetalert2"; +const baseURL = import.meta.env.VITE_API_SERVER; + +const instance = axios.create({ + baseURL, + withCredentials: true, +}); + +instance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + try { + const refreshRes = await axios.post( + `${baseURL}/auth/refresh`, + {}, + { withCredentials: true } + ); + + const newAccessToken = refreshRes.data.accessToken; + localStorage.setItem("token", newAccessToken); + + instance.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`; + originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; + + return instance(originalRequest); + } catch (refreshError) { + console.error("Refresh token gagal:", refreshError); + localStorage.clear(); + window.location.href = "/signin"; + } + } + + return Promise.reject(error); + } +); + async function ApiRequest({ method = "GET", params = {}, - url = "", prefix = "/", token = true, } = {}) { - const baseURL = url || import.meta.env.VITE_API_SERVER; - const instance = axios.create({ baseURL }); - const isFormData = params instanceof FormData; - // request config const request = { method, url: prefix, @@ -25,20 +60,10 @@ async function ApiRequest({ withCredentials: true, }; - // handle download (doc) - if (params === "doc") { - request.responseType = "blob"; - } - - // ambil token - const rawToken = - sessionStorage.getItem("token_redirect") || localStorage.getItem("token"); + const rawToken = localStorage.getItem("token"); if (token && rawToken) { - const cleanToken = rawToken.replace(/"/g, ""); - instance.defaults.headers.common[ - "Authorization" - ] = `Bearer ${cleanToken}`; + request.headers["Authorization"] = `Bearer ${rawToken.replace(/"/g, "")}`; } try { @@ -46,31 +71,27 @@ async function ApiRequest({ return { ...response, error: false }; } catch (error) { const status = error?.response?.status || 500; - const message = - error?.response?.data?.message || error.message || "Something Wrong"; + const message = error?.response?.data?.message || error.message || "Something Wrong"; - cekError(status, message); + await cekError(status, message); return { ...error.response, error: true }; } } -// global error handler +// =============================== +// Global error handler +// =============================== async function cekError(status, message = "") { - console.log("status code", status); - if (status === 401) { - Swal.fire({ + await Swal.fire({ icon: "warning", title: "Peringatan", text: `${message}, Silahkan login`, - }).then((result) => { - if (result.isConfirmed) { - localStorage.clear(); - location.replace("/signin"); - } }); + localStorage.clear(); + window.location.href = "/signin"; } else { - Swal.fire({ + await Swal.fire({ icon: "warning", title: "Peringatan", text: message, @@ -78,14 +99,16 @@ async function cekError(status, message = "") { } } -// wrapper biar frontend lebih simple +// =============================== +// Wrapper simpler +// =============================== const SendRequest = async (queryParams) => { try { const response = await ApiRequest(queryParams); return response?.data || []; } catch (error) { console.error("Request error:", error); - Swal.fire({ icon: "error", text: error.message || "Something went wrong" }); + await Swal.fire({ icon: "error", text: error.message || "Something went wrong" }); return []; } }; From 2778c96143eeb169e0a2ee8ac5751bd79d481ed8 Mon Sep 17 00:00:00 2001 From: Vinix Date: Wed, 1 Oct 2025 11:33:10 +0700 Subject: [PATCH 013/114] add: device API functions for CRUD device --- src/api/master-device.jsx | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/api/master-device.jsx diff --git a/src/api/master-device.jsx b/src/api/master-device.jsx new file mode 100644 index 0000000..15ffc23 --- /dev/null +++ b/src/api/master-device.jsx @@ -0,0 +1,45 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllDevice = async (queryParams) => { + const response = await SendRequest({ + method: 'get', + prefix: `device?${queryParams.toString()}`, + }); + return response; +}; + +const getDeviceById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `device/${id}`, + }); + return response.data; +}; + +const createDevice = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `device`, + params: queryParams, + }); + return response.data; +}; + +const updateDevice = async (device_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `device/${device_id}`, + params: queryParams, + }); + return response.data; +}; + +const deleteDevice = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `device/${queryParams}`, + }); + return response.data; +}; + +export { getAllDevice, getDeviceById, createDevice, updateDevice, deleteDevice }; From 3b51f59679a71e27b276eb6a04cbd4890a8d875e Mon Sep 17 00:00:00 2001 From: Vinix Date: Wed, 1 Oct 2025 14:33:55 +0700 Subject: [PATCH 014/114] update: enhance device API functions with pagination and response structure; modify device form validation and UI components --- src/api/master-device.jsx | 58 +++- src/pages/master/device/IndexDevice.jsx | 2 +- .../master/device/component/DetailDevice.jsx | 293 ++++++++++++------ .../master/device/component/ListDevice.jsx | 224 +++++++------ 4 files changed, 353 insertions(+), 224 deletions(-) diff --git a/src/api/master-device.jsx b/src/api/master-device.jsx index 15ffc23..4b1e84f 100644 --- a/src/api/master-device.jsx +++ b/src/api/master-device.jsx @@ -5,7 +5,39 @@ const getAllDevice = async (queryParams) => { method: 'get', prefix: `device?${queryParams.toString()}`, }); - return response; + console.log('getAllDevice response:', response); + console.log('Query params:', queryParams.toString()); + + // Parse query params to get page and limit + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + // Backend returns all data, so we need to do client-side pagination + const allData = response.data || []; + const totalData = allData.length; + + // Calculate start and end index for current page + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + + // Slice data for current page + const paginatedData = allData.slice(startIndex, endIndex); + + // Transform response to match TableList expected structure + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; }; const getDeviceById = async (id) => { @@ -22,7 +54,13 @@ const createDevice = async (queryParams) => { prefix: `device`, params: queryParams, }); - return response.data; + console.log('createDevice full response:', response); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; }; const updateDevice = async (device_id, queryParams) => { @@ -31,7 +69,13 @@ const updateDevice = async (device_id, queryParams) => { prefix: `device/${device_id}`, params: queryParams, }); - return response.data; + console.log('updateDevice full response:', response); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; }; const deleteDevice = async (queryParams) => { @@ -39,7 +83,13 @@ const deleteDevice = async (queryParams) => { method: 'delete', prefix: `device/${queryParams}`, }); - return response.data; + console.log('deleteDevice full response:', response); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; }; export { getAllDevice, getDeviceById, createDevice, updateDevice, deleteDevice }; diff --git a/src/pages/master/device/IndexDevice.jsx b/src/pages/master/device/IndexDevice.jsx index 0ea060c..1649089 100644 --- a/src/pages/master/device/IndexDevice.jsx +++ b/src/pages/master/device/IndexDevice.jsx @@ -66,7 +66,7 @@ const IndexDevice = memo(function IndexDevice() { setSelectedData={setSelectedData} readOnly={readOnly} showModal={showModal} - permitDefault={true} + permitDefault={false} actionMode={actionMode} /> {actionMode == 'generatepdf' && ( diff --git a/src/pages/master/device/component/DetailDevice.jsx b/src/pages/master/device/component/DetailDevice.jsx index 77ab15f..fe299dc 100644 --- a/src/pages/master/device/component/DetailDevice.jsx +++ b/src/pages/master/device/component/DetailDevice.jsx @@ -1,21 +1,25 @@ import React, { useEffect, useState } from 'react'; -import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Radio } from 'antd'; +import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Radio, Select } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd'; +import { createDevice, updateDevice } from '../../../../api/master-device'; import { Checkbox } from 'antd'; const CheckboxGroup = Checkbox.Group; const { Text } = Typography; +const { TextArea } = Input; const DetailDevice = (props) => { const [confirmLoading, setConfirmLoading] = useState(false); const defaultData = { - id_apd: '', - nama_apd: '', - type_input: 1, - is_active: true, - jenis_permit_default: [], + device_id: '', + device_code: '', + device_name: '', + device_status: true, + device_location: 'Building A', + device_description: '', + ip_address: '', }; const [FormData, setFormData] = useState(defaultData); @@ -50,16 +54,62 @@ const DetailDevice = (props) => { props.setActionMode('list'); }; + const validateIPAddress = (ip) => { + const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + return ipRegex.test(ip); + }; + const handleSave = async () => { setConfirmLoading(true); - if (!FormData.nama_apd) { + // Validasi required fields + if (!FormData.device_code) { NotifOk({ icon: 'warning', title: 'Peringatan', - message: 'Kolom Nama APD Tidak Boleh Kosong', + message: 'Kolom Device Code Tidak Boleh Kosong', }); + setConfirmLoading(false); + return; + } + if (!FormData.device_name) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Device Name Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.device_location) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Device Location Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.ip_address) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom IP Address Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + // Validasi format IP + if (!validateIPAddress(FormData.ip_address)) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Format IP Address Tidak Valid', + }); setConfirmLoading(false); return; } @@ -76,52 +126,49 @@ const DetailDevice = (props) => { } const payload = { - nama_apd: FormData.nama_apd, - is_active: FormData.is_active, - type_input: FormData.type_input, - jenis_permit_default: checkedList, + device_code: FormData.device_code, + device_name: FormData.device_name, + device_status: FormData.device_status, + device_location: FormData.device_location, + device_description: FormData.device_description, + ip_address: FormData.ip_address, }; - if (props.permitDefault) { - try { - let response; - if (!FormData.id_apd) { - response = await createApd(payload); - } else { - response = await updateApd(FormData.id_apd, payload); - } + try { + let response; + if (!FormData.device_id) { + response = await createDevice(payload); + } else { + response = await updateDevice(FormData.device_id, payload); + } - if (response.statusCode === 200) { - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: `Data "${response.data.nama_apd}" berhasil ${ - FormData.id_apd ? 'diubah' : 'ditambahkan' - }.`, - }); + console.log('Save Device Response:', response); - props.setActionMode('list'); - } else { - NotifAlert({ - icon: 'error', - title: 'Gagal', - message: response.message || 'Terjadi kesalahan saat menyimpan data.', - }); - } - } catch (error) { + // Check if response is successful + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Device "${response.data?.device_name || FormData.device_name}" berhasil ${ + FormData.device_id ? 'diubah' : 'ditambahkan' + }.`, + }); + + props.setActionMode('list'); + } else { NotifAlert({ icon: 'error', - title: 'Error', - message: 'Terjadi kesalahan pada server. Coba lagi nanti.', + title: 'Gagal', + message: response?.message || 'Terjadi kesalahan saat menyimpan data.', }); } - } else { - props.setData((prevData) => [ - ...prevData, - { nama_apd: payload.nama_apd, type_input: payload.type_input }, - ]); - - props.setActionMode('list'); + } catch (error) { + console.error('Save Device Error:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', + }); } setConfirmLoading(false); @@ -139,22 +186,35 @@ const DetailDevice = (props) => { const isChecked = event; setFormData({ ...FormData, - is_active: isChecked ? true : false, + device_status: isChecked ? true : false, + }); + }; + + const handleSelectChange = (value) => { + setFormData({ + ...FormData, + device_location: value, }); }; useEffect(() => { const token = localStorage.getItem('token'); if (token) { - getDataJenisPermit(); + // Only call getDataJenisPermit if permitDefault is enabled + if (props.permitDefault) { + getDataJenisPermit(); + } + if (props.selectedData != null) { setFormData(props.selectedData); - setCheckedList(props.selectedData.jenis_permit_default_arr); + if (props.permitDefault && props.selectedData.jenis_permit_default_arr) { + setCheckedList(props.selectedData.jenis_permit_default_arr); + } } else { setFormData(defaultData); } } else { - navigate('/signin'); + // navigate('/signin'); // Uncomment if useNavigate is imported } }, [props.showModal]); @@ -217,74 +277,103 @@ const DetailDevice = (props) => { > {FormData && (
- {props.permitDefault && ( - <> -
-
- Aktif -
-
+
+ Device Status +
+
+
+ -
- -
-
- - {FormData.is_active == 1 ? 'Aktif' : 'Non Aktif'} - -
-
+ checked={FormData.device_status === true} + onChange={handleStatusToggle} + />
- - - )} +
+ + {FormData.device_status === true ? 'Running' : 'Offline'} + +
+
+
+ -
- Device Name +
+ Device Code *
-
- Tipe Input - * -
- -
+
+ Device Name + * + +
+
+ Device Location + * + +
+
+ Device Description +