From d2c755c03df2122e35f001f62c1c863810842df3 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 20 Oct 2025 13:49:42 +0700 Subject: [PATCH 01/13] Refactor: shift management API and UI components --- src/api/master-shift.jsx | 128 +++++- .../jadwalShift/component/ListJadwalShift.jsx | 171 ++++---- src/pages/master/shift/IndexShift.jsx | 127 +++--- .../master/shift/component/DetailShift.jsx | 288 ++++++++++--- .../master/shift/component/ListShift.jsx | 402 ++++++++++++------ 5 files changed, 760 insertions(+), 356 deletions(-) diff --git a/src/api/master-shift.jsx b/src/api/master-shift.jsx index a5a3f24..a4d7fee 100644 --- a/src/api/master-shift.jsx +++ b/src/api/master-shift.jsx @@ -6,9 +6,75 @@ const getAllShift = async (queryParams) => { method: 'get', prefix: `shift?${queryParams.toString()}`, }); - return response; + console.log('getAllShift response:', response); + console.log('Query params:', queryParams.toString()); + + // Check if response has error + if (response.error) { + console.error('getAllShift 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('getAllShift error:', error); + console.error('getAllShift catch error:', error); return { status: 500, data: { @@ -38,12 +104,27 @@ const createShift = async (queryParams) => { const response = await SendRequest({ method: 'post', prefix: `shift`, - data: queryParams, + params: queryParams, }); + console.log('createShift full response:', response); + console.log('createShift 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: [shift_object] } return { statusCode: response.statusCode || 200, - data: response.data, - message: response.message + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows }; }; @@ -51,12 +132,27 @@ const updateShift = async (id, queryParams) => { const response = await SendRequest({ method: 'put', prefix: `shift/${id}`, - data: queryParams, + params: queryParams, }); + console.log('updateShift full response:', response); + console.log('updateShift 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: [shift_object] } return { statusCode: response.statusCode || 200, - data: response.data, - message: response.message + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows }; }; @@ -65,10 +161,24 @@ const deleteShift = async (id) => { method: 'delete', prefix: `shift/${id}`, }); + console.log('deleteShift 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 + message: response.message, + rows: response.rows }; }; diff --git a/src/pages/jadwalShift/component/ListJadwalShift.jsx b/src/pages/jadwalShift/component/ListJadwalShift.jsx index 23f5dae..631c3c9 100644 --- a/src/pages/jadwalShift/component/ListJadwalShift.jsx +++ b/src/pages/jadwalShift/component/ListJadwalShift.jsx @@ -10,68 +10,42 @@ import { 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' - }, -]; +import { getAllJadwalShift, deleteJadwalShift } from '../../../api/jadwal-shift'; const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ { - title: 'Nama Karyawan', - dataIndex: 'nama_employee', - key: 'nama_employee', - }, - { - title: 'Username', - dataIndex: 'username', - key: 'username', + title: 'Tanggal Jadwal', + dataIndex: 'schedule_date', + key: 'schedule_date', + render: (date) => date ? new Date(date).toLocaleDateString('id-ID') : '-', }, { title: 'Nama Shift', - dataIndex: 'nama_shift', - key: 'nama_shift', + dataIndex: 'shift_name', + key: 'shift_name', + render: (text) => text || '-', }, { title: 'Jam Masuk', - dataIndex: 'jam_masuk', - key: 'jam_masuk', + dataIndex: 'start_time', + key: 'start_time', + render: (time) => time || '-', }, { title: 'Jam Pulang', - dataIndex: 'jam_pulang', - key: 'jam_pulang', + dataIndex: 'end_time', + key: 'end_time', + render: (time) => time || '-', }, { - title: 'Whatsapp', - dataIndex: 'whatsapp', - key: 'whatsapp', + title: 'Status', + dataIndex: 'is_active', + key: 'is_active', + render: (isActive) => ( + + {isActive ? 'Aktif' : 'Tidak Aktif'} + + ), }, { title: 'Aksi', @@ -101,39 +75,42 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ ]; const ListJadwalShift = memo(function ListJadwalShift(props) { - const [dataSource, setDataSource] = useState(initialDummyData); const [trigerFilter, setTrigerFilter] = useState(false); const defaultFilter = { criteria: '' }; const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [searchValue, setSearchValue] = useState(''); const navigate = useNavigate(); - // --- DUMMY API --- // - const getDummyData = (queryParams) => { - return new Promise((resolve) => { - const { criteria } = queryParams; - let data = dataSource; - if (criteria) { - data = dataSource.filter(item => - item.nama_employee.toLowerCase().includes(criteria.toLowerCase()) || - item.username.toLowerCase().includes(criteria.toLowerCase()) - ); - } - setTimeout(() => { - resolve({ - status: 200, - data: { - data: data, - paging: { - page: 1, - limit: 10, - total: data.length, - page_total: 1 - } + const getData = async (queryParams) => { + try { + const params = new URLSearchParams({ + page: queryParams.page || 1, + limit: queryParams.limit || 10, + criteria: queryParams.criteria || '' + }); + + const response = await getAllJadwalShift(params); + return response; + } catch (error) { + console.error('Error fetching jadwal shift:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Gagal mengambil data jadwal shift.', + }); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 } - }); - }, 100); - }); + } + }; + } }; useEffect(() => { @@ -146,7 +123,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { } else { navigate('/signin'); } - }, [props.actionMode, dataSource]); // Added dataSource to dependency array + }, [props.actionMode]); const doFilter = () => { setTrigerFilter((prev) => !prev); @@ -179,23 +156,41 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { }; const showDeleteDialog = (param) => { + const dateStr = param.schedule_date ? new Date(param.schedule_date).toLocaleDateString('id-ID') : 'tanggal tidak diketahui'; NotifConfirmDialog({ icon: 'question', title: 'Konfirmasi Hapus', - message: `Jadwal shift untuk "${param.nama_employee}" akan dihapus?`, - onConfirm: () => handleDelete(param.id), + message: `Jadwal shift tanggal ${dateStr} akan dihapus?`, + onConfirm: () => handleDelete(param.schedule_id), onCancel: () => props.setSelectedData(null), }); }; - const handleDelete = (id) => { - setDataSource(prevData => prevData.filter(item => item.id !== id)); - NotifAlert({ - icon: 'success', - title: 'Berhasil', - message: 'Data Jadwal Shift berhasil dihapus.', - }); - doFilter(); // Trigger a re-fetch from the new state + const handleDelete = async (id) => { + try { + const response = await deleteJadwalShift(id); + if (response.statusCode === 200) { + NotifAlert({ + icon: 'success', + title: 'Berhasil', + message: 'Data Jadwal Shift berhasil dihapus.', + }); + doFilter(); + } else { + NotifAlert({ + icon: 'error', + title: 'Error', + message: response.message || 'Gagal menghapus data jadwal shift.', + }); + } + } catch (error) { + console.error('Error deleting jadwal shift:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Terjadi kesalahan saat menghapus data.', + }); + } }; return ( @@ -206,7 +201,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { { const value = e.target.value; @@ -263,11 +258,11 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { { const [actionMode, setActionMode] = useState('list'); const [selectedData, setSelectedData] = useState(null); - const [shiftData, setShiftData] = useState([]); - const [loading, setLoading] = useState(false); + const [readOnly, setReadOnly] = useState(false); + const [showModal, setShowModal] = useState(false); - const fetchData = async () => { - setLoading(true); - try { - const localData = localStorage.getItem('shiftData'); - if (localData) { - setShiftData(JSON.parse(localData)); - } else { - const response = await getAllShift(); - if (response.data) { - setShiftData(response.data.data); - localStorage.setItem('shiftData', JSON.stringify(response.data.data)); - } - } - } catch (error) { - console.error("Error fetching shift data:", error); + 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; } - setLoading(false); }; useEffect(() => { - fetchData(); + const token = localStorage.getItem('token'); + if (token) { + setBreadcrumbItems([ + { title: • Master }, + { title: Shift } + ]); + } else { + navigate('/signin'); + } }, []); - const handleAddShift = (newShift) => { - const newData = { ...newShift, id: Date.now() }; // Simulate adding an ID - const updatedData = [newData, ...shiftData]; - setShiftData(updatedData); - localStorage.setItem('shiftData', JSON.stringify(updatedData)); - setActionMode('list'); - }; - - const handleUpdateShift = (updatedShift) => { - const updatedData = shiftData.map(shift => shift.id === updatedShift.id ? updatedShift : shift); - setShiftData(updatedData); - localStorage.setItem('shiftData', JSON.stringify(updatedData)); - setActionMode('list'); - }; - - const handleDeleteShift = (id) => { - const updatedData = shiftData.filter(shift => shift.id !== id); - setShiftData(updatedData); - localStorage.setItem('shiftData', JSON.stringify(updatedData)); - }; - - return ( - <> - {actionMode === 'list' && ( - - )} - {(actionMode === 'add' || actionMode === 'edit' || actionMode === 'preview') && ( - - )} - + + + + ); -}; +}); export default IndexShift; diff --git a/src/pages/master/shift/component/DetailShift.jsx b/src/pages/master/shift/component/DetailShift.jsx index d0b354b..d8bd410 100644 --- a/src/pages/master/shift/component/DetailShift.jsx +++ b/src/pages/master/shift/component/DetailShift.jsx @@ -7,13 +7,13 @@ const { Text } = Typography; const DetailShift = (props) => { const [confirmLoading, setConfirmLoading] = useState(false); - const readOnly = props.actionMode === 'preview'; const defaultData = { - id: '', - nama_shift: '', - jam_shift: '', - status: true, // default to active + shift_id: '', + shift_name: '', + start_time: '', + end_time: '', + is_active: true, }; const [FormData, setFormData] = useState(defaultData); @@ -26,68 +26,228 @@ const DetailShift = (props) => { const handleSave = async () => { setConfirmLoading(true); - if (!FormData.nama_shift) { - NotifOk({ icon: 'warning', title: 'Peringatan', message: 'Kolom Nama Shift Tidak Boleh Kosong' }); + // Validasi required fields + if (!FormData.shift_name || FormData.shift_name.trim() === '') { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Nama Shift Tidak Boleh Kosong', + }); setConfirmLoading(false); return; } - if (!FormData.jam_shift) { - NotifOk({ icon: 'warning', title: 'Peringatan', message: 'Kolom Jam Shift Tidak Boleh Kosong' }); + if (!FormData.start_time || FormData.start_time.trim() === '') { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Jam Mulai Tidak Boleh Kosong', + }); setConfirmLoading(false); return; } - const payload = { - nama_shift: FormData.nama_shift, - jam_shift: FormData.jam_shift, - status: FormData.status, - }; + if (!FormData.end_time || FormData.end_time.trim() === '') { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Jam Selesai Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + // Validate time format + const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/; + if (!timePattern.test(FormData.start_time)) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: + 'Format Jam Mulai tidak valid. Gunakan format HH:mm atau HH:mm:ss (contoh: 08:00)', + }); + setConfirmLoading(false); + return; + } + + if (!timePattern.test(FormData.end_time)) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: + 'Format Jam Selesai tidak valid. Gunakan format HH:mm atau HH:mm:ss (contoh: 17:00)', + }); + setConfirmLoading(false); + return; + } try { - if (FormData.id) { - props.onUpdate(payload); + if (FormData.shift_id) { + // Update existing shift + const payload = { + shift_name: FormData.shift_name, + start_time: FormData.start_time, + end_time: FormData.end_time, + is_active: FormData.is_active, + }; + + const response = await updateShift(FormData.shift_id, payload); + console.log('updateShift response:', response); + + if (response.statusCode === 200) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Shift "${FormData.shift_name}" berhasil diubah.`, + }); + props.setActionMode('list'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response.message || 'Gagal mengubah data Shift.', + }); + } } else { - props.onAdd(payload); + // Create new shift + const payload = { + shift_name: FormData.shift_name, + start_time: FormData.start_time, + end_time: FormData.end_time, + is_active: FormData.is_active, + }; + + const response = await createShift(payload); + console.log('createShift response:', response); + + if (response.statusCode === 200 || response.statusCode === 201) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Shift "${FormData.shift_name}" berhasil ditambahkan.`, + }); + props.setActionMode('list'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response.message || 'Gagal menambahkan data Shift.', + }); + } } - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: `Data Shift "${payload.nama_shift}" berhasil ${FormData.id ? 'diubah' : 'ditambahkan'}.`, - }); } catch (error) { console.error('Save Shift Error:', error); NotifAlert({ icon: 'error', title: 'Error', - message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', + message: error.message || 'Terjadi kesalahan saat menyimpan data.', }); } setConfirmLoading(false); }; - const handleInputChange = (e) => { - const { name, value } = e.target; - setFormData({ ...FormData, [name]: value }); + // Helper function to format time input + const formatTimeInput = (value) => { + if (!value) return value; + + // Remove any whitespace + value = value.trim(); + + // If user inputs single digit hour like "8:00", convert to "08:00" + const timeRegex = /^(\d{1,2}):(\d{2})(:\d{2})?$/; + const match = value.match(timeRegex); + + if (match) { + const hours = match[1].padStart(2, '0'); + const minutes = match[2]; + const seconds = match[3] || ''; + return `${hours}:${minutes}${seconds}`; + } + + return value; }; - const handleStatusToggle = (checked) => { - setFormData({ ...FormData, status: checked }); + const handleInputChange = (e) => { + const { name, value } = e.target; + + // Just set the value without formatting during typing + setFormData({ + ...FormData, + [name]: value, + }); + }; + + // Format time when user leaves the input field (onBlur) + const handleTimeBlur = (e) => { + const { name, value } = e.target; + + if (name === 'start_time' || name === 'end_time') { + const formattedValue = formatTimeInput(value); + setFormData({ + ...FormData, + [name]: formattedValue, + }); + } + }; + + const handleStatusToggle = (isChecked) => { + setFormData({ + ...FormData, + is_active: isChecked, + }); + }; + + // Helper function to extract time from ISO timestamp + const extractTime = (timeString) => { + if (!timeString) return ''; + + // If it's ISO timestamp like "1970-01-01T08:00:00.000Z" + if (timeString.includes('T')) { + const date = new Date(timeString); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; + } + + // If it's already in HH:mm:ss format, remove seconds + if (timeString.includes(':')) { + const parts = timeString.split(':'); + return `${parts[0]}:${parts[1]}`; + } + + return timeString; }; useEffect(() => { - if (props.selectedData) { - setFormData(props.selectedData); - } else { - setFormData(defaultData); + const token = localStorage.getItem('token'); + if (token) { + if (props.selectedData != null) { + // Only set fields that are in defaultData + const filteredData = { + shift_id: props.selectedData.shift_id || '', + shift_name: props.selectedData.shift_name || '', + start_time: extractTime(props.selectedData.start_time) || '', + end_time: extractTime(props.selectedData.end_time) || '', + is_active: props.selectedData.is_active ?? true, + }; + setFormData(filteredData); + } else { + setFormData(defaultData); + } } - }, [props.actionMode, props.selectedData]); + }, [props.showModal]); return ( @@ -100,7 +260,6 @@ const DetailShift = (props) => { defaultColor: '#23A55A', defaultBorderColor: '#23A55A', defaultHoverColor: '#23A55A', - defaultHoverBorderColor: '#23A55A', }, }, }} @@ -123,7 +282,7 @@ const DetailShift = (props) => { }, }} > - {!readOnly && ( + {!props.readOnly && ( @@ -134,7 +293,8 @@ const DetailShift = (props) => { > {FormData && (
-
+ {/* Status Toggle */} +
Status
@@ -147,42 +307,66 @@ const DetailShift = (props) => { >
- {FormData.status === true ? 'Active' : 'Inactive'} + {FormData.is_active === true ? 'Active' : 'Inactive'}
-
Nama Shift *
- Jam Shift + Jam Mulai * + + Contoh: 08:00 atau 08:00:00 + +
+
+ Jam Selesai + * + + + Contoh: 17:00 atau 17:00:00 +
)} @@ -190,4 +374,4 @@ const DetailShift = (props) => { ); }; -export default DetailShift; \ No newline at end of file +export default DetailShift; diff --git a/src/pages/master/shift/component/ListShift.jsx b/src/pages/master/shift/component/ListShift.jsx index 303c76f..1a2e07e 100644 --- a/src/pages/master/shift/component/ListShift.jsx +++ b/src/pages/master/shift/component/ListShift.jsx @@ -1,183 +1,305 @@ import React, { memo, useState, useEffect } from 'react'; -import { Button, Col, Row, Input, ConfigProvider, Card, Tag, Table, Space } from 'antd'; +import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, - SearchOutlined, EyeOutlined, - SyncOutlined, + SearchOutlined, } from '@ant-design/icons'; -import { NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; +import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; import { useNavigate } from 'react-router-dom'; +import TableList from '../../../../components/Global/TableList'; +import { getAllShift, deleteShift } from '../../../../api/master-shift'; -const ListShift = (props) => { +// Helper function to extract time from ISO timestamp +const extractTime = (timeString) => { + if (!timeString) return '-'; + + // If it's ISO timestamp like "1970-01-01T08:00:00.000Z" + if (timeString.includes('T')) { + const date = new Date(timeString); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; + } + + // If it's already in HH:mm or HH:mm:ss format + if (timeString.includes(':')) { + const parts = timeString.split(':'); + return `${parts[0]}:${parts[1]}`; // Return HH:mm only + } + + return timeString; +}; + +const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Nama Shift', + dataIndex: 'shift_name', + key: 'shift_name', + width: '20%', + }, + { + title: 'Jam Mulai', + dataIndex: 'start_time', + key: 'start_time', + width: '15%', + render: (time) => extractTime(time), + }, + { + title: 'Jam Selesai', + dataIndex: 'end_time', + key: 'end_time', + width: '15%', + render: (time) => extractTime(time), + }, + { + title: 'Status', + dataIndex: 'is_active', + key: 'is_active', + width: '10%', + align: 'center', + render: (_, { is_active }) => { + const color = is_active ? 'green' : 'red'; + const text = is_active ? 'Active' : 'Inactive'; + return ( + + {text} + + ); + }, + }, + { + title: 'Aksi', + key: 'aksi', + align: 'center', + width: '20%', + render: (_, record) => ( + + } - size="large" - /> - - - - - - -
- - - - - - + + + + + + + { + const value = e.target.value; + setSearchValue(value); + // Auto search when clearing by backspace/delete + if (value === '') { + handleSearchClear(); + } + }} + onSearch={handleSearch} + allowClear + onClear={handleSearchClear} + enterButton={ + + } + size="large" + /> + + + + + + + + + + + + + + + + ); -}; +}); -export default memo(ListShift); \ No newline at end of file +export default ListShift; \ No newline at end of file From 6a9bbb2b5eb16321885f20b5892aa2d724a4c8f1 Mon Sep 17 00:00:00 2001 From: vinix Date: Mon, 20 Oct 2025 15:41:16 +0700 Subject: [PATCH 02/13] fix: correct initial state values and improve pagination handling in TableList component --- src/components/Global/TableList.jsx | 48 +++++++++++++++++------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/components/Global/TableList.jsx b/src/components/Global/TableList.jsx index 4630bc8..840fc9a 100644 --- a/src/components/Global/TableList.jsx +++ b/src/components/Global/TableList.jsx @@ -34,9 +34,9 @@ const TableList = memo(function TableList({ const [data, setData] = useState([]); const [pagingResponse, setPagingResponse] = useState({ - totalData: '', - perPage: '', - totalPage: '', + totalData: 0, + perPage: 0, + totalPage: 0, }); const [pagination, setPagination] = useState({ @@ -62,30 +62,38 @@ const TableList = memo(function TableList({ }; const param = new URLSearchParams({ ...paging, ...queryParams }); - const resData = await getData(param); + if (resData) { setTimeout(() => { setGridLoading(false); }, 900); + } else { + setGridLoading(false); + return; } - setData(resData.data.data ?? []); - setFilterData(resData.data.data ?? []); + const dataToSet = resData.data?.data ?? resData.data ?? []; + setData(dataToSet); + setFilterData(dataToSet); if (resData.status == 200) { - setPagingResponse({ - totalData: resData.paging.total_limit, - perPage: resData.paging.page_total, - totalPage: resData.paging.total_page, - }); + const pagingData = resData.data?.paging; - setPagination((prev) => ({ - ...prev, - current: resData.paging.current_page, - limit: resData.paging.current_limit, - total: resData.paging.total_limit, - })); + if (pagingData) { + setPagingResponse({ + totalData: pagingData.total || 0, + perPage: pagingData.limit || 0, + totalPage: pagingData.page_total || 0, + }); + + setPagination((prev) => ({ + ...prev, + current: pagingData.page || 1, + limit: pagingData.limit || 10, + total: pagingData.total || 0, + })); + } } }; @@ -93,7 +101,7 @@ const TableList = memo(function TableList({ setPagination((prev) => ({ ...prev, current: page, - pageSize, + limit: pageSize, })); filter(page, pageSize); }; @@ -138,7 +146,7 @@ const TableList = memo(function TableList({
- Menampilkan {pagingResponse.totalPage} Data dari {pagingResponse.perPage}{' '} + Menampilkan {pagingResponse.totalData} Data dari {pagingResponse.totalPage}{' '} Halaman
@@ -148,7 +156,7 @@ const TableList = memo(function TableList({ onChange={handlePaginationChange} onShowSizeChange={handlePaginationChange} current={pagination.current} - pageSize={pagination.pageSize} + pageSize={pagination.limit} total={pagination.total} /> From 6d45b5bf1166a44e7357be7fc2fdcec66fe4f08a Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 20 Oct 2025 16:00:15 +0700 Subject: [PATCH 03/13] fix CardList --- src/components/Global/CardList.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/Global/CardList.jsx b/src/components/Global/CardList.jsx index 624bca8..746e7c5 100644 --- a/src/components/Global/CardList.jsx +++ b/src/components/Global/CardList.jsx @@ -80,16 +80,18 @@ const CardList = ({ ]} >
- {column.map((itemCard) => ( - <> - {!itemCard.hidden && !itemCard.render && ( -

+ {column.map((itemCard, index) => ( + + {!itemCard.hidden && itemCard.title !== 'No' && itemCard.title !== 'Aksi' && ( +

{itemCard.title}:{' '} - {item[itemCard.key]} + {itemCard.render + ? itemCard.render(item[itemCard.dataIndex], item, index) + : item[itemCard.dataIndex] || item[itemCard.key] || '-' + }

)} - {itemCard.render && itemCard.render} - + ))}
From fb3e5001393b968926b41199bd829923d0ebeb97 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 20 Oct 2025 19:19:59 +0700 Subject: [PATCH 04/13] feat: implement field auto-incrementing code --- .../master/device/component/DetailDevice.jsx | 72 ++++- .../component/DetailPlantSection.jsx | 72 ++++- src/pages/master/tag/component/DetailTag.jsx | 270 ++++++++++++++++-- .../master/unit/component/DetailUnit.jsx | 74 ++++- 4 files changed, 428 insertions(+), 60 deletions(-) diff --git a/src/pages/master/device/component/DetailDevice.jsx b/src/pages/master/device/component/DetailDevice.jsx index b8fb6d3..c49a273 100644 --- a/src/pages/master/device/component/DetailDevice.jsx +++ b/src/pages/master/device/component/DetailDevice.jsx @@ -12,7 +12,7 @@ import { } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd'; -import { createDevice, updateDevice } from '../../../../api/master-device'; +import { createDevice, updateDevice, getAllDevice } from '../../../../api/master-device'; import { Checkbox } from 'antd'; const CheckboxGroup = Checkbox.Group; @@ -33,6 +33,7 @@ const DetailDevice = (props) => { }; const [FormData, setFormData] = useState(defaultData); + const [nextDeviceCode, setNextDeviceCode] = useState('Auto-fill'); const [jenisPermit, setJenisPermit] = useState([]); const [checkedList, setCheckedList] = useState([]); @@ -215,12 +216,55 @@ const DetailDevice = (props) => { }); }; + const generateNextDeviceCode = async () => { + try { + const params = new URLSearchParams({ limit: 10000 }); + const response = await getAllDevice(params); + + if (response && response.data && response.data.data) { + const devices = response.data.data; + + if (devices.length === 0) { + setNextDeviceCode('DVC001'); + return; + } + + // Extract numeric part from device codes and find the maximum + const deviceNumbers = devices + .map((device) => { + const match = device.device_code?.match(/dvc(\d+)/i); + return match ? parseInt(match[1], 10) : 0; + }) + .filter((num) => !isNaN(num)); + + const maxNumber = deviceNumbers.length > 0 ? Math.max(...deviceNumbers) : 0; + const nextNumber = maxNumber + 1; + + // Format with leading zeros (DVC001, DVC002, etc.) + const nextCode = `DVC${String(nextNumber).padStart(3, '0')}`; + setNextDeviceCode(nextCode); + } else { + setNextDeviceCode('DVC001'); + } + } catch (error) { + console.error('Error generating next device code:', error); + setNextDeviceCode('Auto-fill'); + } + }; + useEffect(() => { const token = localStorage.getItem('token'); if (token) { - // Only call getDataJenisPermit if permitDefault is enabled - if (props.permitDefault) { - getDataJenisPermit(); + if (props.showModal) { + // Only call getDataJenisPermit if permitDefault is enabled + if (props.permitDefault) { + getDataJenisPermit(); + } + + // Generate next device code only for add mode + if (props.actionMode === 'add' && !props.selectedData) { + generateNextDeviceCode(); + } } if (props.selectedData != null) { @@ -234,7 +278,7 @@ const DetailDevice = (props) => { } else { // navigate('/signin'); // Uncomment if useNavigate is imported } - }, [props.showModal]); + }, [props.showModal, props.actionMode]); return ( { disabled /> - {/*
+ {/* Device Code - Auto Increment & Read Only */} +
Device Code - * -
*/} +
Device Name * diff --git a/src/pages/master/plantSection/component/DetailPlantSection.jsx b/src/pages/master/plantSection/component/DetailPlantSection.jsx index 3f0849d..a4c7c86 100644 --- a/src/pages/master/plantSection/component/DetailPlantSection.jsx +++ b/src/pages/master/plantSection/component/DetailPlantSection.jsx @@ -9,7 +9,7 @@ import { Divider, } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; -import { createPlantSection, updatePlantSection } from '../../../../api/master-plant-section'; +import { createPlantSection, updatePlantSection, getAllPlantSection } from '../../../../api/master-plant-section'; const { Text } = Typography; @@ -24,6 +24,7 @@ const DetailPlantSection = (props) => { }; const [FormData, setFormData] = useState(defaultData); + const [nextPlantSectionCode, setNextPlantSectionCode] = useState('Auto-fill'); const handleInputChange = (e) => { const { name, value } = e.target; @@ -103,14 +104,56 @@ const DetailPlantSection = (props) => { }); }; + const generateNextPlantSectionCode = async () => { + try { + const params = new URLSearchParams({ limit: 10000 }); + const response = await getAllPlantSection(params); + + if (response && response.data && response.data.data) { + const sections = response.data.data; + + if (sections.length === 0) { + setNextPlantSectionCode('SUB001'); + return; + } + + // Extract numeric part from plant section codes and find the maximum + const sectionNumbers = sections + .map((section) => { + const match = section.sub_section_code?.match(/sub(\d+)/i); + return match ? parseInt(match[1], 10) : 0; + }) + .filter((num) => !isNaN(num)); + + const maxNumber = sectionNumbers.length > 0 ? Math.max(...sectionNumbers) : 0; + const nextNumber = maxNumber + 1; + + // Format with leading zeros (SUB001, SUB002, etc.) + const nextCode = `SUB${String(nextNumber).padStart(3, '0')}`; + setNextPlantSectionCode(nextCode); + } else { + setNextPlantSectionCode('SUB001'); + } + } catch (error) { + console.error('Error generating next plant section code:', error); + setNextPlantSectionCode('Auto-fill'); + } + }; useEffect(() => { + if (props.showModal) { + // Generate next plant section code only for add mode + if (props.actionMode === 'add' && !props.selectedData) { + generateNextPlantSectionCode(); + } + } + if (props.selectedData) { setFormData(props.selectedData); } else { setFormData(defaultData); } - }, [props.showModal, props.selectedData]); + }, [props.showModal, props.selectedData, props.actionMode]); return ( {
- {props.actionMode !== 'add' && ( -
- Plant Section Code - -
- )} + {/* Plant Section Code - Auto Increment & Read Only */} +
+ Plant Section Code + +
Plant Sub Section Name diff --git a/src/pages/master/tag/component/DetailTag.jsx b/src/pages/master/tag/component/DetailTag.jsx index 6da2556..2bf55ea 100644 --- a/src/pages/master/tag/component/DetailTag.jsx +++ b/src/pages/master/tag/component/DetailTag.jsx @@ -26,12 +26,19 @@ const DetailTag = (props) => { unit: '', is_active: true, is_alarm: false, + is_report: false, + is_history: false, + lim_low_crash: '', + lim_low: '', + lim_high: '', + lim_high_crash: '', device_id: null, sub_section_id: null, }; const [FormData, setFormData] = useState(defaultData); + const [nextTagCode, setNextTagCode] = useState('Auto-fill'); const handleCancel = () => { props.setSelectedData(null); @@ -120,13 +127,13 @@ const DetailTag = (props) => { return; } - // Validasi data type harus Diskrit atau Analog - const validDataTypes = ['Diskrit', 'Analog']; + // Validasi data type harus Discrete atau Analog + const validDataTypes = ['Discrete', 'Analog']; if (!validDataTypes.includes(FormData.data_type)) { NotifOk({ icon: 'warning', title: 'Peringatan', - message: `Data Type harus "Diskrit" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`, + message: `Data Type harus "Discrete" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`, }); setConfirmLoading(false); return; @@ -153,6 +160,17 @@ const DetailTag = (props) => { return; } + // Plant Sub Section validation + if (!FormData.sub_section_id) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Plant Sub Section harus dipilih', + }); + setConfirmLoading(false); + return; + } + // Prepare payload berdasarkan backend validation schema const payload = { tag_name: FormData.tag_name.trim(), @@ -161,9 +179,25 @@ const DetailTag = (props) => { unit: FormData.unit.trim(), is_active: FormData.is_active, is_alarm: FormData.is_alarm, + is_report: FormData.is_report, + is_history: FormData.is_history, device_id: parseInt(FormData.device_id), }; + // Add limit fields only if they have values + if (FormData.lim_low_crash !== '' && FormData.lim_low_crash !== null) { + payload.lim_low_crash = parseFloat(FormData.lim_low_crash); + } + if (FormData.lim_low !== '' && FormData.lim_low !== null) { + payload.lim_low = parseFloat(FormData.lim_low); + } + if (FormData.lim_high !== '' && FormData.lim_high !== null) { + payload.lim_high = parseFloat(FormData.lim_high); + } + if (FormData.lim_high_crash !== '' && FormData.lim_high_crash !== null) { + payload.lim_high_crash = parseFloat(FormData.lim_high_crash); + } + // Add sub_section_id only if it's selected if (FormData.sub_section_id) { payload.sub_section_id = parseInt(FormData.sub_section_id); @@ -184,14 +218,17 @@ const DetailTag = (props) => { // Check if response is successful if (response && (response.statusCode === 200 || response.statusCode === 201)) { + // response.data is already an object (converted in master-tag.jsx API) + const tagCode = response.data?.tag_code || ''; + const tagName = response.data?.tag_name || FormData.tag_name || ''; + const tagDisplay = tagCode ? `${tagCode} - ${tagName}` : tagName; + NotifOk({ icon: 'success', title: 'Berhasil', - message: - response.message || - `Data Tag "${response.data?.tag_name || FormData.tag_name}" berhasil ${ - FormData.tag_id ? 'diubah' : 'ditambahkan' - }.`, + message: `Data Tag "${tagDisplay}" berhasil ${ + FormData.tag_id ? 'diubah' : 'ditambahkan' + }.`, }); props.setActionMode('list'); @@ -252,6 +289,20 @@ const DetailTag = (props) => { }); }; + const handleReportToggle = (isChecked) => { + setFormData({ + ...FormData, + is_report: isChecked, + }); + }; + + const handleHistoryToggle = (isChecked) => { + setFormData({ + ...FormData, + is_history: isChecked, + }); + }; + const loadDevices = async () => { setLoadingDevices(true); try { @@ -314,6 +365,42 @@ const DetailTag = (props) => { } }; + const generateNextTagCode = async () => { + try { + const params = new URLSearchParams({ limit: 10000 }); + const response = await getAllTag(params); + + if (response && response.data && response.data.data) { + const tags = response.data.data; + + if (tags.length === 0) { + setNextTagCode('TAG001'); + return; + } + + // Extract numeric part from tag codes and find the maximum + const tagNumbers = tags + .map((tag) => { + const match = tag.tag_code?.match(/tag(\d+)/i); + return match ? parseInt(match[1], 10) : 0; + }) + .filter((num) => !isNaN(num)); + + const maxNumber = tagNumbers.length > 0 ? Math.max(...tagNumbers) : 0; + const nextNumber = maxNumber + 1; + + // Format with leading zeros (TAG001, TAG002, etc.) + const nextCode = `TAG${String(nextNumber).padStart(3, '0')}`; + setNextTagCode(nextCode); + } else { + setNextTagCode('TAG001'); + } + } catch (error) { + console.error('Error generating next tag code:', error); + setNextTagCode('Auto-fill'); + } + }; + useEffect(() => { const token = localStorage.getItem('token'); if (token) { @@ -322,6 +409,11 @@ const DetailTag = (props) => { loadDevices(); loadPlantSubSections(); loadUnits(); + + // Generate next tag code only for add mode + if (props.actionMode === 'add' && !props.selectedData) { + generateNextTagCode(); + } } if (props.selectedData != null) { @@ -335,6 +427,12 @@ const DetailTag = (props) => { unit: props.selectedData.unit || '', is_active: props.selectedData.is_active ?? true, is_alarm: props.selectedData.is_alarm ?? false, + is_report: props.selectedData.is_report ?? false, + is_history: props.selectedData.is_history ?? false, + lim_low_crash: props.selectedData.lim_low_crash ?? '', + lim_low: props.selectedData.lim_low ?? '', + lim_high: props.selectedData.lim_high ?? '', + lim_high_crash: props.selectedData.lim_high_crash ?? '', device_id: props.selectedData.device_id || null, device_code: props.selectedData.device_code || '', device_name: props.selectedData.device_name || '', @@ -347,7 +445,7 @@ const DetailTag = (props) => { } else { // navigate('/signin'); // Uncomment if useNavigate is imported } - }, [props.showModal]); + }, [props.showModal, props.actionMode]); return ( { } Tag`} open={props.showModal} onCancel={handleCancel} + width={800} footer={[ <> { disabled />
- {/* Tag Code hanya ditampilkan saat EDIT atau PREVIEW */} - {(props.actionMode === 'edit' || props.actionMode === 'preview') && ( -
- Tag Code - -
- )} {/* Status dan Alarm dalam satu baris */}
{
+ {/* Report dan History dalam satu baris */} +
+
+ {/* Report Toggle */} +
+
+ Report + * +
+
+
+ +
+
+ {FormData.is_report === true ? 'Yes' : 'No'} +
+
+
+ {/* History Toggle */} +
+
+ History + * +
+
+
+ +
+
+ {FormData.is_history === true ? 'Yes' : 'No'} +
+
+
+
+
+ {/* Tag Code - Auto Increment & Read Only */} +
+ Tag Code + +
Tag Number * @@ -532,7 +706,7 @@ const DetailTag = (props) => { onChange={(value) => handleSelectChange('data_type', value)} disabled={props.readOnly} > - Diskrit + Discrete Analog
@@ -562,8 +736,58 @@ const DetailTag = (props) => { ))} + {/* Limit Fields */} +
+ Limit Low Crash + +
+
+ Limit Low + +
+
+ Limit High + +
+
+ Limit High Crash + +
Plant Sub Section + * -
- )} + {/* Unit Code - Auto Increment & Read Only */} +
+ Unit Code + +
Name * From f7f11907dc6a7e63bebd0fa7e617585e809a583f Mon Sep 17 00:00:00 2001 From: Fachba Date: Tue, 21 Oct 2025 13:01:37 +0700 Subject: [PATCH 05/13] add menu dashboard svg --- src/App.jsx | 17 +++ src/layout/LayoutMenu.jsx | 63 +++++++- src/pages/home/SvgAirDryerA.jsx | 20 +++ src/pages/home/SvgAirDryerB.jsx | 20 +++ src/pages/home/SvgAirDryerC.jsx | 20 +++ src/pages/home/SvgCompressorA.jsx | 20 +++ src/pages/home/SvgCompressorB.jsx | 20 +++ src/pages/home/SvgCompressorC.jsx | 20 +++ src/pages/home/SvgOverview.jsx | 20 +++ src/pages/home/SvgTemplate.jsx | 19 +++ src/pages/home/SvgTest.jsx | 1 + src/pages/home/SvgViewer.jsx | 19 +++ svg/air_dryer_A_rev.svg | 238 ++++++++++++++++++++++++++++++ svg/air_dryer_B_rev.svg | 238 ++++++++++++++++++++++++++++++ svg/air_dryer_C_rev.svg | 238 ++++++++++++++++++++++++++++++ 15 files changed, 968 insertions(+), 5 deletions(-) create mode 100644 src/pages/home/SvgAirDryerA.jsx create mode 100644 src/pages/home/SvgAirDryerB.jsx create mode 100644 src/pages/home/SvgAirDryerC.jsx create mode 100644 src/pages/home/SvgCompressorA.jsx create mode 100644 src/pages/home/SvgCompressorB.jsx create mode 100644 src/pages/home/SvgCompressorC.jsx create mode 100644 src/pages/home/SvgOverview.jsx create mode 100644 src/pages/home/SvgTemplate.jsx create mode 100644 src/pages/home/SvgViewer.jsx create mode 100644 svg/air_dryer_A_rev.svg create mode 100644 svg/air_dryer_B_rev.svg create mode 100644 svg/air_dryer_C_rev.svg diff --git a/src/App.jsx b/src/App.jsx index 511886c..31dfb1d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -35,6 +35,13 @@ import IndexUser from './pages/user/IndexUser'; import IndexMember from './pages/shiftManagement/member/IndexMember'; import SvgTest from './pages/home/SvgTest'; +import SvgOverview from './pages/home/SvgOverview'; +import SvgCompressorA from './pages/home/SvgCompressorA'; +import SvgCompressorB from './pages/home/SvgCompressorB'; +import SvgCompressorC from './pages/home/SvgCompressorC'; +import SvgAirDryerA from './pages/home/SvgAirDryerA'; +import SvgAirDryerB from './pages/home/SvgAirDryerB'; +import SvgAirDryerC from './pages/home/SvgAirDryerC'; const App = () => { return ( @@ -52,6 +59,16 @@ const App = () => { } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + }> } /> } /> diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx index 6e0bc57..e3c9d62 100644 --- a/src/layout/LayoutMenu.jsx +++ b/src/layout/LayoutMenu.jsx @@ -26,6 +26,9 @@ import { TeamOutlined, ClockCircleOutlined, CalendarOutlined, + DesktopOutlined, + NodeExpandOutlined, + GroupOutlined, } from '@ant-design/icons'; const { Text } = Typography; @@ -40,6 +43,48 @@ const allItems = [ ), }, + { + key: 'dashboard-svg', + icon: , + label: 'Dashboard', + children: [ + { + key: 'dashboard-svg-overview', + icon: , + label: Overview, + }, + { + key: 'dashboard-svg-compressor-a', + icon: , + label: Compressor A, + }, + { + key: 'dashboard-svg-compressor-b', + icon: , + label: Compressor B, + }, + { + key: 'dashboard-svg-compressor-c', + icon: , + label: Compressor C, + }, + { + key: 'dashboard-svg-airdryer-a', + icon: , + label: Air Dryer A, + }, + { + key: 'dashboard-svg-airdryer-b', + icon: , + label: Air Dryer B, + }, + { + key: 'dashboard-svg-airdryer-c', + icon: , + label: Air Dryer C, + }, + ], + }, { key: 'master', icon: , @@ -60,16 +105,16 @@ const allItems = [ icon: , label: Device, }, - { - key: 'master-tag', - icon: , - label: Tag, - }, { key: 'master-unit', icon: , label: Unit, }, + { + key: 'master-tag', + icon: , + label: Tag, + }, { key: 'master-status', icon: , @@ -163,6 +208,7 @@ const LayoutMenu = () => { if (pathname === '/notification') return 'notification'; if (pathname === '/event-alarm') return 'event-alarm'; if (pathname === '/jadwal-shift') return 'jadwal-shift'; + if (pathname === '/dashboard-svg') return 'dashboard-svg'; // Handle master routes if (pathname.startsWith('/master/')) { @@ -170,6 +216,12 @@ const LayoutMenu = () => { return `master-${subPath}`; } + // Handle master routes + if (pathname.startsWith('/dashboard-svg/')) { + const subPath = pathParts[1]; + return `dashboard-svg-${subPath}`; + } + // Handle history routes if (pathname.startsWith('/history/')) { const subPath = pathParts[1]; @@ -188,6 +240,7 @@ const LayoutMenu = () => { // Function to get parent key from menu key const getParentKey = (key) => { if (key.startsWith('master-')) return 'master'; + if (key.startsWith('dashboard-svg-')) return 'dashboard-svg'; if (key.startsWith('history-')) return 'history'; if (key.startsWith('shift-')) return 'shift-management'; return null; diff --git a/src/pages/home/SvgAirDryerA.jsx b/src/pages/home/SvgAirDryerA.jsx new file mode 100644 index 0000000..37f07a3 --- /dev/null +++ b/src/pages/home/SvgAirDryerA.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/air_dryer_A_rev.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgAirDryerA = () => { + return ( + + + + ); +}; + +export default SvgAirDryerA; diff --git a/src/pages/home/SvgAirDryerB.jsx b/src/pages/home/SvgAirDryerB.jsx new file mode 100644 index 0000000..6250cea --- /dev/null +++ b/src/pages/home/SvgAirDryerB.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/air_dryer_B_rev.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgAirDryerB = () => { + return ( + + + + ); +}; + +export default SvgAirDryerB; diff --git a/src/pages/home/SvgAirDryerC.jsx b/src/pages/home/SvgAirDryerC.jsx new file mode 100644 index 0000000..cdb61ce --- /dev/null +++ b/src/pages/home/SvgAirDryerC.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/air_dryer_C_rev.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgAirDryerC = () => { + return ( + + + + ); +}; + +export default SvgAirDryerC; diff --git a/src/pages/home/SvgCompressorA.jsx b/src/pages/home/SvgCompressorA.jsx new file mode 100644 index 0000000..bc7ffd2 --- /dev/null +++ b/src/pages/home/SvgCompressorA.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgCompressorA = () => { + return ( + + + + ); +}; + +export default SvgCompressorA; diff --git a/src/pages/home/SvgCompressorB.jsx b/src/pages/home/SvgCompressorB.jsx new file mode 100644 index 0000000..fbf5871 --- /dev/null +++ b/src/pages/home/SvgCompressorB.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgCompressorB = () => { + return ( + + + + ); +}; + +export default SvgCompressorB; diff --git a/src/pages/home/SvgCompressorC.jsx b/src/pages/home/SvgCompressorC.jsx new file mode 100644 index 0000000..0983260 --- /dev/null +++ b/src/pages/home/SvgCompressorC.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgCompressorC = () => { + return ( + + + + ); +}; + +export default SvgCompressorC; diff --git a/src/pages/home/SvgOverview.jsx b/src/pages/home/SvgOverview.jsx new file mode 100644 index 0000000..d5b432d --- /dev/null +++ b/src/pages/home/SvgOverview.jsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Card, Typography, Flex } from 'antd'; +import { setValSvg } from '../../components/Global/MqttConnection'; +import SvgTemplate from './SvgTemplate'; +import SvgViewer from './SvgViewer'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgOverview = () => { + return ( + + + + ); +}; + +export default SvgOverview; diff --git a/src/pages/home/SvgTemplate.jsx b/src/pages/home/SvgTemplate.jsx new file mode 100644 index 0000000..da2a702 --- /dev/null +++ b/src/pages/home/SvgTemplate.jsx @@ -0,0 +1,19 @@ +const SvgTemplate = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default SvgTemplate; diff --git a/src/pages/home/SvgTest.jsx b/src/pages/home/SvgTest.jsx index 1b65a69..87d8beb 100644 --- a/src/pages/home/SvgTest.jsx +++ b/src/pages/home/SvgTest.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Card, Typography, Flex } from 'antd'; // import { ReactSVG } from 'react-svg'; import { setValSvg } from '../../components/Global/MqttConnection'; +import { ReactSVG } from 'react-svg'; const { Text } = Typography; diff --git a/src/pages/home/SvgViewer.jsx b/src/pages/home/SvgViewer.jsx new file mode 100644 index 0000000..242f012 --- /dev/null +++ b/src/pages/home/SvgViewer.jsx @@ -0,0 +1,19 @@ +// SvgViewer.jsx +import { ReactSVG } from 'react-svg'; + +const SvgViewer = ({ filePathSvg, topicMqtt, setValSvg }) => { + return ( + { + svg.setAttribute('width', '100%'); + svg.setAttribute('height', '100%'); + svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); + if (setValSvg) setValSvg(topicMqtt, svg); + }} + style={{ width: '100%', height: '100%' }} + /> + ); +}; + +export default SvgViewer; diff --git a/svg/air_dryer_A_rev.svg b/svg/air_dryer_A_rev.svg new file mode 100644 index 0000000..53876ed --- /dev/null +++ b/svg/air_dryer_A_rev.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heater Temp SP + + ####.## + °F + + Heater Temp + + ####.## + °F + + Heater Temp + + ####.## + °F + HEATER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dew Temp + + ####.## + °C + + Dew Temp + + ####.## + °F + AIROUTLET + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AIRINLET + + + + + + + + + + + + + + + + + + + + + + + + + RUN HOUR + + ####.## + H + + PURGE HOUR + + ####.## + H + + HEATER HOUR + + ####.## + H + + Alarm Info + + ####.## + H + + RT or LT Dry + + LT Dry + RT Dry + + Opmode + + HTD + + Step + ####.## + s + + Cycle Timer + + ####.## + s + Time + ## + + Dryer Status + + + + Shutdown + + + + Alarm Status + + + REGEN + + DRYING + + REGEN + + DRYING + + AIR DRYER UNIT A (01-CL-10532-A) + \ No newline at end of file diff --git a/svg/air_dryer_B_rev.svg b/svg/air_dryer_B_rev.svg new file mode 100644 index 0000000..4dff501 --- /dev/null +++ b/svg/air_dryer_B_rev.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heater Temp SP + + ####.## + °F + + Heater Temp + + ####.## + °F + + Heater Temp + + ####.## + °F + HEATER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dew Temp + + ####.## + °C + + Dew Temp + + ####.## + °F + AIROUTLET + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AIRINLET + + + + + + + + + + + + + + + + + + + + + + + + + RUN HOUR + + ####.## + H + + PURGE HOUR + + ####.## + H + + HEATER HOUR + + ####.## + H + + Alarm Info + + ####.## + H + + RT or LT Dry + + LT Dry + RT Dry + + Opmode + + HTD + + Step + ####.## + s + + Cycle Timer + + ####.## + s + Time + ## + + Dryer Status + + + + Shutdown + + + + Alarm Status + + + REGEN + + DRYING + + REGEN + + DRYING + + AIR DRYER UNIT B (01-CL-10535-B) + \ No newline at end of file diff --git a/svg/air_dryer_C_rev.svg b/svg/air_dryer_C_rev.svg new file mode 100644 index 0000000..3c13ba1 --- /dev/null +++ b/svg/air_dryer_C_rev.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heater Temp SP + + ####.## + °F + + Heater Temp + + ####.## + °F + + Heater Temp + + ####.## + °F + HEATER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dew Temp + + ####.## + °C + + Dew Temp + + ####.## + °F + AIROUTLET + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AIRINLET + + + + + + + + + + + + + + + + + + + + + + + + + RUN HOUR + + ####.## + H + + PURGE HOUR + + ####.## + H + + HEATER HOUR + + ####.## + H + + Alarm Info + + ####.## + H + + RT or LT Dry + + LT Dry + RT Dry + + Opmode + + HTD + + Step + ####.## + s + + Cycle Timer + + ####.## + s + Time + ## + + Dryer Status + + + + Shutdown + + + + Alarm Status + + + REGEN + + DRYING + + REGEN + + DRYING + + AIR DRYER UNIT C (01-CL-10539-C) + \ No newline at end of file From cc124555642738a3fd0c536ff60777a68d5c4349 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Tue, 21 Oct 2025 14:45:22 +0700 Subject: [PATCH 06/13] fix: improve time extraction by utilizing dayjs for ISO timestamp formatting --- src/pages/master/shift/component/DetailShift.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/master/shift/component/DetailShift.jsx b/src/pages/master/shift/component/DetailShift.jsx index d8bd410..86d1b8d 100644 --- a/src/pages/master/shift/component/DetailShift.jsx +++ b/src/pages/master/shift/component/DetailShift.jsx @@ -2,6 +2,10 @@ import { useEffect, useState } from 'react'; import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { createShift, updateShift } from '../../../../api/master-shift'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); const { Text } = Typography; @@ -198,16 +202,13 @@ const DetailShift = (props) => { }); }; - // Helper function to extract time from ISO timestamp + // Helper function to extract time from ISO timestamp using dayjs const extractTime = (timeString) => { if (!timeString) return ''; // If it's ISO timestamp like "1970-01-01T08:00:00.000Z" if (timeString.includes('T')) { - const date = new Date(timeString); - const hours = String(date.getUTCHours()).padStart(2, '0'); - const minutes = String(date.getUTCMinutes()).padStart(2, '0'); - return `${hours}:${minutes}`; + return dayjs.utc(timeString).format('HH:mm'); } // If it's already in HH:mm:ss format, remove seconds From 94d395fe4969b922a64e58561ef264e756bfada9 Mon Sep 17 00:00:00 2001 From: vinix Date: Tue, 21 Oct 2025 08:39:17 +0700 Subject: [PATCH 07/13] feat: implement CRUD operations for status management in master-status API --- src/api/master-status.jsx | 168 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/api/master-status.jsx diff --git a/src/api/master-status.jsx b/src/api/master-status.jsx new file mode 100644 index 0000000..d5ed80d --- /dev/null +++ b/src/api/master-status.jsx @@ -0,0 +1,168 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllStatus = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `status?${queryParams.toString()}`, + }); + + if (response.error) { + console.error('getAllStatus 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 + }; + } + + 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 + } + }; + } + + 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; + + 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('getAllStatus catch error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getStatusById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `status/${id}`, + }); + return response.data; +}; + +const createStatus = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `status`, + params: queryParams, + }); + + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateStatus = async (status_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `status/${status_id}`, + params: queryParams, + }); + + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteStatus = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `status/${queryParams}`, + }); + + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllStatus, getStatusById, createStatus, updateStatus, deleteStatus }; \ No newline at end of file From cf063822eb6ce9da74f0d8a58b31bb864d6a1a14 Mon Sep 17 00:00:00 2001 From: Iqbal Rizqi Kurniawan Date: Tue, 21 Oct 2025 17:07:36 +0700 Subject: [PATCH 08/13] feat: add brand device management with error code handling and navigation --- src/App.jsx | 2 + src/api/master-brand.jsx | 135 ++++++++ src/pages/master/brand/ErrorCode.jsx | 151 ++++++++ src/pages/master/brand/FormBrand.jsx | 59 ++++ .../master/brandDevice/AddBrandDevice.jsx | 325 ++++++++++++++++++ .../master/brandDevice/IndexBrandDevice.jsx | 51 +-- .../component/DetailBrandDevice.jsx | 310 ----------------- .../brandDevice/component/ListBrandDevice.jsx | 9 +- 8 files changed, 677 insertions(+), 365 deletions(-) create mode 100644 src/api/master-brand.jsx create mode 100644 src/pages/master/brand/ErrorCode.jsx create mode 100644 src/pages/master/brand/FormBrand.jsx create mode 100644 src/pages/master/brandDevice/AddBrandDevice.jsx delete mode 100644 src/pages/master/brandDevice/component/DetailBrandDevice.jsx diff --git a/src/App.jsx b/src/App.jsx index 511886c..9f77ca2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,6 +14,7 @@ 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 AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; import IndexPlantSection from './pages/master/plantSection/IndexPlantSection'; import IndexStatus from './pages/master/status/IndexStatus'; import IndexShift from './pages/master/shift/IndexShift'; @@ -57,6 +58,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/api/master-brand.jsx b/src/api/master-brand.jsx new file mode 100644 index 0000000..e68b2bb --- /dev/null +++ b/src/api/master-brand.jsx @@ -0,0 +1,135 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllBrands = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `brand?${queryParams.toString()}`, + }); + 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 + } + }; + } + + 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; + + 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('getAllBrands error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getBrandById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `brand/${id}`, + }); + return response.data; +}; + +const createBrand = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `brand`, + params: queryParams, + }); + if (Array.isArray(response) && response.length === 0) { + return { + statusCode: 500, + data: null, + message: 'Request failed', + rows: 0 + }; + } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateBrand = async (brand_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `brand/${brand_id}`, + params: queryParams, + }); + if (Array.isArray(response) && response.length === 0) { + return { + statusCode: 500, + data: null, + message: 'Request failed', + rows: 0 + }; + } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteBrand = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `brand/${queryParams}`, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand }; diff --git a/src/pages/master/brand/ErrorCode.jsx b/src/pages/master/brand/ErrorCode.jsx new file mode 100644 index 0000000..81bc7da --- /dev/null +++ b/src/pages/master/brand/ErrorCode.jsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Card, Typography, Button, Modal, Form, Input, message } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import TableList from '../../../components/Global/TableList'; +// import { getAllErrorCodesByBrand, createErrorCode, updateErrorCode, deleteErrorCode } from '../../api/master-errorcode'; // Mock this later + +const { Title } = Typography; + +// Mock API functions for now +const mockApi = { + errorCodes: [ + { error_code_id: 1, brand_id: 1, error_code: 'E-001', description: 'Paper Jam' }, + { error_code_id: 2, brand_id: 1, error_code: 'E-002', description: 'Low Ink' }, + ], + getAllErrorCodesByBrand: async (brandId) => { + return { status: 200, data: { data: mockApi.errorCodes.filter(ec => ec.brand_id == brandId) } }; + }, + createErrorCode: async (data) => { + const newId = Math.max(...mockApi.errorCodes.map(ec => ec.error_code_id)) + 1; + const newErrorCode = { ...data, error_code_id: newId }; + mockApi.errorCodes.push(newErrorCode); + return { statusCode: 201, data: newErrorCode }; + }, + updateErrorCode: async (id, data) => { + const index = mockApi.errorCodes.findIndex(ec => ec.error_code_id === id); + if (index !== -1) { + mockApi.errorCodes[index] = { ...mockApi.errorCodes[index], ...data }; + return { statusCode: 200, data: mockApi.errorCodes[index] }; + } + return { statusCode: 404, message: 'Not Found' }; + }, + deleteErrorCode: async (id) => { + const index = mockApi.errorCodes.findIndex(ec => ec.error_code_id === id); + if (index !== -1) { + mockApi.errorCodes.splice(index, 1); + return { statusCode: 200 }; + } + return { statusCode: 404, message: 'Not Found' }; + } +}; + +const ErrorCodePage = () => { + const { brandId } = useParams(); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const [errorCodes, setErrorCodes] = useState([]); + const [loading, setLoading] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingErrorCode, setEditingErrorCode] = useState(null); + + const fetchData = async () => { + setLoading(true); + const response = await mockApi.getAllErrorCodesByBrand(brandId); + if (response.status === 200) { + setErrorCodes(response.data.data); + } + setLoading(false); + }; + + useEffect(() => { + fetchData(); + }, [brandId]); + + const columns = [ + { title: 'Error Code', dataIndex: 'error_code', key: 'error_code' }, + { title: 'Description', dataIndex: 'description', key: 'description' }, + { + title: 'Action', + key: 'action', + render: (_, record) => ( + <> + + + + ), + }, + ]; + + const handleAdd = () => { + setEditingErrorCode(null); + form.resetFields(); + setIsModalVisible(true); + }; + + const handleEdit = (errorCode) => { + setEditingErrorCode(errorCode); + form.setFieldsValue(errorCode); + setIsModalVisible(true); + }; + + const handleDelete = async (id) => { + await mockApi.deleteErrorCode(id); + message.success('Error code deleted successfully'); + fetchData(); + }; + + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + if (editingErrorCode) { + await mockApi.updateErrorCode(editingErrorCode.error_code_id, values); + message.success('Error code updated successfully'); + } else { + await mockApi.createErrorCode({ ...values, brand_id: brandId }); + message.success('Error code created successfully'); + } + setIsModalVisible(false); + fetchData(); + } catch (error) { + console.log('Validate Failed:', error); + } + }; + + return ( + + Manage Error Codes for Brand ID: {brandId} + + ({ data: { data: errorCodes } })} + triger={brandId} + /> + setIsModalVisible(false)} + > +
+ + + + + + + +
+
+ ); +}; + +export default ErrorCodePage; diff --git a/src/pages/master/brand/FormBrand.jsx b/src/pages/master/brand/FormBrand.jsx new file mode 100644 index 0000000..13dabc2 --- /dev/null +++ b/src/pages/master/brand/FormBrand.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { Form, Input, Button, Typography, Card, message } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { createBrand } from '../../api/master-brand'; + +const { Title } = Typography; + +const FormBrand = () => { + const [form] = Form.useForm(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + + const onFinish = async (values) => { + setLoading(true); + try { + const response = await createBrand(values); + if (response.statusCode === 200 || response.statusCode === 201) { + message.success('Brand created successfully!'); + const newBrandId = response.data.brand_id; + // Redirect to the error code page for the new brand + navigate(`/master/brand/${newBrandId}/error-codes`); + } else { + message.error(response.message || 'Failed to create brand.'); + } + } catch (error) { + message.error('An error occurred while creating the brand.'); + console.error(error); + } + setLoading(false); + }; + + return ( + + Add New Brand +
+ + + + + + + + +
+ ); +}; + +export default FormBrand; diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx new file mode 100644 index 0000000..984695f --- /dev/null +++ b/src/pages/master/brandDevice/AddBrandDevice.jsx @@ -0,0 +1,325 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Input, Divider, Typography, Switch, Button, Steps, Form, message, Table, Row, Col, Radio, Card, Tag, Upload, ConfigProvider } from 'antd'; +import { PlusOutlined, UploadOutlined } from '@ant-design/icons'; +import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; + +const { Text, Title } = Typography; +const { Step } = Steps; + +// Mock API for Error Codes (can be moved to a separate file later) +const mockErrorCodeApi = { + errorCodes: [], + createErrorCode: async (data) => { + const newId = mockErrorCodeApi.errorCodes.length > 0 ? Math.max(...mockErrorCodeApi.errorCodes.map(ec => ec.error_code_id)) + 1 : 1; + const newErrorCode = { ...data, error_code_id: newId }; + mockErrorCodeApi.errorCodes.push(newErrorCode); + return { statusCode: 201, data: newErrorCode }; + }, +}; + +const AddBrandDevice = () => { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + const [brandForm] = Form.useForm(); + const [errorCodeForm] = Form.useForm(); + const [confirmLoading, setConfirmLoading] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [anotherSolutionType, setAnotherSolutionType] = useState(null); + const [fileList, setFileList] = useState([]); + + // Watch for form values changes to update the switch color + const statusValue = Form.useWatch('status', errorCodeForm); + + const defaultData = { + brandName: '', + brandType: '', + manufacturer: '', + model: '', + status: true, + }; + + const [formData, setFormData] = useState(defaultData); + const [errorCodes, setErrorCodes] = useState([]); + + const handleCancel = () => { + navigate('/master/brand-device'); + }; + + const handleNextStep = async () => { + try { + await brandForm.validateFields(); + setCurrentStep(1); + } catch (error) { + console.log('Validate Failed:', error); + NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' }); + } + }; + + const handleFinish = async () => { + if (errorCodes.length === 0) { + NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Silakan tambahkan minimal satu error code.' }); + return; + } + + setConfirmLoading(true); + try { + const finalFormData = { ...formData, status: formData.status ? 'Active' : 'Inactive' }; + console.log("Saving brand device:", finalFormData); + await new Promise((resolve) => setTimeout(resolve, 500)); + const newBrandDeviceId = Date.now(); + console.log("Brand device saved with ID:", newBrandDeviceId); + + console.log("Saving error codes:", errorCodes); + for (const errorCode of errorCodes) { + if (errorCode.another_solution === 'image' && errorCode.image) { + console.log(`Uploading image for error code ${errorCode.error_code}:`, errorCode.image.name); + } + await mockErrorCodeApi.createErrorCode({ + ...errorCode, + brand_device_id: newBrandDeviceId + }); + console.log("Saved error code:", errorCode.error_code); + } + + setConfirmLoading(false); + NotifOk({ icon: 'success', title: 'Berhasil', message: 'Brand Device dan Error Code berhasil disimpan.' }); + navigate('/master/brand-device'); + } catch (error) { + setConfirmLoading(false); + console.error("Failed to save data:", error); + NotifAlert({ + icon: "error", + title: "Gagal", + message: "Gagal menyimpan data. Silakan coba lagi.", + }); + } + }; + + const handleAddErrorCode = async () => { + try { + const values = await errorCodeForm.validateFields(); + const newErrorCode = { + ...values, + status: values.status === undefined ? true : values.status, + image: fileList.length > 0 ? fileList[0] : null, + key: `temp-${Date.now()}` + }; + setErrorCodes([...errorCodes, newErrorCode]); + message.success('Error code berhasil ditambahkan'); + errorCodeForm.resetFields(); + setAnotherSolutionType(null); + setFileList([]); + } catch (error) { + console.log('Validate Failed:', error); + NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' }); + } + }; + + const handleDeleteErrorCode = (key) => { + setErrorCodes(errorCodes.filter(item => item.key !== key)); + message.success('Error code berhasil dihapus'); + }; + + const uploadProps = { + onRemove: (file) => { + setFileList([]); + }, + beforeUpload: (file) => { + setFileList([file]); + return false; // Prevent auto-upload + }, + fileList, + }; + + const errorCodeColumns = [ + { title: 'Error Code', dataIndex: 'error_code', key: 'error_code' }, + { title: 'Trouble Description', dataIndex: 'description', key: 'description' }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status) => ( + + {status ? 'Active' : 'Inactive'} + + ), + }, + { + title: 'Action', + key: 'action', + render: (_, record) => ( + + ), + }, + ]; + + useEffect(() => { + brandForm.setFieldsValue(formData); + }, [formData, brandForm]); + + useEffect(() => { + setBreadcrumbItems([ + { title: • Master }, + { title: navigate('/master/brand-device')}>Brand Device }, + { title: Tambah Brand Device } + ]); + }, [setBreadcrumbItems, navigate]); + + const renderStepContent = () => { + if (currentStep === 0) { + return ( +
setFormData(prev => ({...prev, ...allValues}))} initialValues={formData}> + + + + + + + + + + + + + + + + + ); + } + if (currentStep === 1) { + return ( +
+ Tambah Error Code {errorCodes.length + 1} +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + setAnotherSolutionType(e.target.value)}> + Image + Other + + + {anotherSolutionType === 'image' && ( + + + + + + )} + {anotherSolutionType === 'other' && ( + + + + )} + + + + + + Daftar Error Code +
+ + ); + } + return null; + }; + + return ( + + Tambah Brand Device + + + + + +
+ {renderStepContent()} +
+ +
+ + + {currentStep > 0 && ( + + )} + + + {currentStep < 1 && ( + + )} + {currentStep === 1 && ( + + )} + +
+
+ ); +}; + +export default AddBrandDevice; diff --git a/src/pages/master/brandDevice/IndexBrandDevice.jsx b/src/pages/master/brandDevice/IndexBrandDevice.jsx index 53042c1..2deecdf 100644 --- a/src/pages/master/brandDevice/IndexBrandDevice.jsx +++ b/src/pages/master/brandDevice/IndexBrandDevice.jsx @@ -1,8 +1,6 @@ - -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, 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'; @@ -12,35 +10,6 @@ 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) { @@ -55,23 +24,9 @@ const IndexBrandDevice = memo(function IndexBrandDevice() { return ( - - + ); }); -export default IndexBrandDevice; +export default IndexBrandDevice; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/DetailBrandDevice.jsx b/src/pages/master/brandDevice/component/DetailBrandDevice.jsx deleted file mode 100644 index 137bf27..0000000 --- a/src/pages/master/brandDevice/component/DetailBrandDevice.jsx +++ /dev/null @@ -1,310 +0,0 @@ -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 index f53620f..9b1d740 100644 --- a/src/pages/master/brandDevice/component/ListBrandDevice.jsx +++ b/src/pages/master/brandDevice/component/ListBrandDevice.jsx @@ -231,11 +231,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { props.setActionMode('edit'); }; - const showAddModal = (param = null) => { - props.setSelectedData(param); - props.setActionMode('add'); - }; - const showDeleteDialog = (param) => { NotifConfirmDialog({ icon: 'question', @@ -320,7 +315,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { > + { - filter(1, 10); + filter(1, pagination.current_limit); }, [triger]); const filter = async (currentPage, pageSize) => { setGridLoading(true); const paging = { - page: currentPage, - limit: pageSize, + page: Number(currentPage), + limit: Number(pageSize), }; const param = new URLSearchParams({ ...paging, ...queryParams }); const resData = await getData(param); + setData(resData?.data ?? []); + + const pagingData = resData?.paging; + + if (pagingData) { + setPagination((prev) => ({ + ...prev, + current_page: pagingData.current_page || 1, + current_limit: pagingData.current_limit || 10, + total_limit: pagingData.total_limit || 0, + total_page: pagingData.total_page || 1, + })); + } + if (resData) { setTimeout(() => { setGridLoading(false); @@ -72,29 +70,6 @@ const TableList = memo(function TableList({ setGridLoading(false); return; } - - const dataToSet = resData.data?.data ?? resData.data ?? []; - setData(dataToSet); - setFilterData(dataToSet); - - if (resData.status == 200) { - const pagingData = resData.data?.paging; - - if (pagingData) { - setPagingResponse({ - totalData: pagingData.total || 0, - perPage: pagingData.limit || 0, - totalPage: pagingData.page_total || 0, - }); - - setPagination((prev) => ({ - ...prev, - current: pagingData.page || 1, - limit: pagingData.limit || 10, - total: pagingData.total || 0, - })); - } - } }; const handlePaginationChange = (page, pageSize) => { @@ -146,8 +121,8 @@ const TableList = memo(function TableList({
- Menampilkan {pagingResponse.totalData} Data dari {pagingResponse.totalPage}{' '} - Halaman + Menampilkan {pagination.current_limit} data halaman{' '} + {pagination.current_page} dari total {pagination.total_limit} data
@@ -155,9 +130,9 @@ const TableList = memo(function TableList({ showSizeChanger onChange={handlePaginationChange} onShowSizeChange={handlePaginationChange} - current={pagination.current} - pageSize={pagination.limit} - total={pagination.total} + current={pagination.current_page} + pageSize={pagination.current_limit} + total={pagination.total_limit} /> diff --git a/src/components/Global/ToastNotif.jsx b/src/components/Global/ToastNotif.jsx index cf9d8ad..2cdeaf9 100644 --- a/src/components/Global/ToastNotif.jsx +++ b/src/components/Global/ToastNotif.jsx @@ -16,6 +16,7 @@ const NotifOk = ({ icon, title, message }) => { icon: icon, title: title, text: message, + html: message.replace(/\n/g, '
'), }); }; diff --git a/src/pages/auth/Registration.jsx b/src/pages/auth/Registration.jsx deleted file mode 100644 index a836ffd..0000000 --- a/src/pages/auth/Registration.jsx +++ /dev/null @@ -1,461 +0,0 @@ -// import React, { useState } from 'react'; -// import { -// Flex, -// Input, -// InputNumber, -// Form, -// Button, -// Card, -// Space, -// Upload, -// Divider, -// Tooltip, -// message, -// Select, -// } from 'antd'; -// import { -// UploadOutlined, -// UserOutlined, -// IdcardOutlined, -// PhoneOutlined, -// LockOutlined, -// InfoCircleOutlined, -// MailOutlined, -// } from '@ant-design/icons'; -// const { Item } = Form; -// const { Option } = Select; -// import sypiu_ggcp from 'assets/sypiu_ggcp.jpg'; -// import { useNavigate } from 'react-router-dom'; -// import { register, uploadFile, checkUsername } from '../../api/auth'; -// import { NotifAlert } from '../../components/Global/ToastNotif'; - -// const Registration = () => { -// const [form] = Form.useForm(); -// const navigate = useNavigate(); -// const [loading, setLoading] = useState(false); -// const [fileListKontrak, setFileListKontrak] = useState([]); -// const [fileListHsse, setFileListHsse] = useState([]); -// const [fileListIcon, setFileListIcon] = useState([]); - -// // Daftar jenis vendor -// const vendorTypes = [ -// { vendor_type: 1, vendor_type_name: 'One-Time' }, -// { vendor_type: 2, vendor_type_name: 'Rutin' }, -// ]; - -// const onFinish = async (values) => { -// setLoading(true); -// try { -// if (!fileListKontrak.length || !fileListHsse.length) { -// message.error('Harap unggah Lampiran Kontrak Kerja dan HSSE Plan!'); -// setLoading(false); -// return; -// } - -// const formData = new FormData(); -// formData.append('path_kontrak', fileListKontrak[0].originFileObj); -// formData.append('path_hse_plant', fileListHsse[0].originFileObj); -// if (fileListIcon.length) { -// formData.append('path_icon', fileListIcon[0].originFileObj); -// } - -// const uploadResponse = await uploadFile(formData); - -// if (!uploadResponse.data?.pathKontrak && !uploadResponse.data?.pathHsePlant) { -// message.error(uploadResponse.message || 'Gagal mengunggah file.'); -// setLoading(false); -// return; -// } - -// const params = new URLSearchParams({ username: values.username }); -// const usernameCheck = await checkUsername(params); -// if (usernameCheck.data.data && usernameCheck.data.data.available === false) { -// NotifAlert({ -// icon: 'error', -// title: 'Gagal', -// message: usernameCheck.data.message || 'Terjadi kesalahan, silakan coba lagi', -// }); -// setLoading(false); -// return; -// } - -// const registerData = { -// nama_perusahaan: values.namaPerusahaan, -// no_kontak_wo: values.noKontakWo, -// path_kontrak: uploadResponse.data.pathKontrak || '', -// durasi: values.durasiPekerjaan, -// nilai_csms: values.nilaiCsms.toString(), -// vendor_type: values.jenisVendor, // Tambahkan jenis vendor ke registerData -// path_hse_plant: uploadResponse.data.pathHsePlant || '', -// nama_leader: values.penanggungJawab, -// no_identitas: values.noIdentitas, -// no_hp: values.noHandphone, -// email_register: values.username, -// password_register: values.password, -// }; - -// const response = await register(registerData); - -// if (response.data?.id_register) { -// message.success('Data berhasil disimpan!'); - -// try { -// form.resetFields(); -// setFileListKontrak([]); -// setFileListHsse([]); -// setFileListIcon([]); - -// navigate('/registration-submitted'); -// } catch (postSuccessError) { -// message.warning( -// 'Registrasi berhasil, tetapi ada masalah setelahnya. Silakan ke halaman login secara manual.' -// ); -// } -// } else { -// message.error(response.message || 'Pendaftaran gagal, silakan coba lagi.'); -// } -// } catch (error) { -// console.error('Error saat registrasi:', error); -// NotifAlert({ -// icon: 'error', -// title: 'Gagal', -// message: error.message || 'Terjadi kesalahan, silakan coba lagi', -// }); -// } finally { -// setLoading(false); -// } -// }; - -// const onCancel = () => { -// form.resetFields(); -// setFileListKontrak([]); -// setFileListHsse([]); -// setFileListIcon([]); -// navigate('/signin'); -// }; - -// const handleChangeKontrak = ({ fileList }) => { -// setFileListKontrak(fileList); -// }; - -// const handleChangeHsse = ({ fileList }) => { -// setFileListHsse(fileList); -// }; - -// const handleChangeIcon = ({ fileList }) => { -// setFileListIcon(fileList); -// }; - -// const beforeUpload = (file, fieldname) => { -// const isValidType = [ -// 'image/jpeg', -// 'image/jpg', -// 'image/png', -// fieldname !== 'path_icon' ? 'application/pdf' : null, -// ] -// .filter(Boolean) -// .includes(file.type); -// const isNotEmpty = file.size > 0; -// const isSizeValid = file.size / 1024 / 1024 < 10; - -// if (!isValidType) { -// message.error( -// `Hanya file ${ -// fieldname === 'path_icon' ? 'JPG/PNG' : 'PDF/JPG/PNG' -// } yang diperbolehkan!` -// ); -// return false; -// } -// if (!isNotEmpty) { -// message.error('File tidak boleh kosong!'); -// return false; -// } -// if (!isSizeValid) { -// message.error('Ukuran file maksimal 10MB!'); -// return false; -// } -// return true; -// }; - -// return ( -// -// -//

Formulir Pendaftaran

-// -//
-// } -// > -// -// {/* 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" -// /> -// - -// {/* Akun Pengguna */} -// -// Akun Pengguna (digunakan sebagai user login SYPIU) -// -// -// } -// placeholder="Masukkan Email" -// size="large" -// /> -// -// -// } -// placeholder="Masukkan Password" -// size="large" -// /> -// - -// {/* Tombol */} -// -// -// -// -// -// -// -// -// -// ); -// }; - -// export default Registration; diff --git a/src/pages/auth/SignIn.jsx b/src/pages/auth/SignIn.jsx index f63ea0d..3e95747 100644 --- a/src/pages/auth/SignIn.jsx +++ b/src/pages/auth/SignIn.jsx @@ -31,8 +31,9 @@ const SignIn = () => { prefix: 'auth/generate-captcha', token: false, }); - setCaptchaSvg(res.data.svg || ''); - setCaptchaText(res.data.text || ''); + + setCaptchaSvg(res.data?.data?.svg || ''); + setCaptchaText(res.data?.data?.text || ''); } catch (err) { console.error('Error fetching captcha:', err); } @@ -57,8 +58,8 @@ const SignIn = () => { withCredentials: true, }); - const user = res?.data?.user || res?.user; - const accessToken = res?.data?.accessToken || res?.tokens?.accessToken; + const user = res?.data?.data?.user || res?.user; + const accessToken = res?.data?.data?.accessToken || res?.tokens?.accessToken; if (user && accessToken) { localStorage.setItem('token', accessToken); diff --git a/src/pages/master/brandDevice/component/ListBrandDevice.jsx b/src/pages/master/brandDevice/component/ListBrandDevice.jsx index 9b1d740..cd9b654 100644 --- a/src/pages/master/brandDevice/component/ListBrandDevice.jsx +++ b/src/pages/master/brandDevice/component/ListBrandDevice.jsx @@ -10,6 +10,7 @@ import { import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; import { useNavigate } from 'react-router-dom'; import TableList from '../../../../components/Global/TableList'; +import { getAllBrands } from '../../../../api/master-brand'; // Dummy data const initialBrandDeviceData = [ @@ -145,55 +146,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { const navigate = useNavigate(); - // Dummy data function to simulate API call - now uses state - const getAllBrandDevice = async (params) => { - // Simulate API delay - await new Promise((resolve) => setTimeout(resolve, 300)); - - // Extract URLSearchParams - TableList sends URLSearchParams object - const searchParam = params.get('search') || ''; - const page = parseInt(params.get('page')) || 1; - const limit = parseInt(params.get('limit')) || 10; - - console.log('getAllBrandDevice called with:', { searchParam, page, limit }); - - // Filter by search - let filteredBrandDevices = brandDeviceData; - if (searchParam) { - const searchLower = searchParam.toLowerCase(); - filteredBrandDevices = brandDeviceData.filter( - (brand) => - brand.brandName.toLowerCase().includes(searchLower) || - brand.brandType.toLowerCase().includes(searchLower) || - brand.manufacturer.toLowerCase().includes(searchLower) || - brand.model.toLowerCase().includes(searchLower) - ); - } - - // Pagination logic - const totalData = filteredBrandDevices.length; - const totalPages = Math.ceil(totalData / limit); - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - const paginatedData = filteredBrandDevices.slice(startIndex, endIndex); - - // Return structure that matches TableList expectation - 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) { @@ -333,7 +285,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { showPreviewModal={showPreviewModal} showEditModal={showEditModal} showDeleteDialog={showDeleteDialog} - getData={getAllBrandDevice} + getData={getAllBrands} queryParams={formDataFilter} columns={columns(showPreviewModal, showEditModal, showDeleteDialog)} triger={trigerFilter} diff --git a/src/pages/master/device/component/DetailDevice.jsx b/src/pages/master/device/component/DetailDevice.jsx index c49a273..a0d951a 100644 --- a/src/pages/master/device/component/DetailDevice.jsx +++ b/src/pages/master/device/component/DetailDevice.jsx @@ -1,20 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { - Modal, - Input, - Divider, - Typography, - Switch, - Button, - ConfigProvider, - Radio, - Select, -} from 'antd'; +import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; -import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd'; -import { createDevice, updateDevice, getAllDevice } from '../../../../api/master-device'; -import { Checkbox } from 'antd'; -const CheckboxGroup = Checkbox.Group; +import { createDevice, updateDevice } from '../../../../api/master-device'; +import { validateRun } from '../../../../Utils/validate'; const { Text } = Typography; const { TextArea } = Input; @@ -27,156 +15,60 @@ const DetailDevice = (props) => { device_code: '', device_name: '', is_active: true, - device_location: 'Building A', + device_location: '', device_description: '', ip_address: '', }; - const [FormData, setFormData] = useState(defaultData); - const [nextDeviceCode, setNextDeviceCode] = useState('Auto-fill'); - - const [jenisPermit, setJenisPermit] = useState([]); - const [checkedList, setCheckedList] = useState([]); - - const onChange = (list) => { - setCheckedList(list); - }; - - const onChangeRadio = (e) => { - setFormData({ - ...FormData, - type_input: e.target.value, - }); - }; - - const getDataJenisPermit = async () => { - setCheckedList([]); - const result = await getJenisPermit(); - const data = result.data ?? []; - const names = data.map((item) => ({ - value: item.id_jenis_permit, - label: item.nama_jenis_permit, - })); - setJenisPermit(names); - }; + const [formData, setFormData] = useState(defaultData); const handleCancel = () => { props.setSelectedData(null); props.setActionMode('list'); }; - const validateIPAddress = (ip) => { - const ipRegex = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - return ipRegex.test(ip); - }; - const handleSave = async () => { setConfirmLoading(true); - // Validasi required fields - // if (!FormData.device_code) { - // NotifOk({ - // icon: 'warning', - // title: 'Peringatan', - // message: 'Kolom Device Code Tidak Boleh Kosong', - // }); - // setConfirmLoading(false); - // return; - // } + // Daftar aturan validasi + const validationRules = [ + { field: 'device_name', label: 'Device Name', required: true }, + { field: 'ip_address', label: 'Ip Address', required: true, ip: true }, + ]; - if (!FormData.device_name) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Device Name Tidak Boleh Kosong', - }); - setConfirmLoading(false); + if ( + validateRun(formData, validationRules, (errorMessages) => { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: errorMessages, + }); + setConfirmLoading(false); + }) + ) return; - } - - if (!FormData.device_location) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Device Location Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - if (!FormData.ip_address) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom IP Address Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - // Validasi format IP - if (!validateIPAddress(FormData.ip_address)) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Format IP Address Tidak Valid', - }); - setConfirmLoading(false); - return; - } - - if (props.permitDefault && checkedList.length === 0) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Jenis Permit Tidak Boleh Kosong', - }); - - setConfirmLoading(false); - return; - } - - // Backend validation schema doesn't include device_code - const payload = { - device_name: FormData.device_name, - is_active: FormData.is_active, - device_location: FormData.device_location, - ip_address: FormData.ip_address, - }; - - // For CREATE: device_description is required (cannot be empty) - // For UPDATE: device_description is optional - if (!FormData.device_id) { - // Creating - ensure description is not empty - payload.device_description = FormData.device_description || '-'; - } else { - // Updating - include description as-is - payload.device_description = FormData.device_description; - } - - console.log('Payload to send:', payload); try { - let response; - if (!FormData.device_id) { - response = await createDevice(payload); - } else { - response = await updateDevice(FormData.device_id, payload); - } + const payload = { + device_name: formData.device_name, + is_active: formData.is_active, + device_location: formData.device_location, + ip_address: formData.ip_address, + }; - console.log('Save Device Response:', response); + const response = !formData.device_id + ? await updateDevice(formData.device_id, payload) + : await createDevice(payload); // Check if response is successful if (response && (response.statusCode === 200 || response.statusCode === 201)) { - // Response.data is now a single object (already extracted from array) - const deviceName = response.data?.device_name || FormData.device_name; + const deviceName = response.data?.device_name || formData.device_name; NotifOk({ icon: 'success', title: 'Berhasil', message: `Data Device "${deviceName}" berhasil ${ - FormData.device_id ? 'diubah' : 'ditambahkan' + formData.device_id ? 'diubah' : 'ditambahkan' }.`, }); @@ -203,7 +95,7 @@ const DetailDevice = (props) => { const handleInputChange = (e) => { const { name, value } = e.target; setFormData({ - ...FormData, + ...formData, [name]: value, }); }; @@ -211,78 +103,22 @@ const DetailDevice = (props) => { const handleStatusToggle = (event) => { const isChecked = event; setFormData({ - ...FormData, + ...formData, is_active: isChecked ? true : false, }); }; - const generateNextDeviceCode = async () => { - try { - const params = new URLSearchParams({ limit: 10000 }); - const response = await getAllDevice(params); - - if (response && response.data && response.data.data) { - const devices = response.data.data; - - if (devices.length === 0) { - setNextDeviceCode('DVC001'); - return; - } - - // Extract numeric part from device codes and find the maximum - const deviceNumbers = devices - .map((device) => { - const match = device.device_code?.match(/dvc(\d+)/i); - return match ? parseInt(match[1], 10) : 0; - }) - .filter((num) => !isNaN(num)); - - const maxNumber = deviceNumbers.length > 0 ? Math.max(...deviceNumbers) : 0; - const nextNumber = maxNumber + 1; - - // Format with leading zeros (DVC001, DVC002, etc.) - const nextCode = `DVC${String(nextNumber).padStart(3, '0')}`; - setNextDeviceCode(nextCode); - } else { - setNextDeviceCode('DVC001'); - } - } catch (error) { - console.error('Error generating next device code:', error); - setNextDeviceCode('Auto-fill'); - } - }; - useEffect(() => { - const token = localStorage.getItem('token'); - if (token) { - if (props.showModal) { - // Only call getDataJenisPermit if permitDefault is enabled - if (props.permitDefault) { - getDataJenisPermit(); - } - - // Generate next device code only for add mode - if (props.actionMode === 'add' && !props.selectedData) { - generateNextDeviceCode(); - } - } - - if (props.selectedData != null) { - setFormData(props.selectedData); - if (props.permitDefault && props.selectedData.jenis_permit_default_arr) { - setCheckedList(props.selectedData.jenis_permit_default_arr); - } - } else { - setFormData(defaultData); - } + if (props.selectedData) { + setFormData(props.selectedData); } else { - // navigate('/signin'); // Uncomment if useNavigate is imported + setFormData(defaultData); } - }, [props.showModal, props.actionMode]); + }, [props.showModal, props.selectedData, props.actionMode]); return ( { , ]} > - {FormData && ( + {formData && (
@@ -355,14 +191,14 @@ const DetailDevice = (props) => { disabled={props.readOnly} style={{ backgroundColor: - FormData.is_active === true ? '#23A55A' : '#bfbfbf', + formData.is_active === true ? '#23A55A' : '#bfbfbf', }} - checked={FormData.is_active === true} + checked={formData.is_active === true} onChange={handleStatusToggle} />
- {FormData.is_active === true ? 'Running' : 'Offline'} + {formData.is_active === true ? 'Running' : 'Offline'}
@@ -371,7 +207,7 @@ const DetailDevice = (props) => { Device ID @@ -381,13 +217,13 @@ const DetailDevice = (props) => { Device Code @@ -396,7 +232,7 @@ const DetailDevice = (props) => { * {
Device Location - * { * { Device Description