From 988dcda0e21a196109f9f045f5aeb2ef298f539e Mon Sep 17 00:00:00 2001 From: Iqbal Rizqi Kurniawan Date: Wed, 22 Oct 2025 14:24:52 +0700 Subject: [PATCH] feat: enhance DetailShift and ListShift components with improved validation and UI updates --- .../master/shift/component/DetailShift.jsx | 413 ++++-------------- .../master/shift/component/ListShift.jsx | 290 +++++------- .../master/status/component/DetailStatus.jsx | 56 +-- src/pages/role/component/DetailRole.jsx | 116 ++--- 4 files changed, 268 insertions(+), 607 deletions(-) diff --git a/src/pages/master/shift/component/DetailShift.jsx b/src/pages/master/shift/component/DetailShift.jsx index 86d1b8d..d4df499 100644 --- a/src/pages/master/shift/component/DetailShift.jsx +++ b/src/pages/master/shift/component/DetailShift.jsx @@ -1,13 +1,14 @@ -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 React, { useEffect, useState } from 'react'; +import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider, TimePicker, Space } from 'antd'; +import { NotifOk } from '../../../../components/Global/ToastNotif'; import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -dayjs.extend(utc); +// Mock API calls for demonstration +const createShift = async (payload) => ({ statusCode: 201, data: { ...payload, shift_id: Date.now() } }); +const updateShift = async (id, payload) => ({ statusCode: 200, data: { ...payload, shift_id: id } }); const { Text } = Typography; +const timeFormat = 'HH:mm'; const DetailShift = (props) => { const [confirmLoading, setConfirmLoading] = useState(false); @@ -15,12 +16,12 @@ const DetailShift = (props) => { const defaultData = { shift_id: '', shift_name: '', - start_time: '', - end_time: '', + start_time: '08:00', + end_time: '16:00', is_active: true, }; - const [FormData, setFormData] = useState(defaultData); + const [formData, setFormData] = useState(defaultData); const handleCancel = () => { props.setSelectedData(null); @@ -30,349 +31,125 @@ const DetailShift = (props) => { const handleSave = async () => { setConfirmLoading(true); - // 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.start_time || FormData.start_time.trim() === '') { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Jam Mulai Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - 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)', - }); + if (!formData.shift_name) { + NotifOk({ icon: 'warning', title: 'Peringatan', message: 'Nama Shift wajib diisi.' }); setConfirmLoading(false); return; } try { - 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 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); + const response = + props.actionMode === 'edit' + ? await updateShift(formData.shift_id, payload) + : await createShift(payload); - 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.', - }); - } + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ icon: 'success', title: 'Berhasil', message: `Data Shift berhasil disimpan.` }); + props.setActionMode('list'); } else { - // 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: 'error', title: 'Gagal', message: response?.message || 'Gagal menyimpan data.' }); } } catch (error) { - console.error('Save Shift Error:', error); - NotifAlert({ - icon: 'error', - title: 'Error', - message: error.message || 'Terjadi kesalahan saat menyimpan data.', - }); + NotifOk({ icon: 'error', title: 'Error', message: error.message || 'Terjadi kesalahan server.' }); + } finally { + setConfirmLoading(false); } - - setConfirmLoading(false); - }; - - // 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 handleInputChange = (e) => { const { name, value } = e.target; - - // Just set the value without formatting during typing - setFormData({ - ...FormData, - [name]: value, - }); + 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 using dayjs - const extractTime = (timeString) => { - if (!timeString) return ''; - - // If it's ISO timestamp like "1970-01-01T08:00:00.000Z" - if (timeString.includes('T')) { - return dayjs.utc(timeString).format('HH:mm'); - } - - // 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; + const handleTimeChange = (time, timeString, field) => { + setFormData({ ...formData, [field]: timeString }); }; useEffect(() => { - 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); - } + if (props.selectedData) { + setFormData(props.selectedData); + } else { + setFormData(defaultData); } - }, [props.showModal]); + }, [props.showModal, props.selectedData]); + + const modalTitle = `${props.actionMode === 'add' ? 'Tambah' : props.actionMode === 'preview' ? 'Preview' : 'Edit'} Shift`; return ( - - - - - {!props.readOnly && ( - - )} - - , + + + {!props.readOnly && ( + + )} + , ]} > - {FormData && ( +
- {/* Status Toggle */} -
-
- Status -
-
-
- -
-
- {FormData.is_active === true ? 'Active' : 'Inactive'} -
-
-
-
- Nama Shift - * - Status +
+ setFormData({ ...formData, is_active: checked })} /> -
-
- Jam Mulai - * - - - Contoh: 08:00 atau 08:00:00 - -
-
- Jam Selesai - * - - - Contoh: 17:00 atau 17:00:00 - + {formData.is_active ? 'Active' : 'Inactive'}
- )} + + +
+ Nama Shift + * + +
+ +
+ Waktu Shift + * + + handleTimeChange(time, timeString, 'start_time')} + style={{ width: '50%' }} + placeholder="Waktu Mulai" + disabled={props.readOnly} + /> + handleTimeChange(time, time-string, 'end_time')} + style={{ width: '50%' }} + placeholder="Waktu Selesai" + disabled={props.readOnly} + /> + +
+
); }; -export default DetailShift; +export default DetailShift; \ No newline at end of file diff --git a/src/pages/master/shift/component/ListShift.jsx b/src/pages/master/shift/component/ListShift.jsx index 1a2e07e..0ee09e5 100644 --- a/src/pages/master/shift/component/ListShift.jsx +++ b/src/pages/master/shift/component/ListShift.jsx @@ -10,62 +10,46 @@ import { 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'; +// import { getAllShift, deleteShift } from '../../../../api/master-shift'; // <-- API needs to be created -// 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; -}; +// Mock API calls for demonstration +const getAllShift = async () => ({ + data: [ + { shift_id: 1, shift_name: 'Pagi', start_time: '08:00', end_time: '16:00', is_active: true }, + { shift_id: 2, shift_name: 'Sore', start_time: '16:00', end_time: '00:00', is_active: true }, + { shift_id: 3, shift_name: 'Malam', start_time: '00:00', end_time: '08:00', is_active: false }, + ], + statusCode: 200, +}); +const deleteShift = async (id) => ({ statusCode: 200, message: 'Data berhasil dihapus' }); const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ { - title: 'No', - key: 'no', - width: '5%', - align: 'center', - render: (_, __, index) => index + 1, - }, - { - title: 'Nama Shift', + title: 'Shift Name', dataIndex: 'shift_name', key: 'shift_name', - width: '20%', + width: '30%', + render: (text, record, index) => `${index + 1}. ${text}`, }, { - title: 'Jam Mulai', + title: 'Start Time', dataIndex: 'start_time', key: 'start_time', width: '15%', - render: (time) => extractTime(time), + align: 'center', }, { - title: 'Jam Selesai', + title: 'End Time', dataIndex: 'end_time', key: 'end_time', width: '15%', - render: (time) => extractTime(time), + align: 'center', }, { title: 'Status', dataIndex: 'is_active', key: 'is_active', - width: '10%', + width: '15%', align: 'center', render: (_, { is_active }) => { const color = is_active ? 'green' : 'red'; @@ -81,36 +65,12 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ title: 'Aksi', key: 'aksi', align: 'center', - width: '20%', + width: '25%', render: (_, record) => ( - + + + + + + { + const value = e.target.value; + setSearchValue(value); + // Auto search when clearing by backspace/delete + if (value === '') { + handleSearchClear(); } - size="large" - /> - - - - } + style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }} > - - - - - - - - - - - - + Search + + } + size="large" + /> + + + + + + + + + + + + + + + ); }); diff --git a/src/pages/master/status/component/DetailStatus.jsx b/src/pages/master/status/component/DetailStatus.jsx index 85d3410..167dbab 100644 --- a/src/pages/master/status/component/DetailStatus.jsx +++ b/src/pages/master/status/component/DetailStatus.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Switch } from 'antd'; +import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Switch, Row, Col } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { validateRun } from '../../../../Utils/validate'; import { createStatus, updateStatus } from '../../../../api/master-status'; @@ -46,7 +46,6 @@ const DetailStatus = (props) => { { field: 'status_number', label: 'Status Number', required: true }, { field: 'status_name', label: 'Status Name', required: true }, { field: 'status_color', label: 'Status Color', required: true }, - { field: 'status_description', label: 'Description', required: true }, ]; if ( @@ -145,29 +144,35 @@ const DetailStatus = (props) => { {formData.is_active ? 'Active' : 'Inactive'}
-
- Status Number - * - -
-
- Status Name - * - -
+ + +
+ Status Number + * + +
+ + +
+ Status Name + * + +
+ +
Status Color * @@ -181,7 +186,6 @@ const DetailStatus = (props) => {
Description - *