From 6be90b6ea91a1a01e7f2cd1916ec2e7d82a6ffb9 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Fri, 17 Oct 2025 15:48:14 +0700 Subject: [PATCH] feat: add unit management functionality with list, detail, and API integration --- src/App.jsx | 2 + src/api/master-unit.jsx | 197 +++++++++++++ src/layout/LayoutMenu.jsx | 6 + src/pages/master/tag/component/DetailTag.jsx | 56 +++- src/pages/master/unit/IndexUnit.jsx | 76 +++++ .../master/unit/component/DetailUnit.jsx | 253 ++++++++++++++++ src/pages/master/unit/component/ListUnit.jsx | 276 ++++++++++++++++++ 7 files changed, 858 insertions(+), 8 deletions(-) create mode 100644 src/api/master-unit.jsx create mode 100644 src/pages/master/unit/IndexUnit.jsx create mode 100644 src/pages/master/unit/component/DetailUnit.jsx create mode 100644 src/pages/master/unit/component/ListUnit.jsx diff --git a/src/App.jsx b/src/App.jsx index 106c0a3..511886c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,6 +12,7 @@ import Blank from './pages/blank/Blank'; // Master import IndexDevice from './pages/master/device/IndexDevice'; import IndexTag from './pages/master/tag/IndexTag'; +import IndexUnit from './pages/master/unit/IndexUnit'; import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; import IndexPlantSection from './pages/master/plantSection/IndexPlantSection'; import IndexStatus from './pages/master/status/IndexStatus'; @@ -54,6 +55,7 @@ const App = () => { }> } /> } /> + } /> } /> } /> } /> diff --git a/src/api/master-unit.jsx b/src/api/master-unit.jsx new file mode 100644 index 0000000..b03c91a --- /dev/null +++ b/src/api/master-unit.jsx @@ -0,0 +1,197 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllUnit = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `unit?${queryParams.toString()}`, + }); + console.log('getAllUnit response:', response); + console.log('Query params:', queryParams.toString()); + + // Check if response has error + if (response.error) { + console.error('getAllUnit error response:', response); + return { + status: response.statusCode || 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: response.message + }; + } + + // Check if backend returns paginated data + if (response.paging) { + const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0; + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: response.paging.current_page || 1, + limit: response.paging.current_limit || 10, + total: totalData, + page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10)) + }, + total: totalData + } + }; + } + + // Fallback: If backend returns all data without pagination + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + // Client-side pagination + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllUnit catch error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getUnitById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `unit/${id}`, + }); + return response.data; +}; + +const createUnit = async (queryParams) => { + // Map frontend fields to backend fields + const backendParams = { + unit_name: queryParams.name, + is_active: queryParams.is_active, + }; + + const response = await SendRequest({ + method: 'post', + prefix: `unit`, + params: backendParams, + }); + console.log('createUnit full response:', response); + console.log('createUnit payload sent:', backendParams); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [unit_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateUnit = async (unit_id, queryParams) => { + // Map frontend fields to backend fields + const backendParams = { + unit_name: queryParams.name, + is_active: queryParams.is_active, + }; + + const response = await SendRequest({ + method: 'put', + prefix: `unit/${unit_id}`, + params: backendParams, + }); + console.log('updateUnit full response:', response); + console.log('updateUnit payload sent:', backendParams); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows, data: [unit_object] } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteUnit = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `unit/${queryParams}`, + }); + console.log('deleteUnit full response:', response); + + // Check if response has error flag + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + // Backend returns: { statusCode, message, rows: null, data: true } + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllUnit, getUnitById, createUnit, updateUnit, deleteUnit }; diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx index 8c847eb..6e0bc57 100644 --- a/src/layout/LayoutMenu.jsx +++ b/src/layout/LayoutMenu.jsx @@ -15,6 +15,7 @@ import { RollbackOutlined, ProductOutlined, TagOutlined, + AppstoreOutlined, MobileOutlined, WarningOutlined, LineChartOutlined, @@ -64,6 +65,11 @@ const allItems = [ icon: , label: Tag, }, + { + key: 'master-unit', + icon: , + label: Unit, + }, { key: 'master-status', icon: , diff --git a/src/pages/master/tag/component/DetailTag.jsx b/src/pages/master/tag/component/DetailTag.jsx index 1e4154c..6da2556 100644 --- a/src/pages/master/tag/component/DetailTag.jsx +++ b/src/pages/master/tag/component/DetailTag.jsx @@ -4,6 +4,7 @@ import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { createTag, updateTag, getAllTag } from '../../../../api/master-tag'; import { getAllDevice } from '../../../../api/master-device'; import { getAllPlantSection } from '../../../../api/master-plant-section'; +import { getAllUnit } from '../../../../api/master-unit'; const { Text } = Typography; @@ -13,6 +14,8 @@ const DetailTag = (props) => { const [loadingDevices, setLoadingDevices] = useState(false); const [plantSubSectionList, setPlantSubSectionList] = useState([]); const [loadingPlantSubSections, setLoadingPlantSubSections] = useState(false); + const [unitList, setUnitList] = useState([]); + const [loadingUnits, setLoadingUnits] = useState(false); const defaultData = { tag_id: '', @@ -290,13 +293,35 @@ const DetailTag = (props) => { } }; + const loadUnits = async () => { + setLoadingUnits(true); + try { + const params = new URLSearchParams({ limit: 1000 }); + const response = await getAllUnit(params); + + if (response && response.data && response.data.data) { + const units = response.data.data; + + // Filter hanya unit yang active (is_active === true) + const activeUnits = units.filter((unit) => unit.is_active === true); + + setUnitList(activeUnits); + } + } catch (error) { + console.error('Error loading units:', error); + } finally { + setLoadingUnits(false); + } + }; + useEffect(() => { const token = localStorage.getItem('token'); if (token) { if (props.showModal) { - // Load devices and plant sub sections when modal opens + // Load devices, plant sub sections, and units when modal opens loadDevices(); loadPlantSubSections(); + loadUnits(); } if (props.selectedData != null) { @@ -514,13 +539,28 @@ const DetailTag = (props) => {
Unit * - +
Plant Sub Section diff --git a/src/pages/master/unit/IndexUnit.jsx b/src/pages/master/unit/IndexUnit.jsx new file mode 100644 index 0000000..31ca52d --- /dev/null +++ b/src/pages/master/unit/IndexUnit.jsx @@ -0,0 +1,76 @@ +import React, { memo, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import ListUnit from './component/ListUnit'; +import DetailUnit from './component/DetailUnit'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; +import { Typography } from 'antd'; + +const { Text } = Typography; + +const IndexUnit = memo(function IndexUnit() { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + const [readOnly, setReadOnly] = useState(false); + const [showModal, setShowmodal] = useState(false); + + const setMode = (param) => { + setActionMode(param); + switch (param) { + case 'add': + setReadOnly(false); + setShowmodal(true); + break; + + case 'edit': + setReadOnly(false); + setShowmodal(true); + break; + + case 'preview': + setReadOnly(true); + setShowmodal(true); + break; + + default: + setShowmodal(false); + break; + } + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setBreadcrumbItems([ + { title: • Master }, + { title: Unit } + ]); + } else { + navigate('/signin'); + } + }, []); + + return ( + + + + + ); +}); + +export default IndexUnit; diff --git a/src/pages/master/unit/component/DetailUnit.jsx b/src/pages/master/unit/component/DetailUnit.jsx new file mode 100644 index 0000000..6f5bc53 --- /dev/null +++ b/src/pages/master/unit/component/DetailUnit.jsx @@ -0,0 +1,253 @@ +import { useEffect, useState } from 'react'; +import { Modal, Input, Typography, Button, ConfigProvider, Switch } from 'antd'; +import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; +import { createUnit, updateUnit } from '../../../../api/master-unit'; + +const { Text } = Typography; + +const DetailUnit = (props) => { + const [confirmLoading, setConfirmLoading] = useState(false); + + const defaultData = { + unit_id: '', + unit_code: '', + unit_name: '', + is_active: true, + }; + + const [FormData, setFormData] = useState(defaultData); + + const handleCancel = () => { + props.setSelectedData(null); + props.setActionMode('list'); + }; + + const handleSave = async () => { + setConfirmLoading(true); + + // Validasi required fields + if (!FormData.unit_name || FormData.unit_name.trim() === '') { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: 'Kolom Name Tidak Boleh Kosong', + }); + setConfirmLoading(false); + return; + } + + try { + if (FormData.unit_id) { + // Update existing unit + const payload = { + name: FormData.unit_name, + is_active: FormData.is_active, + }; + + const response = await updateUnit(FormData.unit_id, payload); + console.log('updateUnit response:', response); + + if (response.statusCode === 200) { + // Get updated data to show unit_code in notification + const unitCode = response.data?.unit_code || FormData.unit_code; + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Unit "${unitCode} - ${FormData.unit_name}" berhasil diubah.`, + }); + props.setActionMode('list'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response.message || 'Gagal mengubah data Unit.', + }); + } + } else { + // Create new unit + const payload = { + name: FormData.unit_name, + is_active: FormData.is_active, + }; + + const response = await createUnit(payload); + console.log('createUnit response:', response); + + if (response.statusCode === 200 || response.statusCode === 201) { + // Get unit_code from response + const unitCode = response.data?.unit_code || 'N/A'; + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Unit "${unitCode} - ${FormData.unit_name}" berhasil ditambahkan.`, + }); + props.setActionMode('list'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response.message || 'Gagal menambahkan data Unit.', + }); + } + } + } catch (error) { + console.error('Save Unit Error:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: error.message || 'Terjadi kesalahan saat menyimpan data.', + }); + } + + setConfirmLoading(false); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...FormData, + [name]: value, + }); + }; + + const handleStatusToggle = (isChecked) => { + setFormData({ + ...FormData, + is_active: isChecked, + }); + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + if (props.selectedData != null) { + // Only set fields that are in defaultData + const filteredData = { + unit_id: props.selectedData.unit_id || '', + unit_code: props.selectedData.unit_code || '', + unit_name: props.selectedData.unit_name || '', + is_active: props.selectedData.is_active ?? true, + }; + setFormData(filteredData); + } else { + setFormData(defaultData); + } + } + }, [props.showModal]); + + return ( + + + + + + {!props.readOnly && ( + + )} + + , + ]} + > + {FormData && ( +
+ {/* Status Toggle */} +
+
+ Status +
+
+
+ +
+
+ + {FormData.is_active === true ? 'Active' : 'Inactive'} + +
+
+
+ {/* Unit Code - Display only for edit/preview */} + {FormData.unit_code && ( +
+ Unit Code + +
+ )} +
+ Name + * + +
+
+ )} +
+ ); +}; + +export default DetailUnit; diff --git a/src/pages/master/unit/component/ListUnit.jsx b/src/pages/master/unit/component/ListUnit.jsx new file mode 100644 index 0000000..a640566 --- /dev/null +++ b/src/pages/master/unit/component/ListUnit.jsx @@ -0,0 +1,276 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input } from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + EyeOutlined, + SearchOutlined, +} from '@ant-design/icons'; +import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; +import { useNavigate } from 'react-router-dom'; +import TableList from '../../../../components/Global/TableList'; +import { getAllUnit, deleteUnit } from '../../../../api/master-unit'; + +const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Unit Code', + dataIndex: 'unit_code', + key: 'unit_code', + width: '20%', + }, + { + title: 'Name', + dataIndex: 'unit_name', + key: 'unit_name', + width: '20%', + }, + { + 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" + /> + + + + + + + + + + + + + + + + + ); +}); + +export default ListUnit;