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) => (
+
+ }
+ onClick={() => showPreviewModal(record)}
+ style={{
+ color: '#1890ff',
+ borderColor: '#1890ff',
+ }}
+ />
+ }
+ onClick={() => showEditModal(record)}
+ style={{
+ color: '#faad14',
+ borderColor: '#faad14',
+ }}
+ />
+ }
+ onClick={() => showDeleteDialog(record)}
+ style={{
+ borderColor: '#ff4d4f',
+ }}
+ />
+
+ ),
+ },
+];
+
+const ListUnit = memo(function ListUnit(props) {
+ const [trigerFilter, setTrigerFilter] = useState(false);
+
+ const defaultFilter = {
+ criteria: '',
+ };
+ const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
+ const [searchValue, setSearchValue] = useState('');
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ if (props.actionMode == 'list') {
+ doFilter();
+ }
+ } else {
+ navigate('/signin');
+ }
+ }, [props.actionMode]);
+
+ const doFilter = () => {
+ setTrigerFilter((prev) => !prev);
+ };
+
+ const handleSearch = () => {
+ setFormDataFilter((prev) => ({ ...prev, criteria: searchValue }));
+ doFilter();
+ };
+
+ const handleSearchClear = () => {
+ setSearchValue('');
+ setFormDataFilter((prev) => ({ ...prev, criteria: '' }));
+ doFilter();
+ };
+
+ const showPreviewModal = (param) => {
+ props.setSelectedData(param);
+ props.setActionMode('preview');
+ };
+
+ const showEditModal = (param = null) => {
+ props.setSelectedData(param);
+ props.setActionMode('edit');
+ };
+
+ const showAddModal = (param = null) => {
+ props.setSelectedData(param);
+ props.setActionMode('add');
+ };
+
+ const showDeleteDialog = (param) => {
+ NotifConfirmDialog({
+ icon: 'question',
+ title: 'Konfirmasi',
+ message: `Apakah anda yakin hapus data "${param.unit_code} - ${param.unit_name}" ?`,
+ onConfirm: () => handleDelete(param),
+ onCancel: () => props.setSelectedData(null),
+ });
+ };
+
+ const handleDelete = async (param) => {
+ try {
+ const response = await deleteUnit(param.unit_id);
+ console.log('deleteUnit response:', response);
+
+ if (response.statusCode === 200) {
+ NotifAlert({
+ icon: 'success',
+ title: 'Berhasil',
+ message: `Data Unit "${param.unit_code} - ${param.unit_name}" berhasil dihapus.`,
+ });
+ // Refresh table
+ doFilter();
+ } else {
+ NotifAlert({
+ icon: 'error',
+ title: 'Gagal',
+ message: response.message || 'Gagal menghapus data Unit.',
+ });
+ }
+ } catch (error) {
+ console.error('Delete Unit Error:', error);
+ NotifAlert({
+ icon: 'error',
+ title: 'Error',
+ message: error.message || 'Terjadi kesalahan saat menghapus data.',
+ });
+ }
+ };
+
+ // Function untuk dipanggil dari DetailUnit setelah create/update
+ const refreshData = () => {
+ doFilter();
+ };
+
+ // Pass refresh function to props
+ if (props.setRefreshData) {
+ props.setRefreshData(refreshData);
+ }
+
+ return (
+
+
+
+
+
+
+ {
+ const value = e.target.value;
+ setSearchValue(value);
+ // Auto search when clearing by backspace/delete
+ if (value === '') {
+ handleSearchClear();
+ }
+ }}
+ onSearch={handleSearch}
+ allowClear
+ onClear={handleSearchClear}
+ enterButton={
+ }
+ style={{
+ backgroundColor: '#23A55A',
+ borderColor: '#23A55A',
+ }}
+ >
+ Search
+
+ }
+ size="large"
+ />
+
+
+
+
+ }
+ onClick={() => showAddModal()}
+ size="large"
+ >
+ Tambah Data
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+export default ListUnit;