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.css b/src/App.css index e69de29..464d0c1 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,6 @@ +/* Global Card Styling */ +.ant-card { + border-radius: 12px !important; + box-shadow: 0 5px 10px 5px rgba(0, 0, 0, 0.07) !important; + margin-bottom: 24px !important; +} diff --git a/src/App.jsx b/src/App.jsx index 4872856..511886c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,44 +1,52 @@ 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'; +import IndexTag from './pages/master/tag/IndexTag'; +import IndexUnit from './pages/master/unit/IndexUnit'; +import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; +import IndexPlantSection from './pages/master/plantSection/IndexPlantSection'; +import IndexStatus from './pages/master/status/IndexStatus'; +import IndexShift from './pages/master/shift/IndexShift'; -// Setting +// Jadwal Shift +import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift'; + +// History +import IndexTrending from './pages/history/trending/IndexTrending'; +import IndexReport from './pages/history/report/IndexReport'; + +// Other Pages +import IndexNotification from './pages/notification/IndexNotification'; +import IndexEventAlarm from './pages/eventAlarm/IndexEventAlarm'; +import IndexRole from './pages/role/IndexRole'; +import IndexUser from './pages/user/IndexUser'; + +// Shift Management +import IndexMember from './pages/shiftManagement/member/IndexMember'; + +import SvgTest from './pages/home/SvgTest'; 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 */} }> } /> } /> @@ -46,8 +54,44 @@ 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..3f539d6 100644 --- a/src/Utils/Auth/Logout.jsx +++ b/src/Utils/Auth/Logout.jsx @@ -1,7 +1,13 @@ -const handleLogOut = () => { - localStorage.removeItem('Auth'); - localStorage.removeItem('session'); - window.location.replace('/signin'); -} +const handleLogOut = (navigate) => { + // Hapus semua data localStorage + localStorage.clear(); -export default handleLogOut; \ No newline at end of file + // Redirect ke halaman signin + if (navigate) { + navigate('/signin', { replace: true }); + } else { + window.location.replace('/signin'); + } +}; + +export default handleLogOut; 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/api/jadwal-shift.jsx b/src/api/jadwal-shift.jsx new file mode 100644 index 0000000..2ae403a --- /dev/null +++ b/src/api/jadwal-shift.jsx @@ -0,0 +1,67 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllJadwalShift = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `jadwal-shift?${queryParams.toString()}`, + }); + return response; + } catch (error) { + console.error('getAllJadwalShift error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const createJadwalShift = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `jadwal-shift`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const updateJadwalShift = async (id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `jadwal-shift/${id}`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const deleteJadwalShift = async (id) => { + const response = await SendRequest({ + method: 'delete', + prefix: `jadwal-shift/${id}`, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +export { getAllJadwalShift, createJadwalShift, updateJadwalShift, deleteJadwalShift }; diff --git a/src/api/master-device.jsx b/src/api/master-device.jsx new file mode 100644 index 0000000..9493cc8 --- /dev/null +++ b/src/api/master-device.jsx @@ -0,0 +1,169 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllDevice = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `device?${queryParams.toString()}`, + }); + console.log('getAllDevice response:', response); + console.log('Query params:', queryParams.toString()); + + // Backend response structure: + // { + // statusCode: 200, + // data: [...devices], + // paging: { + // current_page: 1, + // current_limit: 10, + // total_limit: 50, + // total_page: 5 + // } + // } + + // Check if backend returns paginated data + if (response.paging) { + const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0; + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: response.paging.current_page || 1, + limit: response.paging.current_limit || 10, + total: totalData, + page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10)) + }, + total: totalData + } + }; + } + + // Fallback: If backend returns all data without pagination (old behavior) + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllDevice error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +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, + }); + console.log('createDevice full response:', response); + console.log('createDevice payload sent:', queryParams); + + // Backend returns: { statusCode, message, rows, data: [device_object] } + // Check if response is empty array (error from SendRequest) + if (Array.isArray(response) && response.length === 0) { + return { + statusCode: 500, + data: null, + message: 'Request failed', + rows: 0 + }; + } + + // Extract first item from data array + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateDevice = async (device_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `device/${device_id}`, + params: queryParams, + }); + console.log('updateDevice full response:', response); + console.log('updateDevice payload sent:', queryParams); + + // Backend returns: { statusCode, message, rows, data: [device_object] } + // Check if response is empty array (error from SendRequest) + if (Array.isArray(response) && response.length === 0) { + return { + statusCode: 500, + data: null, + message: 'Request failed', + rows: 0 + }; + } + + // Extract first item from data array + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteDevice = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `device/${queryParams}`, + }); + console.log('deleteDevice full response:', response); + // Backend returns: { statusCode, message, rows: null, data: true } + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllDevice, getDeviceById, createDevice, updateDevice, deleteDevice }; diff --git a/src/api/master-plant-section.jsx b/src/api/master-plant-section.jsx new file mode 100644 index 0000000..594723b --- /dev/null +++ b/src/api/master-plant-section.jsx @@ -0,0 +1,187 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllPlantSection = async (queryParams) => { + try { + // Ensure queryParams is URLSearchParams object + const params = queryParams instanceof URLSearchParams ? queryParams : new URLSearchParams(queryParams); + + const response = await SendRequest({ + method: 'get', + prefix: `plant-sub-section?${params.toString()}`, + }); + console.log('getAllPlantSection response:', response); + console.log('Query params:', params.toString()); + + // Backend response structure: + // { + // statusCode: 200, + // data: [...plantSections], + // paging: { + // current_page: 1, + // current_limit: 10, + // total_limit: 50, + // total_page: 5 + // } + // } + + // Check if backend returns paginated data + if (response.paging) { + // Extract total_data from first record, or fallback to total_limit or rows + const totalData = response.data?.[0]?.total_data || response.paging.total_limit || response.rows || response.data?.length || 0; + + // Use total_limit as total count, handle 0 values for page/limit + const currentPage = response.paging.current_page || 1; + const currentLimit = response.paging.current_limit || 10; + const totalPages = response.paging.total_page || Math.ceil(totalData / currentLimit); + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: totalPages + }, + total: totalData + } + }; + } + + // Fallback: If backend returns all data without pagination (old behavior) + const parsedParams = Object.fromEntries(params); + const currentPage = parseInt(parsedParams.page) || 1; + const currentLimit = parseInt(parsedParams.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllPlantSection error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getPlantSectionById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `plant-sub-section/${id}`, + }); + return response.data; +}; + +const createPlantSection = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `plant-sub-section`, + params: queryParams, + }); + console.log('createPlantSection full response:', response); + console.log('createPlantSection payload sent:', queryParams); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [plantSection_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updatePlantSection = async (plant_section_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `plant-sub-section/${plant_section_id}`, + params: queryParams, + }); + console.log('updatePlantSection full response:', response); + console.log('updatePlantSection payload sent:', queryParams); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [plantSection_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deletePlantSection = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `plant-sub-section/${queryParams}`, + }); + console.log('deletePlantSection full response:', response); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows: null, data: true } + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllPlantSection, getPlantSectionById, createPlantSection, updatePlantSection, deletePlantSection }; diff --git a/src/api/master-shift.jsx b/src/api/master-shift.jsx new file mode 100644 index 0000000..a5a3f24 --- /dev/null +++ b/src/api/master-shift.jsx @@ -0,0 +1,75 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllShift = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `shift?${queryParams.toString()}`, + }); + return response; + } catch (error) { + console.error('getAllShift error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getShiftById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `shift/${id}`, + }); + return response.data; +}; + +const createShift = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `shift`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const updateShift = async (id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `shift/${id}`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const deleteShift = async (id) => { + const response = await SendRequest({ + method: 'delete', + prefix: `shift/${id}`, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +export { getAllShift, getShiftById, createShift, updateShift, deleteShift }; diff --git a/src/api/master-tag.jsx b/src/api/master-tag.jsx new file mode 100644 index 0000000..f6a8eca --- /dev/null +++ b/src/api/master-tag.jsx @@ -0,0 +1,178 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllTag = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `tags?${queryParams.toString()}`, + }); + + // Check if response has error + if (response.error) { + console.error('getAllTag error response:', response); + return { + status: response.statusCode || 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: response.message + }; + } + + // Check if backend returns paginated data + if (response.paging) { + const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0; + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: response.paging.current_page || 1, + limit: response.paging.current_limit || 10, + total: totalData, + page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10)) + }, + total: totalData + } + }; + } + + // Fallback: If backend returns all data without pagination (old behavior) + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllTag catch error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getTagById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `tags/${id}`, + }); + return response.data; +}; + +const createTag = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `tags`, + params: queryParams, + }); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [tag_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateTag = async (tag_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `tags/${tag_id}`, + params: queryParams, + }); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [tag_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteTag = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `tags/${queryParams}`, + }); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows: null, data: true } + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllTag, getTagById, createTag, updateTag, deleteTag }; diff --git a/src/api/master-unit.jsx b/src/api/master-unit.jsx new file mode 100644 index 0000000..b03c91a --- /dev/null +++ b/src/api/master-unit.jsx @@ -0,0 +1,197 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllUnit = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `unit?${queryParams.toString()}`, + }); + console.log('getAllUnit response:', response); + console.log('Query params:', queryParams.toString()); + + // Check if response has error + if (response.error) { + console.error('getAllUnit error response:', response); + return { + status: response.statusCode || 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: response.message + }; + } + + // Check if backend returns paginated data + if (response.paging) { + const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0; + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: response.paging.current_page || 1, + limit: response.paging.current_limit || 10, + total: totalData, + page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10)) + }, + total: totalData + } + }; + } + + // Fallback: If backend returns all data without pagination + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllUnit catch error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getUnitById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `unit/${id}`, + }); + return response.data; +}; + +const createUnit = async (queryParams) => { + // Map frontend fields to backend fields + const backendParams = { + unit_name: queryParams.name, + is_active: queryParams.is_active, + }; + + const response = await SendRequest({ + method: 'post', + prefix: `unit`, + params: backendParams, + }); + console.log('createUnit full response:', response); + console.log('createUnit payload sent:', backendParams); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [unit_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateUnit = async (unit_id, queryParams) => { + // Map frontend fields to backend fields + const backendParams = { + unit_name: queryParams.name, + is_active: queryParams.is_active, + }; + + const response = await SendRequest({ + method: 'put', + prefix: `unit/${unit_id}`, + params: backendParams, + }); + console.log('updateUnit full response:', response); + console.log('updateUnit payload sent:', backendParams); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [unit_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteUnit = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `unit/${queryParams}`, + }); + console.log('deleteUnit full response:', response); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows: null, data: true } + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllUnit, getUnitById, createUnit, updateUnit, deleteUnit }; diff --git a/src/api/role.jsx b/src/api/role.jsx new file mode 100644 index 0000000..5a0b62f --- /dev/null +++ b/src/api/role.jsx @@ -0,0 +1,188 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllRole = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `roles?${queryParams.toString()}`, + }); + + console.log('Role API Response:', response); + + // Check if backend returns paginated data + if (response.paging) { + // Backend already provides pagination info + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: response.paging, + total: response.paging.total || 0 + } + }; + } + + // Fallback: If backend returns all data without pagination + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit), + }, + total: totalData, + }, + }; + } catch (error) { + console.error('getAllRole error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getRoleById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `roles/${id}`, + }); + return response.data; +}; + +const createRole = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `roles`, + params: queryParams, + }); + + console.log('Create Role API Response:', response); + + // Check for error status (not 200, 201, or success) + const isSuccess = + response.statusCode === 200 || response.statusCode === 201 || response.status === 'success'; + + if (!isSuccess && response.statusCode >= 400) { + let errorMessage = response.message || 'Gagal menambahkan role'; + + // Handle SQL unique constraint violation + if ( + errorMessage.includes('UNIQUE KEY constraint') || + errorMessage.includes('duplicate key') + ) { + errorMessage = `Role dengan nama "${queryParams.role_name}" sudah ada. Silakan gunakan nama lain.`; + } + + return { + statusCode: response.statusCode, + data: response.data, + message: errorMessage, + }; + } + + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message || 'Berhasil menambahkan role', + }; +}; + +const updateRole = async (role_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `roles/${role_id}`, + params: queryParams, + }); + + console.log('Update Role API Response:', response); + + // Check for error status (not 200, 201, or success) + const isSuccess = + response.statusCode === 200 || response.statusCode === 201 || response.status === 'success'; + + if (!isSuccess && response.statusCode >= 400) { + let errorMessage = response.message || 'Gagal mengubah role'; + + // Handle SQL unique constraint violation + if ( + errorMessage.includes('UNIQUE KEY constraint') || + errorMessage.includes('duplicate key') + ) { + errorMessage = `Role dengan nama "${queryParams.role_name}" sudah ada. Silakan gunakan nama lain.`; + } + + return { + statusCode: response.statusCode, + data: response.data, + message: errorMessage, + }; + } + + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message || 'Berhasil mengubah role', + }; +}; + +const deleteRole = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `roles/${queryParams}`, + }); + + console.log('Delete API Response:', response); + + // Check for errors + if (response.statusCode !== 200) { + let errorMessage = response.message || 'Gagal menghapus role'; + + // Handle foreign key constraint + if (errorMessage.includes('REFERENCE constraint') || errorMessage.includes('foreign key')) { + errorMessage = 'Role tidak dapat dihapus karena masih digunakan oleh user.'; + } + + return { + statusCode: response.statusCode, + data: response.data, + message: errorMessage, + }; + } + + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + }; +}; + +export { getAllRole, getRoleById, createRole, updateRole, deleteRole }; diff --git a/src/api/user.jsx b/src/api/user.jsx new file mode 100644 index 0000000..208f5ba --- /dev/null +++ b/src/api/user.jsx @@ -0,0 +1,207 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllUser = async (queryParams) => { + try { + console.log('getAllUser queryParams:', queryParams.toString()); + + const response = await SendRequest({ + method: 'get', + prefix: `user?${queryParams.toString()}`, + }); + + console.log('getAllUser response:', response); + + // Backend now handles pagination, just return the response + // Expected backend response structure: + // { + // statusCode: 200, + // data: [...users], + // paging: { page, limit, total, page_total } + // } + + // Check if backend returns paginated data + if (response.paging) { + // Filter out super admin users (is_sa = true) + const allData = response.data || []; + const filteredData = allData.filter(user => user.is_sa !== true && user.is_sa !== 1); + + // Recalculate pagination info after filtering + const totalAfterFilter = filteredData.length; + const currentPage = response.paging.page || 1; + const currentLimit = response.paging.limit || 10; + + return { + status: response.statusCode || 200, + data: { + data: filteredData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalAfterFilter, + page_total: Math.ceil(totalAfterFilter / currentLimit) + }, + total: totalAfterFilter + } + }; + } + + // Fallback: If backend returns all data without pagination (old behavior) + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + + // Filter out users with is_sa = true or 1 (client-side filtering) + const filteredData = allData.filter(user => user.is_sa !== true && user.is_sa !== 1); + const totalData = filteredData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = filteredData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllUser error:', error); + // Return empty data on error to prevent app crash + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getUserById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `user/${id}`, + }); + return response.data; +}; + +const createUser = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `user`, + params: queryParams, + }); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const updateUser = async (user_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `user/${user_id}`, + params: queryParams, + }); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const deleteUser = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `user/${queryParams}`, + }); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const approveUser = async (user_id) => { + const response = await SendRequest({ + method: 'put', + prefix: `user/${user_id}/approve`, + }); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const rejectUser = async (user_id) => { + const response = await SendRequest({ + method: 'put', + prefix: `user/${user_id}/reject`, + }); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const toggleActiveUser = async (user_id, is_active) => { + const response = await SendRequest({ + method: 'put', + prefix: `user/${user_id}`, + params: { + is_active: is_active + }, + }); + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const changePassword = async (user_id, new_password) => { + const response = await SendRequest({ + method: 'put', + prefix: `user/change-password/${user_id}`, + params: { + new_password: new_password + }, + }); + + console.log('Change Password Response:', response); + + // Return full response with statusCode + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message || 'Password berhasil diubah' + }; +}; + +export { getAllUser, getUserById, createUser, updateUser, deleteUser, approveUser, rejectUser, toggleActiveUser, changePassword }; \ No newline at end of file diff --git a/src/assets/bg_cod.jpg b/src/assets/bg_cod.jpg new file mode 100644 index 0000000..fe4e55d Binary files /dev/null and b/src/assets/bg_cod.jpg differ diff --git a/src/assets/sypiu_ggcp.jpg b/src/assets/sypiu_ggcp.jpg deleted file mode 100644 index 5c8fec4..0000000 Binary files a/src/assets/sypiu_ggcp.jpg and /dev/null differ diff --git a/src/components/Global/ApiRequest.jsx b/src/components/Global/ApiRequest.jsx index 8811919..78441e9 100644 --- a/src/components/Global/ApiRequest.jsx +++ b/src/components/Global/ApiRequest.jsx @@ -1,127 +1,170 @@ -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, +const baseURL = import.meta.env.VITE_API_SERVER; + +const instance = axios.create({ + baseURL, + withCredentials: true, +}); + +// axios khusus refresh +const refreshApi = axios.create({ + baseURL, + withCredentials: true, +}); + + +instance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + console.error("🚨 Response Error Interceptor:", { + status: error.response?.status, + url: originalRequest.url, + message: error.response?.data?.message, + hasRetried: originalRequest._retry }); - const isFormData = urlParams.params instanceof FormData; + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; - const request = { - method: urlParams.method, - url: urlParams.prefix ?? '/', - data: urlParams.params, - // yang lama - // headers: { - // 'Content-Type': 'application/json', - // 'Accept-Language': 'en_US', - // }, + try { + console.log("🔄 Refresh token dipanggil..."); + const refreshRes = await refreshApi.post("/auth/refresh-token"); - // yang baru - headers: { - 'Accept-Language': 'en_US', - ...(isFormData ? {} : { 'Content-Type': 'application/json' }), - }, - }; + const newAccessToken = refreshRes.data.data.accessToken; + localStorage.setItem("token", newAccessToken); + console.log("✅ Token refreshed successfully"); - if (urlParams.params === 'doc') { - request.responseType = 'arraybuffer'; - request.headers['Content-Type'] = 'blob'; + // update token di header + instance.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`; + originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; + + console.log("🔁 Retrying original request..."); + return instance(originalRequest); + } catch (refreshError) { + console.error("❌ Refresh token gagal:", refreshError.response?.data || refreshError.message); + localStorage.clear(); + window.location.href = "/signin"; + } } - // console.log(request); + return Promise.reject(error); + } +); - // console.log('prefix', urlParams.prefix); +async function ApiRequest({ + method = "GET", + params = {}, + prefix = "/", + token = true, +} = {}) { + const isFormData = params instanceof FormData; - const tokenRedirect = sessionStorage.getItem('token_redirect'); + const request = { + method, + url: prefix, + data: params, + headers: { + "Accept-Language": "en_US", + ...(isFormData ? {} : { "Content-Type": "application/json" }), + }, + }; - let stringToken = ''; + const rawToken = localStorage.getItem("token"); + if (token && rawToken) { + const cleanToken = rawToken.replace(/"/g, ""); + request.headers["Authorization"] = `Bearer ${cleanToken}`; + console.log("🔐 Sending request with token:", cleanToken.substring(0, 20) + "..."); + } else { + console.warn("⚠️ No token found in localStorage"); + } - if (tokenRedirect !== null) { - stringToken = tokenRedirect; - // console.log(`sessionStorage: ${tokenRedirect}`); - } else { - stringToken = localStorage.getItem('token'); - // console.log(`localStorage: ${stringToken}`); + console.log("📤 API Request:", { method, url: prefix, hasToken: !!rawToken }); + + try { + const response = await instance(request); + console.log("✅ API Response:", { url: prefix, status: response.status, statusCode: response.data?.statusCode }); + return { ...response, error: false }; + } catch (error) { + const status = error?.response?.status || 500; + const message = error?.response?.data?.message || error.message || "Something Wrong"; + console.error("❌ API Error:", { + url: prefix, + status, + message, + fullError: error?.response?.data + }); + + if (status !== 401) { + await cekError(status, message); } - 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; - }); + return { ...error.response, error: true }; + } } -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) => {}); - } +async function cekError(status, message = "") { + if (status === 403) { + await Swal.fire({ + icon: "warning", + title: "Forbidden", + text: message, + }); + } else if (status >= 500) { + await Swal.fire({ + icon: "error", + title: "Server Error", + text: message, + }); + } else { + await Swal.fire({ + icon: "warning", + title: "Peringatan", + text: message, + }); + } } 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); + console.log("📦 SendRequest response:", { + hasError: response.error, + status: response.status, + statusCode: response.data?.statusCode, + data: response.data + }); + + // If ApiRequest returned error flag, return error structure + if (response.error) { + const errorMsg = response.data?.message || response.statusText || "Request failed"; + console.error("❌ SendRequest error response:", errorMsg); + + // Return consistent error structure instead of empty array + return { + statusCode: response.status || 500, + message: errorMsg, + data: null, + error: true + }; } + + return response?.data || { statusCode: 200, data: [], message: "Success" }; + } catch (error) { + console.error("❌ SendRequest catch error:", error); + + // Don't show Swal here, let the calling code handle it + // This allows better error handling in each API call + return { + statusCode: 500, + message: error.message || "Something went wrong", + data: null, + error: true + }; + } }; export { ApiRequest, SendRequest }; diff --git a/src/components/Global/CardList.jsx b/src/components/Global/CardList.jsx new file mode 100644 index 0000000..624bca8 --- /dev/null +++ b/src/components/Global/CardList.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Card, Button, Row, Col, Typography, Space, Tag } from 'antd'; +import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; + +const { Text } = Typography; + +const CardList = ({ + data, + column, + header, + showPreviewModal, + showEditModal, + showDeleteDialog, + cardColor, +}) => { + const getCardStyle = () => { + const color = cardColor ?? '#F3EDEA'; // Orange color + return { + border: `2px solid ${color}`, + borderRadius: '8px', + textAlign: 'center', // Center text + }; + }; + + const getTitleStyle = (color) => { + const backgroundColor = color ?? '#FCF2ED'; + return { + backgroundColor, + color: '#fff', + padding: '2px 8px', + borderRadius: '4px', + display: 'inline-block', // ganti inline-block → block + width: 'fit-content', // biar lebarnya tetap menyesuaikan teks + }; + }; + + return ( + + {data.map((item) => ( + + + + {item[header]} + + + } + style={getCardStyle()} + actions={[ + + - - - ))} - - + }, + { value: 'table', icon: }, + ]} + value={viewMode} + onChange={setViewMode} + /> + {(isMobile && mobile) || viewMode === 'card' ? ( + ) : ( - {/* TABLE */} ({ ...item, key: index }))} pagination={false} loading={gridLoading} - scroll={{ - y: 520, - x: 1300, - }} + scroll={{ y: 520 }} /> - - {/* PAGINATION */} - - - -
- Menampilkan {pagingResponse.totalData} Data dari{' '} - {pagingResponse.perPage} Halaman -
- - - - - - )} + {/* PAGINATION */} + + +
+ Menampilkan {pagingResponse.totalPage} Data dari {pagingResponse.perPage}{' '} + Halaman +
+ + + + + ); }); 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); diff --git a/src/index.css b/src/index.css index 30d122f..875f836 100644 --- a/src/index.css +++ b/src/index.css @@ -18,4 +18,38 @@ html body { margin: 0; height: 100vh; +} + +/* Custom Orange Sidebar Menu Styles */ +.custom-orange-menu.ant-menu-dark .ant-menu-item-selected { + background-color: rgba(255, 255, 255, 0.2) !important; + color: white !important; +} + +.custom-orange-menu.ant-menu-dark .ant-menu-item-selected::after { + border-right-color: white !important; +} + +.custom-orange-menu.ant-menu-dark .ant-menu-item:hover, +.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title:hover { + background-color: rgba(255, 255, 255, 0.15) !important; + color: white !important; +} + +.custom-orange-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title { + color: white !important; +} + +.custom-orange-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub { + background: rgba(0, 0, 0, 0.2) !important; +} + +.custom-orange-menu.ant-menu-dark .ant-menu-item, +.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title { + color: rgba(255, 255, 255, 0.9) !important; +} + +.custom-orange-menu.ant-menu-dark .ant-menu-item-active, +.custom-orange-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title { + color: white !important; } \ No newline at end of file diff --git a/src/layout/LayoutHeader.jsx b/src/layout/LayoutHeader.jsx index 21a7717..0cb0a8a 100644 --- a/src/layout/LayoutHeader.jsx +++ b/src/layout/LayoutHeader.jsx @@ -1,5 +1,5 @@ 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'; @@ -12,18 +12,52 @@ const { Header } = Layout; const LayoutHeader = () => { const { breadcrumbItems } = useBreadcrumb(); const navigate = useNavigate(); - const { - token: { colorBgContainer, colorBorder, colorText }, - } = theme.useToken(); - // Ambil data user dari localStorage dan dekripsi + // 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 + let userData = null; + const sessionData = localStorage.getItem('session'); - const userData = sessionData ? decryptData(sessionData) : null; - // console.log(userData); + 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); + } + } + } - const roleName = userData?.user?.approval || userData?.user?.partner_name || 'Guest'; + // console.log('User data di header:', userData?.user); - const userName = userData?.user?.name || userData?.user?.username || '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 ( <> @@ -35,11 +69,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 +122,7 @@ const LayoutHeader = () => { { - handleLogOut(); - navigate('/signin'); + handleLogOut(navigate); }} aria-label="Log out from the application" style={{ diff --git a/src/layout/LayoutLogo.jsx b/src/layout/LayoutLogo.jsx index 086db57..9ecdcc0 100644 --- a/src/layout/LayoutLogo.jsx +++ b/src/layout/LayoutLogo.jsx @@ -10,14 +10,14 @@ const LayoutLogo = () => { display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: '#001529', padding: '1rem', borderRadius: '1rem', }} >
{ alignItems: 'center', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.2)', position: 'relative', - overflow: 'hidden' + overflow: 'hidden', }} > {/* Ring sebelum logo utama */} @@ -37,7 +37,7 @@ const LayoutLogo = () => { width: 160, height: 160, position: 'absolute', - zIndex: 1 + zIndex: 1, }} /> @@ -66,4 +66,4 @@ const LayoutLogo = () => { ); }; -export default LayoutLogo; \ No newline at end of file +export default LayoutLogo; diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx index 33fb21f..6e0bc57 100644 --- a/src/layout/LayoutMenu.jsx +++ b/src/layout/LayoutMenu.jsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { Link, useLocation } from 'react-router-dom'; import { Menu, Typography, Image } from 'antd'; import { getSessionData } from '../components/Global/Formatter'; -import { HomeOutlined, +import { + HomeOutlined, DatabaseOutlined, SettingOutlined, UserOutlined, @@ -12,7 +13,19 @@ import { HomeOutlined, HistoryOutlined, DollarOutlined, RollbackOutlined, - ProductOutlined + ProductOutlined, + TagOutlined, + AppstoreOutlined, + MobileOutlined, + WarningOutlined, + LineChartOutlined, + FileTextOutlined, + BellOutlined, + AlertOutlined, + SafetyOutlined, + TeamOutlined, + ClockCircleOutlined, + CalendarOutlined, } from '@ant-design/icons'; const { Text } = Typography; @@ -20,30 +33,185 @@ const { Text } = Typography; const allItems = [ { key: 'home', - icon: , - label: Home, + icon: , + label: ( + + Home + + ), }, { key: 'master', - icon: , + icon: , label: 'Master', children: [ + { + key: 'master-plant-section', + icon: , + label: Plant Section, + }, + { + key: 'master-brand-device', + icon: , + label: Brand Device, + }, { key: 'master-device', - icon: , + icon: , label: Device, }, + { + key: 'master-tag', + icon: , + label: Tag, + }, + { + key: 'master-unit', + icon: , + label: Unit, + }, + { + key: 'master-status', + icon: , + label: Status, + }, + { + key: 'master-shift', + icon: , + label: Shift, + }, ], }, + { + key: 'history', + icon: , + label: 'History', + children: [ + { + key: 'history-trending', + icon: , + label: Trending, + }, + { + key: 'history-report', + icon: , + label: Report, + }, + ], + }, + { + key: 'notification', + icon: , + label: ( + + Notifikasi + + ), + }, + { + key: 'event-alarm', + icon: , + label: ( + + Event Alarm + + ), + }, + { + key: 'role', + icon: , + label: ( + + Role + + ), + }, + { + key: 'user', + icon: , + label: ( + + User + + ), + }, + { + key: 'jadwal-shift', + icon: , + label: ( + + Jadwal Shift + + ), + }, ]; const LayoutMenu = () => { + const location = useLocation(); const [stateOpenKeys, setStateOpenKeys] = useState(['home']); + const [selectedKeys, setSelectedKeys] = useState(['home']); - const getLevelKeys = items1 => { + // Function to get menu key from current path + const getMenuKeyFromPath = (pathname) => { + // Remove leading slash and split path + const pathParts = pathname.replace(/^\//, '').split('/'); + + // Handle different route patterns + if (pathname === '/dashboard/home') return 'home'; + if (pathname === '/user') return 'user'; + if (pathname === '/role') return 'role'; + if (pathname === '/notification') return 'notification'; + if (pathname === '/event-alarm') return 'event-alarm'; + if (pathname === '/jadwal-shift') return 'jadwal-shift'; + + // Handle master routes + if (pathname.startsWith('/master/')) { + const subPath = pathParts[1]; + return `master-${subPath}`; + } + + // Handle history routes + if (pathname.startsWith('/history/')) { + const subPath = pathParts[1]; + return `history-${subPath}`; + } + + // Handle shift management routes + if (pathname.startsWith('/shift-management/')) { + const subPath = pathParts[1]; + return `shift-${subPath}`; + } + + return 'home'; // default + }; + + // Function to get parent key from menu key + const getParentKey = (key) => { + if (key.startsWith('master-')) return 'master'; + if (key.startsWith('history-')) return 'history'; + if (key.startsWith('shift-')) return 'shift-management'; + return null; + }; + + // Update selected and open keys when route changes + useEffect(() => { + const currentKey = getMenuKeyFromPath(location.pathname); + setSelectedKeys([currentKey]); + + const parentKey = getParentKey(currentKey); + + // If current menu has parent, open it. Otherwise, close all dropdowns + if (parentKey) { + setStateOpenKeys([parentKey]); + } else { + setStateOpenKeys([]); + } + }, [location.pathname]); + + const getLevelKeys = (items1) => { const key = {}; const func = (items2, level = 1) => { - items2.forEach(item => { + items2.forEach((item) => { if (item.key) { key[item.key] = level; } @@ -58,12 +226,16 @@ const LayoutMenu = () => { const levelKeys = getLevelKeys(allItems); - const onOpenChange = openKeys => { - const currentOpenKey = openKeys.find(key => stateOpenKeys.indexOf(key) === -1); + const onOpenChange = (openKeys) => { + const currentOpenKey = openKeys.find((key) => stateOpenKeys.indexOf(key) === -1); if (currentOpenKey !== undefined) { - const repeatIndex = openKeys.filter(key => key !== currentOpenKey).findIndex(key => levelKeys[key] === levelKeys[currentOpenKey]); + const repeatIndex = openKeys + .filter((key) => key !== currentOpenKey) + .findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]); setStateOpenKeys( - openKeys.filter((_, index) => index !== repeatIndex).filter(key => levelKeys[key] <= levelKeys[currentOpenKey]), + openKeys + .filter((_, index) => index !== repeatIndex) + .filter((key) => levelKeys[key] <= levelKeys[currentOpenKey]) ); } else { setStateOpenKeys(openKeys); @@ -73,38 +245,46 @@ const LayoutMenu = () => { const session = getSessionData(); const isAdmin = session?.user?.user_id; - const karyawan = ()=>{ - return allItems.filter( - item => item.key !== 'setting' - // tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan - // && item.key !== 'master' - // && item.key !== 'master' - ).map(item=>{ - if(item.key === 'master'){ - return{ - ...item, - // buka command dibawah jika terdapat sub menu yang di sembunyikan - // children: item.children.filter( - // child => child.key !== 'master-product' - // tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan - // && child.key !== 'master-service' - // ) + const karyawan = () => { + return allItems + .filter( + (item) => item.key !== 'setting' + // tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan + // && item.key !== 'master' + // && item.key !== 'master' + ) + .map((item) => { + if (item.key === 'master') { + return { + ...item, + // buka command dibawah jika terdapat sub menu yang di sembunyikan + // children: item.children.filter( + // child => child.key !== 'master-product' + // tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan + // && child.key !== 'master-service' + // ) + }; } - } - return item; - }); + return item; + }); }; const items = isAdmin === 1 ? allItems : karyawan(); return ( ); }; -export default LayoutMenu; \ No newline at end of file +export default LayoutMenu; diff --git a/src/layout/LayoutSidebar.jsx b/src/layout/LayoutSidebar.jsx index 4589344..e5fc25c 100644 --- a/src/layout/LayoutSidebar.jsx +++ b/src/layout/LayoutSidebar.jsx @@ -1,4 +1,4 @@ -import React from 'react' +import React from 'react'; import { Layout } from 'antd'; import LayoutLogo from './LayoutLogo'; import LayoutMenu from './LayoutMenu'; @@ -6,7 +6,8 @@ import LayoutMenu from './LayoutMenu'; const { Sider } = Layout; const LayoutSidebar = () => { return ( - { @@ -15,11 +16,20 @@ const LayoutSidebar = () => { onCollapse={(collapsed, type) => { // console.log(collapsed, type); }} + style={{ + background: 'linear-gradient(180deg, #FF8C42 0%, #FF6B35 100%)', + overflow: 'auto', + height: '100vh', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + }} > - ) -} + ); +}; -export default LayoutSidebar +export default LayoutSidebar; diff --git a/src/layout/MainLayout.jsx b/src/layout/MainLayout.jsx index 6d035f9..f752177 100644 --- a/src/layout/MainLayout.jsx +++ b/src/layout/MainLayout.jsx @@ -1,33 +1,38 @@ -import React from 'react'; -import { Layout, theme } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { Layout, theme, Grid } from 'antd'; import LayoutFooter from './LayoutFooter'; import LayoutHeader from './LayoutHeader'; import LayoutSidebar from './LayoutSidebar'; - const { Content } = Layout; +const { useBreakpoint } = Grid; const MainLayout = ({ children }) => { - const { - token: { colorBgContainer, borderRadiusLG }, - } = theme.useToken(); + const { + token: { colorBgContainer, borderRadiusLG }, + } = theme.useToken(); - return ( - - - - - - {/*
+ + + + + {/*
{ borderRadius: borderRadiusLG, }} > */} - {children} - {/*
*/} -
- {/* */} -
- - ); + {children} + {/*
*/} +
+ {/* */} +
+
+ ); }; -export default MainLayout; \ No newline at end of file +export default MainLayout; diff --git a/src/main.jsx b/src/main.jsx index a76e718..d5321c1 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' +import './App.css' import { BreadcrumbProvider } from './layout/LayoutBreadcrumb.jsx'; ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/src/pages/auth/Registration.jsx b/src/pages/auth/Registration.jsx 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..f63ea0d 100644 --- a/src/pages/auth/SignIn.jsx +++ b/src/pages/auth/SignIn.jsx @@ -1,186 +1,179 @@ +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 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'; +import { SendRequest } from '../../components/Global/ApiRequest'; +import bg_cod from 'assets/bg_cod.jpg'; +import logo from 'assets/freepik/LOGOPIU.png'; const SignIn = () => { - const [captchaSvg, setCaptchaSvg] = React.useState(''); - const [userInput, setUserInput] = React.useState(''); - const [message, setMessage] = React.useState(''); - const [captchaText, setcaptchaText] = React.useState(''); - + const [form] = Form.useForm(); + const [captchaSvg, setCaptchaSvg] = useState(''); + const [captchaText, setCaptchaText] = useState(''); + const [loading, setLoading] = useState(false); const navigate = useNavigate(); - // let url = `${import.meta.env.VITE_API_SERVER}/users`; - React.useEffect(() => { + const defaultSignIn = { + identifier: 'superadmin@cod.com', + password: '@Superadmin123', + captcha: '', + }; + + const moveToSignUp = () => { + navigate('/signup'); + }; + + // 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(); }, []); - // Fetch the CAPTCHA SVG from the backend - const fetchCaptcha = async () => { + const handleOnSubmit = async (values) => { + setLoading(true); 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 - } - ); + const res = await SendRequest({ + method: 'post', + prefix: 'auth/login', + params: { + identifier: values.identifier, + password: values.password, + captcha: values.captcha, + captchaText: captchaText, + }, + withCredentials: true, + }); - // Ambil header - const captchaToken = response.headers.get('X-Captcha-Token'); + const user = res?.data?.user || res?.user; + const accessToken = res?.data?.accessToken || res?.tokens?.accessToken; - // console.log('Captcha Token:', decryptData(captchaToken)); + if (user && accessToken) { + localStorage.setItem('token', accessToken); + localStorage.setItem('user', JSON.stringify(user)); - setcaptchaText(decryptData(captchaToken)); + NotifAlert({ + icon: 'success', + title: 'Login Berhasil', + message: res?.message || 'Selamat datang!', + }); - 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); + navigate('/dashboard/home'); + } + } catch (err) { + // hanya handle invalid captcha disini + if (err?.response?.data?.message?.toLowerCase().includes('captcha')) { + NotifAlert({ + icon: 'warning', + title: 'Peringatan', + message: 'Invalid captcha', + }); + fetchCaptcha(); } 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.', + title: 'Login Gagal', + message: err?.message || 'Terjadi kesalahan', }); + fetchCaptcha(); } - } catch (error) { - console.error('Error verifying CAPTCHA:', error); - setMessage('An error occurred. Please try again.'); + } finally { + setLoading(false); } - - setUserInput(''); // Clear the input field - }; - - const moveToRegistration = (e) => { - // e.preventDefault(); - // navigate("/signup") - navigate('/registration'); }; return ( - <> - - + + + + +
+
- - - -
- - - - - - - - -
- {/* {message} */} - setUserInput(e.target.value)} - /> - - - - - - - - - - - + + + + + + + +
+ + + + + + + + + + + + + + ); }; diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx index 8bd1d14..9b7a211 100644 --- a/src/pages/auth/Signup.jsx +++ b/src/pages/auth/Signup.jsx @@ -1,158 +1,201 @@ -import { Flex, Input, Form, Button, Card, Space, Image } from 'antd' -import React from 'react' -// import handleSignIn from '../../Utils/Auth/SignIn'; -import sypiu_ggcp from 'assets/sypiu_ggcp.jpg'; -import {useNavigate} from "react-router-dom"; +import React, { useState } from 'react'; +import { Flex, Input, Form, Button, Card, Space, Image, Row, Col } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import bg_cod from 'assets/bg_cod.jpg'; +import logo from 'assets/freepik/LOGOPIU.png'; +import { register } from '../../api/auth'; +import { NotifOk, NotifAlert } from '../../components/Global/ToastNotif'; const SignUp = () => { + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const navigate = useNavigate(); - const [captchaSvg, setCaptchaSvg] = React.useState(''); - const [userInput, setUserInput] = React.useState(''); - const [message, setMessage] = React.useState(''); + const [isRegistered, setIsRegistered] = useState(false); - const navigate = useNavigate(); - // let url = `${import.meta.env.VITE_API_SERVER}/users`; + const moveToSignin = () => { + navigate('/signin'); + }; - React.useEffect(() => { - fetchCaptcha(); - }, []); + const handleSignUp = async (values) => { + const { user_fullname, user_name, user_email, user_phone, user_password, confirmPassword } = + values; - // Fetch the CAPTCHA SVG from the backend - const fetchCaptcha = async () => { - try { - const response = await fetch('http://localhost:9528/generate-captcha'); - const data = await response.text(); - setCaptchaSvg(data); - } catch (error) { - console.error('Error fetching CAPTCHA:', error); - } - }; - - const handleOnSubmt = async (e) => { - // console.log('Received values of form: ', e); - // await handleSignIn(e); - e.preventDefault(); - - try { - const response = await fetch('http://localhost:5000/verify-captcha', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userInput }), - }); - - const data = await response.json(); - if (data.success) { - setMessage('CAPTCHA verified successfully!'); - } else { - setMessage('CAPTCHA verification failed. Try again.'); - fetchCaptcha(); // Refresh CAPTCHA on failure + // Validasi confirm password + if (user_password !== confirmPassword) { + NotifAlert({ + icon: 'error', + title: 'Password Tidak Sama', + message: 'Password dan confirm password harus sama', + }); + form.resetFields(['password', 'confirmPassword']); + return; } - } 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") - } + // Validasi nomor telepon Indonesia + const phoneRegex = /^(?:\+62|62|0)8[1-9][0-9]{6,11}$/; + if (!phoneRegex.test(user_phone)) { + NotifAlert({ + icon: 'error', + title: 'Format Telepon Salah', + message: 'Nomor telepon tidak valid (harus nomor Indonesia)', + }); + form.resetFields(['user_phone']); + return; + } - return ( - <> + // Validasi password kompleks + const passwordErrors = []; + if (user_password.length < 8) passwordErrors.push('Minimal 8 karakter'); + if (!/[A-Z]/.test(user_password)) passwordErrors.push('Harus ada huruf kapital'); + if (!/[0-9]/.test(user_password)) passwordErrors.push('Harus ada angka'); + if (!/[!@#$%^&*(),.?":{}|<>]/.test(user_password)) + passwordErrors.push('Harus ada karakter spesial'); + if (passwordErrors.length) { + NotifAlert({ + icon: 'error', + title: 'Password Tidak Valid', + message: passwordErrors.join(', '), + }); + form.resetFields(['user_password', 'confirmPassword']); + return; + } - - - - - -
-
- - - - + - - - -
+

