diff --git a/src/App.jsx b/src/App.jsx index d3f9c69..6bf096f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,6 +15,10 @@ import IndexTag from './pages/master/tag/IndexTag'; import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; import IndexPlantSection from './pages/master/plantSection/IndexPlantSection'; import IndexStatus from './pages/master/status/IndexStatus'; +import IndexShift from './pages/master/shift/IndexShift'; + +// Jadwal Shift +import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift'; // History import IndexTrending from './pages/history/trending/IndexTrending'; @@ -53,10 +57,14 @@ const App = () => { } /> } /> } /> - + } /> } /> + }> + } /> + + }> } /> } /> diff --git a/src/api/jadwal-shift.jsx b/src/api/jadwal-shift.jsx new file mode 100644 index 0000000..2ae403a --- /dev/null +++ b/src/api/jadwal-shift.jsx @@ -0,0 +1,67 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllJadwalShift = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `jadwal-shift?${queryParams.toString()}`, + }); + return response; + } catch (error) { + console.error('getAllJadwalShift error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const createJadwalShift = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `jadwal-shift`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const updateJadwalShift = async (id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `jadwal-shift/${id}`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const deleteJadwalShift = async (id) => { + const response = await SendRequest({ + method: 'delete', + prefix: `jadwal-shift/${id}`, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +export { getAllJadwalShift, createJadwalShift, updateJadwalShift, deleteJadwalShift }; diff --git a/src/api/master-shift.jsx b/src/api/master-shift.jsx new file mode 100644 index 0000000..a5a3f24 --- /dev/null +++ b/src/api/master-shift.jsx @@ -0,0 +1,75 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllShift = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `shift?${queryParams.toString()}`, + }); + return response; + } catch (error) { + console.error('getAllShift error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getShiftById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `shift/${id}`, + }); + return response.data; +}; + +const createShift = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `shift`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const updateShift = async (id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `shift/${id}`, + data: queryParams, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +const deleteShift = async (id) => { + const response = await SendRequest({ + method: 'delete', + prefix: `shift/${id}`, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message + }; +}; + +export { getAllShift, getShiftById, createShift, updateShift, deleteShift }; diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx index 28cf0ad..97f044f 100644 --- a/src/layout/LayoutMenu.jsx +++ b/src/layout/LayoutMenu.jsx @@ -70,6 +70,11 @@ const allItems = [ icon: , label: Status, }, + { + key: 'master-shift', + icon: , + label: Shift, + }, ], }, { diff --git a/src/pages/jadwalShift/IndexJadwalShift.jsx b/src/pages/jadwalShift/IndexJadwalShift.jsx new file mode 100644 index 0000000..36e2fc8 --- /dev/null +++ b/src/pages/jadwalShift/IndexJadwalShift.jsx @@ -0,0 +1,28 @@ +import { useState } from 'react'; +import ListJadwalShift from './component/ListJadwalShift'; +import DetailJadwalShift from './component/DetailJadwalShift'; + +const IndexJadwalShift = () => { + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + + return ( + <> + {actionMode === 'list' && ( + + )} + {(actionMode === 'add' || actionMode === 'edit') && ( + + )} + + ); +}; + +export default IndexJadwalShift; diff --git a/src/pages/jadwalShift/component/DetailJadwalShift.jsx b/src/pages/jadwalShift/component/DetailJadwalShift.jsx new file mode 100644 index 0000000..1e11da8 --- /dev/null +++ b/src/pages/jadwalShift/component/DetailJadwalShift.jsx @@ -0,0 +1,210 @@ +import { useEffect, useState } from 'react'; +import { Modal, Select, Divider, Typography, Button, ConfigProvider } from 'antd'; +import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { createJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift'; +import { getAllShift } from '../../../api/master-shift'; +import { getAllUser } from '../../../api/user'; + +const { Text } = Typography; +const { Option } = Select; + +const DetailJadwalShift = (props) => { + const [confirmLoading, setConfirmLoading] = useState(false); + const [shifts, setShifts] = useState([]); + const [users, setUsers] = useState([]); + + const defaultData = { + id: '', + id_shift: null, + id_user: null, + }; + + const [FormData, setFormData] = useState(defaultData); + + const fetchShifts = async () => { + const response = await getAllShift(new URLSearchParams()); + setShifts(response.data.data); + }; + + const fetchUsers = async () => { + const response = await getAllUser(new URLSearchParams("limit=1000")); // Fetch all users + setUsers(response.data.data); + }; + + useEffect(() => { + fetchShifts(); + fetchUsers(); + }, []); + + const handleCancel = () => { + props.setSelectedData(null); + props.setActionMode('list'); + }; + + const handleSave = async () => { + setConfirmLoading(true); + + if (!FormData.id_shift) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Shift Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + if (!FormData.id_user) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom User Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + const payload = { + id_shift: FormData.id_shift, + id_user: FormData.id_user, + }; + + try { + const response = FormData.id + ? await updateJadwalShift(FormData.id, payload) + : await createJadwalShift(payload); + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Jadwal Shift berhasil ${FormData.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 Jadwal Shift Error:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', + }); + } + + setConfirmLoading(false); + }; + + const handleSelectChange = (name, value) => { + setFormData({ + ...FormData, + [name]: value, + }); + }; + + useEffect(() => { + if (props.selectedData) { + setFormData(props.selectedData); + } else { + setFormData(defaultData); + } + }, [props.actionMode, props.selectedData]); + + return ( + + + + + + + + , + ]} + > + {FormData && ( +
+
+ Shift + * + +
+
+ User + * + +
+
+ )} +
+ ); +}; + +export default DetailJadwalShift; diff --git a/src/pages/jadwalShift/component/ListJadwalShift.jsx b/src/pages/jadwalShift/component/ListJadwalShift.jsx new file mode 100644 index 0000000..641a33c --- /dev/null +++ b/src/pages/jadwalShift/component/ListJadwalShift.jsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react'; +import { Button, Table, Space, Popconfirm } from 'antd'; +import { getAllJadwalShift, deleteJadwalShift } from '../../../api/jadwal-shift'; +import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; + +const ListJadwalShift = (props) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchData = async () => { + setLoading(true); + try { + const response = await getAllJadwalShift(new URLSearchParams()); + if (response.data && response.data.data) { + setData(response.data.data); + } else { + setData([]); + } + } catch (error) { + console.error("Failed to fetch shift schedule data:", error); + setData([]); // Set data to empty array on error + } finally { + setLoading(false); + } + }; + + const handleAdd = () => { + props.setSelectedData(null); + props.setActionMode('add'); + }; + + const handleEdit = (record) => { + props.setSelectedData(record); + props.setActionMode('edit'); + }; + + const handleDelete = async (id) => { + try { + const response = await deleteJadwalShift(id); + if (response.statusCode === 200) { + NotifOk({ icon: 'success', title: 'Berhasil', message: 'Data jadwal shift berhasil dihapus' }); + fetchData(); + } else { + NotifAlert({ icon: 'error', title: 'Gagal', message: response.message }); + } + } catch (error) { + NotifAlert({ icon: 'error', title: 'Error', message: error.message }); + } + }; + + useEffect(() => { + if(props.actionMode === 'list') { + fetchData(); + } + }, [props.actionMode]); + + const columns = [ + { + title: 'Nama Shift', + dataIndex: 'nama_shift', + key: 'nama_shift', + }, + { + title: 'Jam Shift', + dataIndex: 'jam_shift', + key: 'jam_shift', + }, + { + title: 'Username', + dataIndex: 'username', + key: 'username', + }, + { + title: 'Nama Employee', + dataIndex: 'nama_employee', + key: 'nama_employee', + }, + { + title: 'WhatsApp', + dataIndex: 'whatsapp', + key: 'whatsapp', + }, + { + title: 'Aksi', + key: 'action', + render: (text, record) => ( + + + handleDelete(record.id)} + okText="Yes" + cancelText="No" + > + + + + ), + }, + ]; + + return ( +
+ + + + ); +}; + +export default ListJadwalShift; diff --git a/src/pages/master/shift/IndexShift.jsx b/src/pages/master/shift/IndexShift.jsx new file mode 100644 index 0000000..bb67155 --- /dev/null +++ b/src/pages/master/shift/IndexShift.jsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import ListShift from './component/ListShift'; +import DetailShift from './component/DetailShift'; + +const IndexShift = () => { + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + + 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 new file mode 100644 index 0000000..1ca4d31 --- /dev/null +++ b/src/pages/master/shift/component/DetailShift.jsx @@ -0,0 +1,142 @@ +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'; + +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 + }; + + const [FormData, setFormData] = useState(defaultData); + + const handleCancel = () => { + props.setSelectedData(null); + props.setActionMode('list'); + }; + + const handleSave = async () => { + setConfirmLoading(true); + + if (!FormData.nama_shift) { + 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' }); + setConfirmLoading(false); + return; + } + + const payload = { + nama_shift: FormData.nama_shift, + jam_shift: FormData.jam_shift, + status: FormData.status, + }; + + try { + const response = FormData.id + ? await updateShift(FormData.id, payload) + : await createShift(payload); + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Shift "${FormData.nama_shift}" berhasil ${FormData.id ? 'diubah' : 'ditambahkan'}.`, + }); + props.setActionMode('list'); + } else { + NotifAlert({ icon: 'error', title: 'Gagal', message: response?.message || 'Terjadi kesalahan saat menyimpan data.' }); + } + } catch (error) { + NotifAlert({ icon: 'error', title: 'Error', message: error.message || 'Terjadi kesalahan pada server.' }); + } + + setConfirmLoading(false); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ ...FormData, [name]: value }); + }; + + const handleStatusToggle = (checked) => { + setFormData({ ...FormData, status: checked }); + }; + + useEffect(() => { + if (props.selectedData) { + setFormData(props.selectedData); + } else { + setFormData(defaultData); + } + }, [props.actionMode, props.selectedData]); + + return ( + Batal, + !readOnly && ( + + ), + ]} + > + {FormData && ( +
+
+ Status +
+ + {FormData.status ? 'Active' : 'Inactive'} +
+
+ +
+ Nama Shift + * + +
+
+ Jam Shift + * + +
+
+ )} +
+ ); +}; + +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 new file mode 100644 index 0000000..ab20b52 --- /dev/null +++ b/src/pages/master/shift/component/ListShift.jsx @@ -0,0 +1,181 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Button, Col, Row, Input, ConfigProvider, Card, Tag } from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + SearchOutlined, + EyeOutlined, +} from '@ant-design/icons'; +import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; +import { useNavigate } from 'react-router-dom'; +import TableList from '../../../../components/Global/TableList'; +import { getAllShift, deleteShift } from '../../../../api/master-shift'; + +const ListShift = (props) => { + const [trigerFilter, setTrigerFilter] = useState(false); + const defaultFilter = { search: '' }; + const [formDataFilter, setFormDataFilter] = useState(defaultFilter); + const [searchValue, setSearchValue] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (!token) { + navigate('/signin'); + } + }, [navigate]); + + const doFilter = () => { + setTrigerFilter((prev) => !prev); + }; + + const handleSearch = (value) => { + setFormDataFilter({ search: value }); + doFilter(); + }; + + const handleSearchClear = () => { + setSearchValue(''); + setFormDataFilter(defaultFilter); + doFilter(); + }; + + const showPreviewModal = (record) => { + props.setSelectedData(record); + props.setActionMode('preview'); + }; + + const showEditModal = (record) => { + props.setSelectedData(record); + props.setActionMode('edit'); + }; + + const showAddModal = () => { + props.setSelectedData(null); + props.setActionMode('add'); + }; + + const showDeleteDialog = (record) => { + NotifConfirmDialog({ + icon: 'question', + title: 'Konfirmasi', + message: `Apakah anda yakin ingin menghapus data shift "${record.nama_shift}"?`, + onConfirm: () => handleDelete(record.id), + }); + }; + + const handleDelete = async (id) => { + try { + const response = await deleteShift(id); + if (response.statusCode === 200) { + NotifAlert({ icon: 'success', title: 'Berhasil', message: 'Data shift berhasil dihapus' }); + doFilter(); + } else { + NotifAlert({ icon: 'error', title: 'Gagal', message: response.message }); + } + } catch (error) { + NotifAlert({ icon: 'error', title: 'Error', message: error.message }); + } + }; + + const columns = [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Nama Shift', + dataIndex: 'nama_shift', + key: 'nama_shift', + }, + { + title: 'Jam Shift', + dataIndex: 'jam_shift', + key: 'jam_shift', + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + align: 'center', + render: (status) => ( + + {status ? 'Active' : 'Inactive'} + + ), + }, + { + title: 'Aksi', + key: 'action', + align: 'center', + width: '15%', + render: (_, record) => ( + +
+ setSearchValue(e.target.value)} + onSearch={handleSearch} + allowClear={{ + clearIcon: x, + }} + enterButton={} + size="large" + /> + + + + + + + + + + + + + + ); +}; + +export default memo(ListShift); \ No newline at end of file