Registration

- - - + + +
+ + + + + + + + + + - {/* setUserInput(e.target.value)} - /> */} + + + + + + + + + + + + - - - + + + + + - - + + + ); +}; - - - - - - ) -} - -export default SignUp +export default SignUp; diff --git a/src/pages/eventAlarm/IndexEventAlarm.jsx b/src/pages/eventAlarm/IndexEventAlarm.jsx new file mode 100644 index 0000000..c33939a --- /dev/null +++ b/src/pages/eventAlarm/IndexEventAlarm.jsx @@ -0,0 +1,72 @@ +import React, { memo, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useBreadcrumb } from '../../layout/LayoutBreadcrumb'; +import { Form, Typography } from 'antd'; +import ListEventAlarm from './component/ListEventAlarm'; +import DetailEventAlarm from './component/DetailEventAlarm'; + +const { Text } = Typography; + +const IndexEventAlarm = memo(function IndexEventAlarm() { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + const [form] = Form.useForm(); + + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + const [isModalVisible, setIsModalVisible] = useState(false); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setBreadcrumbItems([ + { + title: ( + + • Event Alarm + + ), + }, + ]); + } else { + navigate('/signin'); + } + }, [navigate, setBreadcrumbItems]); + + useEffect(() => { + if (actionMode === 'preview') { + setIsModalVisible(true); + if (selectedData) { + form.setFieldsValue(selectedData); + } + } else { + setIsModalVisible(false); + form.resetFields(); + } + }, [actionMode, selectedData, form]); + + const handleCancel = () => { + setActionMode('list'); + setSelectedData(null); + form.resetFields(); + }; + + return ( + + + + + ); +}); + +export default IndexEventAlarm; diff --git a/src/pages/eventAlarm/component/DetailEventAlarm.jsx b/src/pages/eventAlarm/component/DetailEventAlarm.jsx new file mode 100644 index 0000000..0a0b487 --- /dev/null +++ b/src/pages/eventAlarm/component/DetailEventAlarm.jsx @@ -0,0 +1,58 @@ +import { memo } from 'react'; +import { Modal, Divider, Descriptions } from 'antd'; + +const DetailEventAlarm = memo(function DetailEventAlarm({ visible, onCancel, selectedData }) { + return ( + + {selectedData && ( +
+ + + {selectedData.tanggal} + + + {selectedData.plant_sub_section} + + + {selectedData.device} + + + {selectedData.tag} + + + {selectedData.engineer} + + + + + + {/* Additional Info */} +
+
+ Catatan: Event alarm ini telah tercatat dalam sistem untuk + monitoring dan analisis lebih lanjut. +
+
+
+ )} +
+ ); +}); + +export default DetailEventAlarm; diff --git a/src/pages/eventAlarm/component/ListEventAlarm.jsx b/src/pages/eventAlarm/component/ListEventAlarm.jsx new file mode 100644 index 0000000..d313c3e --- /dev/null +++ b/src/pages/eventAlarm/component/ListEventAlarm.jsx @@ -0,0 +1,246 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Button, Row, Col, Card, Input } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import TableList from '../../../components/Global/TableList'; + +// Dummy data untuk riwayat alarm +const initialAlarmsData = [ + { + alarm_id: 1, + tanggal: '2025-01-15 08:30:00', + plant_sub_section: 'Plant A - Section 1', + device: 'Device 001', + tag: 'TEMP-001', + engineer: 'Pras', + }, + { + alarm_id: 2, + tanggal: '2025-01-15 09:15:00', + plant_sub_section: 'Plant B - Section 2', + device: 'Device 002', + tag: 'PRESS-002', + engineer: 'Bagus', + }, + { + alarm_id: 3, + tanggal: '2025-01-15 10:00:00', + plant_sub_section: 'Plant A - Section 3', + device: 'Device 003', + tag: 'FLOW-003', + engineer: 'iqbal', + }, + { + alarm_id: 4, + tanggal: '2025-01-15 11:45:00', + plant_sub_section: 'Plant C - Section 1', + device: 'Device 004', + tag: 'LEVEL-004', + engineer: 'riski', + }, + { + alarm_id: 5, + tanggal: '2025-01-15 13:20:00', + plant_sub_section: 'Plant B - Section 3', + device: 'Device 005', + tag: 'TEMP-005', + engineer: 'anton', + }, + { + alarm_id: 6, + tanggal: '2025-01-15 14:00:00', + plant_sub_section: 'Plant A - Section 2', + device: 'Device 006', + tag: 'PRESS-006', + engineer: 'kurniawan', + }, + { + alarm_id: 7, + tanggal: '2025-01-15 15:30:00', + plant_sub_section: 'Plant C - Section 2', + device: 'Device 007', + tag: 'FLOW-007', + engineer: 'wawan', + }, +]; + +const ListEventAlarm = memo(function ListEventAlarm(props) { + const columns = [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Tanggal', + dataIndex: 'tanggal', + key: 'tanggal', + width: '15%', + }, + { + title: 'Plant Sub Section', + dataIndex: 'plant_sub_section', + key: 'plant_sub_section', + width: '25%', + }, + { + title: 'Device', + dataIndex: 'device', + key: 'device', + width: '15%', + }, + { + title: 'Tag', + dataIndex: 'tag', + key: 'tag', + width: '15%', + }, + { + title: 'Engineer', + dataIndex: 'engineer', + key: 'engineer', + width: '15%', + }, + ]; + const [trigerFilter, setTrigerFilter] = useState(false); + const [alarmsData] = useState(initialAlarmsData); + + const defaultFilter = { search: '' }; + const [formDataFilter, setFormDataFilter] = useState(defaultFilter); + const [searchValue, setSearchValue] = useState(''); + + const navigate = useNavigate(); + + // Dummy data function to simulate API call + const getAllEventAlarm = async (params) => { + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Extract URLSearchParams + const searchParam = params.get('search') || ''; + const page = parseInt(params.get('page')) || 1; + const limit = parseInt(params.get('limit')) || 10; + + console.log('getAllEventAlarm called with:', { searchParam, page, limit }); + + // Filter by search + let filteredAlarms = alarmsData; + if (searchParam) { + const searchLower = searchParam.toLowerCase(); + filteredAlarms = alarmsData.filter( + (alarm) => + alarm.tanggal.toLowerCase().includes(searchLower) || + alarm.plant_sub_section.toLowerCase().includes(searchLower) || + alarm.device.toLowerCase().includes(searchLower) || + alarm.tag.toLowerCase().includes(searchLower) || + alarm.engineer.toLowerCase().includes(searchLower) + ); + } + + // Pagination logic + const totalData = filteredAlarms.length; + const totalPages = Math.ceil(totalData / limit); + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedData = filteredAlarms.slice(startIndex, endIndex); + + return { + status: 200, + statusCode: 200, + data: { + data: paginatedData, + total: totalData, + paging: { + page: page, + limit: limit, + total: totalData, + page_total: totalPages, + }, + }, + }; + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + if (props.actionMode == 'list') { + setFormDataFilter(defaultFilter); + doFilter(); + } + } else { + navigate('/signin'); + } + }, [props.actionMode, alarmsData]); + + const doFilter = () => { + setTrigerFilter((prev) => !prev); + }; + + const handleSearch = () => { + setFormDataFilter({ search: searchValue }); + setTrigerFilter((prev) => !prev); + }; + + const handleSearchClear = () => { + setSearchValue(''); + setFormDataFilter({ search: '' }); + setTrigerFilter((prev) => !prev); + }; + + return ( + + + + + + + { + const value = e.target.value; + setSearchValue(value); + // Auto search when clearing by backspace/delete + if (value === '') { + setFormDataFilter({ search: '' }); + setTrigerFilter((prev) => !prev); + } + }} + onSearch={handleSearch} + allowClear={{ + clearIcon: , + }} + enterButton={ + + } + size="large" + /> + + + + + + + + + + ); +}); + +export default ListEventAlarm; diff --git a/src/pages/history/report/IndexReport.jsx b/src/pages/history/report/IndexReport.jsx new file mode 100644 index 0000000..00a4149 --- /dev/null +++ b/src/pages/history/report/IndexReport.jsx @@ -0,0 +1,275 @@ +import React, { memo, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; +import { Typography, Table, Card, Select, DatePicker, Button, Row, Col } from 'antd'; +import { FileTextOutlined } from '@ant-design/icons'; +import { decryptData } from '../../../components/Global/Formatter'; +import dayjs from 'dayjs'; + +const { Text } = Typography; + +// New data structure for tag history +const tagHistoryData = [ + { + tag: 'TEMP_SENSOR_1', + color: '#FF6B4A', + history: [ + { timestamp: '2025-10-09 08:00', value: 75 }, + { timestamp: '2025-10-09 08:05', value: 76 }, + { timestamp: '2025-10-09 08:10', value: 75 }, + ], + }, + { + tag: 'GAS_LEAK_SENSOR_1', + color: '#4ECDC4', + history: [ + { timestamp: '2025-10-09 08:00', value: 10 }, + { timestamp: '2025-10-09 08:05', value: 150 }, + { timestamp: '2025-10-09 08:10', value: 12 }, + ], + }, + { + tag: 'PRESSURE_SENSOR_1', + color: '#FFE66D', + history: [ + { timestamp: '2025-10-09 08:00', value: 1.2 }, + { timestamp: '2025-10-09 08:05', value: 1.3 }, + { timestamp: '2025-10-09 08:10', value: 1.2 }, + ], + }, +]; + +const IndexReport = memo(function IndexReport() { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + + const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); + const [startDate, setStartDate] = useState(dayjs('2025-09-30')); + const [endDate, setEndDate] = useState(dayjs('2025-10-09')); + const [periode, setPeriode] = useState('30 Menit'); + const [userRole, setUserRole] = useState(null); + const [roleLevel, setRoleLevel] = useState(null); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + // Get user data and role + let userData = null; + const sessionData = localStorage.getItem('session'); + if (sessionData) { + userData = decryptData(sessionData); + } else { + const userRaw = localStorage.getItem('user'); + if (userRaw) { + try { + userData = { user: JSON.parse(userRaw) }; + } catch (e) { + console.error('Error parsing user data:', e); + } + } + } + + if (userData?.user) { + setUserRole(userData.user.role_name); + setRoleLevel(userData.user.role_level); + } + + setBreadcrumbItems([ + { + title: ( + + • History + + ), + }, + { + title: ( + + Report + + ), + }, + ]); + } else { + navigate('/signin'); + } + }, []); + + const handleReset = () => { + setPlantSubSection('Semua Plant'); + setStartDate(dayjs('2025-09-30')); + setEndDate(dayjs('2025-10-09')); + setPeriode('30 Menit'); + }; + + // Check if user has permission to view data (all except guest) + const canViewData = userRole && userRole !== 'guest'; + + // Convert tag history data to table format + const convertToTableData = () => { + const timestamps = {}; // Use an object to collect data per timestamp + + tagHistoryData.forEach((tagData) => { + tagData.history.forEach((point) => { + if (!timestamps[point.timestamp]) { + timestamps[point.timestamp] = { + key: point.timestamp, + 'Date and Time': point.timestamp, + }; + } + timestamps[point.timestamp][tagData.tag] = point.value; + }); + }); + + // Convert the object to an array + return Object.values(timestamps); + }; + + const tableData = convertToTableData(); + + // Create dynamic columns based on tags + const tags = tagHistoryData.map((tagData) => tagData.tag); + + const columns = [ + { + title: 'Date and Time', + dataIndex: 'Date and Time', + key: 'Date and Time', + fixed: 'left', + width: 180, + render: (text) => {text}, + }, + ...tags.map((tag) => ({ + title: tag, + dataIndex: tag, + key: tag, + align: 'center', + width: 150, + render: (value) => {value !== undefined ? value : '-'}, + })), + ]; + + return ( + +
+ {/* Filter Section */} + +
+ + ☰ Filter Data + +
+ +
+
+ + Plant Sub Section + + +
+ + + + + + + + + + + + {/* Table Section */} + {/* {!canViewData ? ( + + + Anda tidak memiliki akses untuk melihat data report. +
+ Silakan hubungi administrator untuk mendapatkan akses. +
+
+ ) : ( */} + +
+ + ☰ History Report + +
+
+ + {/* )} */} + + + ); +}); + +export default IndexReport; diff --git a/src/pages/history/trending/IndexTrending.jsx b/src/pages/history/trending/IndexTrending.jsx new file mode 100644 index 0000000..83e1cb0 --- /dev/null +++ b/src/pages/history/trending/IndexTrending.jsx @@ -0,0 +1,297 @@ +import React, { memo, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; +import { Typography, Select, DatePicker, Button, Row, Col, Card } from 'antd'; +import { ResponsiveLine } from '@nivo/line'; +import { FileTextOutlined } from '@ant-design/icons'; +import { decryptData } from '../../../components/Global/Formatter'; +import dayjs from 'dayjs'; +import './trending.css'; + +const { Text } = Typography; + +const IndexTrending = memo(function IndexTrending() { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + + const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); + const [startDate, setStartDate] = useState(dayjs('2025-09-30')); + const [endDate, setEndDate] = useState(dayjs('2025-10-09')); + const [periode, setPeriode] = useState('10 Menit'); + const [userRole, setUserRole] = useState(null); + const [roleLevel, setRoleLevel] = useState(null); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + // Get user data and role + let userData = null; + const sessionData = localStorage.getItem('session'); + if (sessionData) { + userData = decryptData(sessionData); + } else { + const userRaw = localStorage.getItem('user'); + if (userRaw) { + try { + userData = { user: JSON.parse(userRaw) }; + } catch (e) { + console.error('Error parsing user data:', e); + } + } + } + + if (userData?.user) { + setUserRole(userData.user.role_name); + setRoleLevel(userData.user.role_level); + } + + setBreadcrumbItems([ + { + title: ( + + • History + + ), + }, + { + title: ( + + Trending + + ), + }, + ]); + } else { + navigate('/signin'); + } + }, []); + + const tagTrendingData = [ + { + id: 'TEMP_SENSOR_1', + color: '#FF6B4A', + data: [ + { y: '08:00', x: 75 }, + { y: '08:05', x: 76 }, + { y: '08:10', x: 75 }, + { y: '08:15', x: 77 }, + { y: '08:20', x: 76 }, + { y: '08:25', x: 78 }, + { y: '08:30', x: 79 }, + ], + }, + { + id: 'GAS_LEAK_SENSOR_1', + color: '#4ECDC4', + data: [ + { y: '08:00', x: 10 }, + { y: '08:05', x: 150 }, + { y: '08:10', x: 40 }, + { y: '08:15', x: 20 }, + { y: '08:20', x: 15 }, + { y: '08:25', x: 18 }, + { y: '08:30', x: 25 }, + ], + }, + { + id: 'PRESSURE_SENSOR_1', + color: '#FFE66D', + data: [ + { y: '08:00', x: 1.2 }, + { y: '08:05', x: 1.3 }, + { y: '08:10', x: 1.2 }, + { y: '08:15', x: 1.4 }, + { y: '08:20', x: 1.5 }, + { y: '08:25', x: 1.3 }, + { y: '08:30', x: 1.2 }, + ], + }, + ]; + + const handleReset = () => { + setPlantSubSection('Semua Plant'); + setStartDate(dayjs('2025-09-30')); + setEndDate(dayjs('2025-10-09')); + setPeriode('10 Menit'); + }; + + // Check if user has permission to view data (all except guest) + const canViewData = userRole && userRole !== 'guest'; + + // Check if user can export/filter (administrator, engineer) + const canExportData = userRole && (userRole === 'administrator' || userRole === 'engineer'); + + return ( + + {/* Filter Section */} + +
+ + ☰ Filter Data + +
+ +
+
+ + Plant Sub Section + + +
+ + + + + + + + + + + + {/* Charts Section */} + {/* {!canViewData ? ( + + + Anda tidak memiliki akses untuk melihat data trending. +
+ Silakan hubungi administrator untuk mendapatkan akses. +
+
+ ) : ( */} + <> + + {/* Line Chart */} + + +
+ + ☰ Tag Value Trending + +
+
+ +
+
+ + + + {/* )} */} + + ); +}); + +export default IndexTrending; diff --git a/src/pages/history/trending/trending.css b/src/pages/history/trending/trending.css new file mode 100644 index 0000000..02da5b9 --- /dev/null +++ b/src/pages/history/trending/trending.css @@ -0,0 +1,118 @@ +.trending-container { + padding: 20px; + background-color: #f5f5f5; + min-height: 100vh; +} + +/* Filter Card */ +.filter-card { + margin-bottom: 24px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.filter-header { + padding-bottom: 12px; + border-bottom: 1px solid #f0f0f0; +} + +.filter-item { + margin-bottom: 8px; +} + +/* Statistic Cards */ +.stat-card { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + position: relative; + overflow: hidden; + height: 100%; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; +} + +.stat-card-red::before { + background-color: #ff4d4f; +} + +.stat-card-orange::before { + background-color: #ff9f43; +} + +.stat-card-green::before { + background-color: #52c41a; +} + +.stat-card-blue::before { + background-color: #1890ff; +} + +.stat-icon { + position: absolute; + right: 16px; + top: 16px; + opacity: 0.2; +} + +.stat-card-red .stat-icon { + color: #ff4d4f; +} + +.stat-card-orange .stat-icon { + color: #ff9f43; +} + +.stat-card-green .stat-icon { + color: #52c41a; +} + +.stat-card-blue .stat-icon { + color: #1890ff; +} + +/* Chart Cards */ +.chart-card { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + height: 100%; +} + +.chart-header { + padding-bottom: 12px; + border-bottom: 1px solid #f0f0f0; +} + +/* Responsive */ +@media (max-width: 768px) { + .trending-container { + padding: 12px; + } + + .stat-card { + margin-bottom: 12px; + } + + .chart-card { + margin-bottom: 16px; + } +} + +/* Custom Ant Design overrides */ +.trending-container .ant-statistic-title { + margin-bottom: 4px; +} + +.trending-container .ant-statistic-content { + font-size: 28px; +} + +.trending-container .ant-card-body { + padding: 20px; +} 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 diff --git a/src/pages/home/SvgTest.jsx b/src/pages/home/SvgTest.jsx new file mode 100644 index 0000000..1b65a69 --- /dev/null +++ b/src/pages/home/SvgTest.jsx @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +// import { ReactSVG } from 'react-svg'; +import { setValSvg } from '../../components/Global/MqttConnection'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgTest = () => { + return ( + <> + + + + Example SVG Value By Mqtt + + + + { + setValSvg(topicMqtt, svg); + }} + /> + + ); +}; + +export default SvgTest; diff --git a/src/pages/jadwalShift/IndexJadwalShift.jsx b/src/pages/jadwalShift/IndexJadwalShift.jsx new file mode 100644 index 0000000..95895a2 --- /dev/null +++ b/src/pages/jadwalShift/IndexJadwalShift.jsx @@ -0,0 +1,74 @@ +import React, { memo, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import ListJadwalShift from './component/ListJadwalShift'; +import DetailJadwalShift from './component/DetailJadwalShift'; +import { useBreadcrumb } from '../../layout/LayoutBreadcrumb'; +import { Typography } from 'antd'; + +const { Text } = Typography; + +const IndexJadwalShift = memo(function IndexJadwalShift() { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + const [readOnly, setReadOnly] = useState(false); + const [showModal, setShowModal] = useState(false); + + const setMode = (param) => { + setShowModal(true); + switch (param) { + case 'add': + setReadOnly(false); + break; + + case 'edit': + setReadOnly(false); + break; + + case 'preview': + setReadOnly(true); + break; + + default: + setShowModal(false); + break; + } + setActionMode(param); + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setBreadcrumbItems([ + { title: • Jadwal }, + { title: Jadwal Shift } + ]); + } else { + navigate('/signin'); + } + }, []); + + return ( + + + + + ); +}); + +export default IndexJadwalShift; \ No newline at end of file diff --git a/src/pages/jadwalShift/component/DetailJadwalShift.jsx b/src/pages/jadwalShift/component/DetailJadwalShift.jsx new file mode 100644 index 0000000..4f77120 --- /dev/null +++ b/src/pages/jadwalShift/component/DetailJadwalShift.jsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState } from 'react'; +import { + Modal, + Input, + Typography, + Button, + ConfigProvider, + Row, + Col +} from 'antd'; +import { NotifOk } from '../../../components/Global/ToastNotif'; + +const { Text } = Typography; + +const DetailJadwalShift = (props) => { + const [confirmLoading, setConfirmLoading] = useState(false); + + const defaultData = { + id: '', + nama_shift: '', + jam_masuk: '', + jam_pulang: '', + username: '', + nama_employee: '', + whatsapp: '' + }; + + const [FormData, setFormData] = useState(defaultData); + + const handleCancel = () => { + props.setSelectedData(null); + props.setActionMode('list'); + }; + + const handleSave = async () => { + setConfirmLoading(true); + // This is a dummy save function for slicing purposes + setTimeout(() => { + setConfirmLoading(false); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Data dummy berhasil disimpan.', + }); + props.setActionMode('list'); + }, 1000); + }; + + useEffect(() => { + if (props.selectedData) { + setFormData(props.selectedData); + } else { + setFormData(defaultData); + } + }, [props.showModal, props.selectedData]); + + // Dummy handler for slicing + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ ...FormData, [name]: value }); + }; + + return ( + + + + + + {!props.readOnly && ( + + )} + + , + ]} + > + {FormData && ( +
+ +
+ Nama Karyawan + + + + Username + + + + Nama Shift + + + + Whatsapp + + + + Jam Masuk + + + + Jam Pulang + + + + + )} + + ); +}; + +export default DetailJadwalShift; diff --git a/src/pages/jadwalShift/component/ListJadwalShift.jsx b/src/pages/jadwalShift/component/ListJadwalShift.jsx new file mode 100644 index 0000000..23f5dae --- /dev/null +++ b/src/pages/jadwalShift/component/ListJadwalShift.jsx @@ -0,0 +1,282 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input } from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + EyeOutlined, + SearchOutlined, +} from '@ant-design/icons'; +import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; +import { useNavigate } from 'react-router-dom'; +import TableList from '../../../components/Global/TableList'; + +// --- DUMMY DATA (Initial State) --- // +const initialDummyData = [ + { + id: 1, + nama_shift: 'Shift Pagi', + jam_masuk: '07:00', + jam_pulang: '15:00', + username: 'd.sanjaya', + nama_employee: 'Dede Sanjaya', + whatsapp: '081234567890' + }, + { + id: 2, + nama_shift: 'Shift Siang', + jam_masuk: '15:00', + jam_pulang: '23:00', + username: 'a.wijaya', + nama_employee: 'Andi Wijaya', + whatsapp: '081234567891' + }, + { + id: 3, + nama_shift: 'Shift Malam', + jam_masuk: '23:00', + jam_pulang: '07:00', + username: 'b.cahya', + nama_employee: 'Budi Cahya', + whatsapp: '081234567892' + }, +]; + +const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ + { + title: 'Nama Karyawan', + dataIndex: 'nama_employee', + key: 'nama_employee', + }, + { + title: 'Username', + dataIndex: 'username', + key: 'username', + }, + { + title: 'Nama Shift', + dataIndex: 'nama_shift', + key: 'nama_shift', + }, + { + title: 'Jam Masuk', + dataIndex: 'jam_masuk', + key: 'jam_masuk', + }, + { + title: 'Jam Pulang', + dataIndex: 'jam_pulang', + key: 'jam_pulang', + }, + { + title: 'Whatsapp', + dataIndex: 'whatsapp', + key: 'whatsapp', + }, + { + title: 'Aksi', + key: 'aksi', + align: 'center', + render: (_, record) => ( + + + + + { + const value = e.target.value; + setSearchValue(value); + if (value === '') { + handleSearchClear(); + } + }} + onSearch={handleSearch} + allowClear={{ + clearIcon: , + }} + enterButton={ + + } + size="large" + /> + + + + + + + + + + + + + + + + + ); +}); + +export default ListJadwalShift; \ No newline at end of file diff --git a/src/pages/master/brandDevice/IndexBrandDevice.jsx b/src/pages/master/brandDevice/IndexBrandDevice.jsx new file mode 100644 index 0000000..53042c1 --- /dev/null +++ b/src/pages/master/brandDevice/IndexBrandDevice.jsx @@ -0,0 +1,77 @@ + +import React, { memo, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import ListBrandDevice from './component/ListBrandDevice'; +import DetailBrandDevice from './component/DetailBrandDevice'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; +import { Typography } from 'antd'; + +const { Text } = Typography; + +const IndexBrandDevice = memo(function IndexBrandDevice() { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + const [readOnly, setReadOnly] = useState(false); + const [showModal, setShowmodal] = useState(false); + + const setMode = (param) => { + setActionMode(param); + switch (param) { + case 'add': + setReadOnly(false); + setShowmodal(true); + break; + + case 'edit': + setReadOnly(false); + setShowmodal(true); + break; + + case 'preview': + setReadOnly(true); + setShowmodal(true); + break; + + default: + setShowmodal(false); + break; + } + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setBreadcrumbItems([ + { title: • Master }, + { title: Brand Device } + ]); + } else { + navigate('/signin'); + } + }, []); + + return ( + + + + + ); +}); + +export default IndexBrandDevice; diff --git a/src/pages/master/brandDevice/component/DetailBrandDevice.jsx b/src/pages/master/brandDevice/component/DetailBrandDevice.jsx new file mode 100644 index 0000000..137bf27 --- /dev/null +++ b/src/pages/master/brandDevice/component/DetailBrandDevice.jsx @@ -0,0 +1,310 @@ +import { useEffect, useState } from 'react'; +import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd'; +import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; + +const { Text } = Typography; + +const DetailBrandDevice = (props) => { + const [confirmLoading, setConfirmLoading] = useState(false); + + const defaultData = { + brand_id: '', + brandName: '', + brandType: '', + manufacturer: '', + model: '', + status: 'Active', + }; + + const [FormData, setFormData] = useState(defaultData); + + const handleCancel = () => { + props.setSelectedData(null); + props.setActionMode('list'); + }; + + const handleSave = async () => { + setConfirmLoading(true); + + // Validasi required fields + if (!FormData.brandName) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Brand Name Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.brandType) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Type Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.manufacturer) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Manufacturer Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.model) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Model Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.status) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Status Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + const payload = { + brandName: FormData.brandName, + brandType: FormData.brandType, + manufacturer: FormData.manufacturer, + model: FormData.model, + status: FormData.status, + }; + + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 500)); + + const response = { + statusCode: FormData.brand_id ? 200 : 201, + data: { + brandName: FormData.brandName, + }, + }; + + console.log('Save Brand Device Response:', response); + + // Check if response is successful + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Brand Device "${ + response.data?.brandName || FormData.brandName + }" berhasil ${FormData.brand_id ? 'diubah' : 'ditambahkan'}.`, + }); + + props.setActionMode('list'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Terjadi kesalahan saat menyimpan data.', + }); + } + } catch (error) { + console.error('Save Brand Device Error:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', + }); + } + + setConfirmLoading(false); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...FormData, + [name]: value, + }); + }; + + const handleSelectChange = (name, value) => { + setFormData({ + ...FormData, + [name]: value, + }); + }; + + const handleStatusToggle = (event) => { + const isChecked = event; + setFormData({ + ...FormData, + status: isChecked ? true : false, + }); + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + if (props.selectedData != null) { + setFormData(props.selectedData); + } else { + setFormData(defaultData); + } + } else { + // navigate('/signin'); // Uncomment if useNavigate is imported + } + }, [props.showModal]); + + return ( + + + + + + {!props.readOnly && ( + + )} + + , + ]} + > + {FormData && ( +
+
+
+ Status +
+
+
+ +
+
+ {FormData.status === true ? 'Active' : 'Inactive'} +
+
+
+ + +
+ Brand Name + * + +
+
+ Type + * + +
+
+ Manufacturer + * + +
+
+ Model + * + +
+
+ )} +
+ ); +}; + +export default DetailBrandDevice; diff --git a/src/pages/master/brandDevice/component/ListBrandDevice.jsx b/src/pages/master/brandDevice/component/ListBrandDevice.jsx new file mode 100644 index 0000000..f53620f --- /dev/null +++ b/src/pages/master/brandDevice/component/ListBrandDevice.jsx @@ -0,0 +1,353 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag } from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + SearchOutlined, + EyeOutlined, +} from '@ant-design/icons'; +import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; +import { useNavigate } from 'react-router-dom'; +import TableList from '../../../../components/Global/TableList'; + +// Dummy data +const initialBrandDeviceData = [ + { + brand_id: 1, + brandName: 'Siemens S7-1200', + brandType: 'PLC', + manufacturer: 'Siemens', + model: 'S7-1200', + status: 'Active', + }, + { + brand_id: 2, + brandName: 'Allen Bradley CompactLogix', + brandType: 'PLC', + manufacturer: 'Rockwell Automation', + model: 'CompactLogix 5370', + status: 'Active', + }, + { + brand_id: 3, + brandName: 'Schneider Modicon M580', + brandType: 'PLC', + manufacturer: 'Schneider Electric', + model: 'M580', + status: 'Active', + }, + { + brand_id: 4, + brandName: 'Mitsubishi FX5U', + brandType: 'PLC', + manufacturer: 'Mitsubishi', + model: 'FX5U', + status: 'Inactive', + }, +]; + +const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Brand Device ', + dataIndex: 'brandName', + key: 'brandName', + width: '20%', + }, + { + title: 'Type', + dataIndex: 'brandType', + key: 'brandType', + width: '15%', + }, + { + title: 'Manufacturer', + dataIndex: 'manufacturer', + key: 'manufacturer', + width: '20%', + }, + { + title: 'model', + dataIndex: 'model', + key: 'model', + width: '15%', + }, + { + title: 'status', + dataIndex: 'status', + key: 'status', + width: '10%', + align: 'center', + render: (_, { status }) => ( + <> + {status === 'Active' ? ( + + Active + + ) : ( + + Inactive + + )} + + ), + }, + { + title: 'Aksi', + key: 'action', + align: 'center', + width: '15%', + render: (_, record) => ( + +
+ + + { + const value = e.target.value; + setSearchValue(value); + // Auto search when clearing by backspace/delete + if (value === '') { + setFormDataFilter({ search: '' }); + setTrigerFilter((prev) => !prev); + } + }} + onSearch={handleSearch} + allowClear={{ + clearIcon: , + }} + enterButton={ + + } + size="large" + /> + + + + + + + + + + + + + + + + + ); +}); + +export default ListBrandDevice; diff --git a/src/pages/master/brandDevice/component/ListErrorMaster.jsx b/src/pages/master/brandDevice/component/ListErrorMaster.jsx new file mode 100644 index 0000000..2041e7e --- /dev/null +++ b/src/pages/master/brandDevice/component/ListErrorMaster.jsx @@ -0,0 +1,33 @@ +import React, { memo } from 'react'; +import { Row, Col } from 'antd'; + +const ListErrorMaster = memo(function ListErrorMaster(props) { + return ( + + + +
+

+ Cooming soon +

+
+ + + + ); +}); + +export default ListErrorMaster; 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/CardDevice.jsx b/src/pages/master/device/component/CardDevice.jsx new file mode 100644 index 0000000..eb2ad74 --- /dev/null +++ b/src/pages/master/device/component/CardDevice.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Card, Button, Row, Col, Typography, Space, Tag } from 'antd'; +import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; + +const { Text } = Typography; + +const CardDevice = ({ data, showPreviewModal, showEditModal, showDeleteDialog }) => { + const getCardStyle = () => { + const color = '#FF8C42'; // Orange color + return { + border: `2px solid ${color}`, + borderRadius: '8px', + textAlign: 'center' // Center text + }; + }; + + const getTitleStyle = () => { + const backgroundColor = '#FF8C42'; // Orange color + return { + backgroundColor, + color: '#fff', + padding: '2px 8px', + borderRadius: '4px', + display: 'inline-block', + }; + }; + + return ( + + {data.map((item) => ( + + + {item.device_name} + + } + style={getCardStyle()} + actions={[ + +