feat: enhance status management with validation and improved UI components

This commit is contained in:
2025-10-21 23:05:39 +07:00
parent 5ec26ecbe8
commit 9091392dfb
4 changed files with 379 additions and 359 deletions

View File

@@ -5,87 +5,23 @@ import { Typography } from 'antd';
import ListStatus from './component/ListStatus'; import ListStatus from './component/ListStatus';
import DetailStatus from './component/DetailStatus'; import DetailStatus from './component/DetailStatus';
import { NotifConfirmDialog, NotifAlert } from '../../../components/Global/ToastNotif';
const { Text } = Typography; const { Text } = Typography;
// Mock Data
const initialData = [
{
key: '3',
statusCode: 3,
statusName: 'Done',
description: 'Indicates that the process is complete.',
},
{
key: '1',
statusCode: 1,
statusName: 'Warning',
description: 'Indicates a warning condition.',
},
{
key: '2',
statusCode: 2,
statusName: 'Alarm',
description: 'Indicates an alarm condition.',
},
{
key: '4',
statusCode: 4,
statusName: 'Critical',
description: 'Indicates a critical condition.',
},
];
const IndexStatus = memo(function IndexStatus() { const IndexStatus = memo(function IndexStatus() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
const [data, setData] = useState(initialData);
const [actionMode, setActionMode] = useState('list'); const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null); const [selectedData, setSelectedData] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [readOnly, setReadOnly] = useState(false); const [readOnly, setReadOnly] = useState(false);
// Mock API function
const getAllStatus = async (params) => {
const { page = 1, limit = 10, search = '' } = Object.fromEntries(params.entries());
let filteredData = data;
if (search) {
filteredData = data.filter(item =>
item.statusName.toLowerCase().includes(search.toLowerCase())
);
}
const start = (page - 1) * limit;
const end = start + limit;
const paginatedData = filteredData.slice(start, end);
return new Promise(resolve => {
setTimeout(() => {
resolve({
status: 200,
data: {
data: paginatedData,
total: filteredData.length,
paging: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredData.length,
},
},
});
}, 500);
});
};
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
setBreadcrumbItems([ setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> }, { title: <Text strong> Master</Text> },
{ title: <Text strong style={{ fontSize: '14px' }}>Status</Text> } { title: <Text strong>Status</Text> }
]); ]);
} else { } else {
navigate('/signin'); navigate('/signin');
@@ -101,64 +37,21 @@ const IndexStatus = memo(function IndexStatus() {
} }
}, [actionMode]); }, [actionMode]);
const handleDataSaved = (values) => {
let newData = [...data];
if (values.key) { // Editing
const index = newData.findIndex((item) => values.key === item.key);
if (index > -1) {
newData.splice(index, 1, values);
}
} else { // Adding
const newKey = (Math.max(...data.map(item => parseInt(item.key))) + 1).toString();
newData = [{ key: newKey, ...values }, ...newData];
}
setData(newData);
};
const handleEdit = (record) => {
setSelectedData(record);
setActionMode('edit');
};
const handlePreview = (record) => {
setSelectedData(record);
setActionMode('preview');
};
const handleDelete = (record) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi',
message: `Apakah anda yakin ingin menghapus status "${record.statusName}"?`,
onConfirm: () => {
const newData = data.filter((item) => item.key !== record.key);
setData(newData);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Status "${record.statusName}" berhasil dihapus.`,
});
},
});
};
return ( return (
<React.Fragment> <React.Fragment>
{actionMode === 'list' &&
<ListStatus <ListStatus
setActionMode={setActionMode} setActionMode={setActionMode}
handleEdit={handleEdit} setSelectedData={setSelectedData}
handleDelete={handleDelete} actionMode={actionMode}
handlePreview={handlePreview}
getAllStatus={getAllStatus}
data={data}
/> />
}
<DetailStatus <DetailStatus
showModal={isModalVisible} showModal={isModalVisible}
setActionMode={setActionMode} setActionMode={setActionMode}
selectedData={selectedData} selectedData={selectedData}
readOnly={readOnly}
onDataSaved={handleDataSaved}
setSelectedData={setSelectedData} setSelectedData={setSelectedData}
readOnly={readOnly}
/> />
</React.Fragment> </React.Fragment>
); );

View File

@@ -1,66 +1,113 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Form } from 'antd'; import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Switch } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { validateRun } from '../../../../Utils/validate';
import { createStatus, updateStatus } from '../../../../api/master-status';
const { Text } = Typography; const { Text } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const DetailStatus = (props) => { const DetailStatus = (props) => {
const [form] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const defaultData = { const defaultData = {
key: '', status_id: '',
statusCode: '', status_number: null,
statusName: '', status_name: '',
description: '', status_color: '',
status_description: '',
is_active: true,
}; };
const [FormData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleInputNumberChange = (value) => {
setFormData({ ...formData, status_number: value });
};
const handleStatusToggle = (checked) => {
setFormData({ ...formData, is_active: checked });
};
const handleCancel = () => { const handleCancel = () => {
props.setSelectedData(null); props.setSelectedData(null);
props.setActionMode('list'); props.setActionMode('list');
form.resetFields();
}; };
const handleSave = async () => { const handleSave = async () => {
try {
const values = await form.validateFields();
setConfirmLoading(true); setConfirmLoading(true);
const validationRules = [
{ 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 (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
) {
return;
}
try {
const payload = { const payload = {
key: FormData.key, status_number: formData.status_number,
...values, status_name: formData.status_name,
status_color: formData.status_color,
status_description: formData.status_description,
is_active: formData.is_active,
}; };
props.onDataSaved(payload); const response = formData.status_id
? await updateStatus(formData.status_id, payload)
: await createStatus(payload);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const action = formData.status_id ? 'diubah' : 'ditambahkan';
NotifOk({ NotifOk({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
message: `Data Status "${payload.statusName}" berhasil ${ message: `Data Status "${payload.status_name}" berhasil ${action}.`,
payload.key ? 'diubah' : 'ditambahkan'
}.`,
}); });
setConfirmLoading(false);
props.setActionMode('list'); props.setActionMode('list');
form.resetFields(); } else {
} catch (errorInfo) { NotifAlert({
console.log('Failed:', errorInfo); icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menyimpan data.',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server.',
});
} finally {
setConfirmLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
if (props.selectedData) { if (props.selectedData) {
setFormData(props.selectedData); setFormData({ ...defaultData, ...props.selectedData });
form.setFieldsValue(props.selectedData);
} else { } else {
setFormData(defaultData); setFormData(defaultData);
form.resetFields();
} }
}, [props.showModal, props.selectedData, form]); }, [props.showModal, props.selectedData]);
return ( return (
<Modal <Modal
@@ -78,81 +125,72 @@ const DetailStatus = (props) => {
footer={ footer={
!props.readOnly && ( !props.readOnly && (
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px' }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px' }}>
<ConfigProvider
theme={{
token: { colorPrimary: '#23A55A' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: 'white',
defaultHoverBg: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button> <Button onClick={handleCancel}>Batal</Button>
</ConfigProvider>
<ConfigProvider
theme={{
token: { colorPrimary: '#23A55A' },
components: {
Button: {
defaultBg: '#23A55A',
defaultColor: 'white',
defaultBorderColor: '#23A55A',
defaultHoverColor: 'white',
defaultHoverBg: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button type="primary" loading={confirmLoading} onClick={handleSave}> <Button type="primary" loading={confirmLoading} onClick={handleSave}>
Simpan Simpan
</Button> </Button>
</ConfigProvider>
</div> </div>
) )
} }
> >
<Divider /> <Divider />
<Form form={form} layout="vertical" name="detailStatusForm"> <div style={{ marginBottom: 12 }}>
<Form.Item <Text strong>Status</Text>
name="statusCode" <div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
label={<Text strong>Status Code</Text>} <Switch
rules={[{ required: true, message: 'Silakan masukkan kode status!' }]} disabled={props.readOnly}
> checked={formData.is_active}
onChange={handleStatusToggle}
/>
<Text style={{ marginLeft: 8 }}>{formData.is_active ? 'Active' : 'Inactive'}</Text>
</div>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Status Number</Text>
<Text style={{ color: 'red' }}> *</Text>
<InputNumber <InputNumber
placeholder="Masukan code status" name="status_number"
value={formData.status_number}
placeholder="Masukan nomor status"
readOnly={props.readOnly} readOnly={props.readOnly}
style={{ width: '100%' }} style={{ width: '100%' }}
onChange={handleInputNumberChange}
/> />
</Form.Item> </div>
<Form.Item <div style={{ marginBottom: 12 }}>
name="statusName" <Text strong>Status Name</Text>
label={<Text strong>Status Name</Text>} <Text style={{ color: 'red' }}> *</Text>
rules={[{ required: true, message: 'Silakan masukkan nama status!' }]}
>
<Input <Input
name="status_name"
value={formData.status_name}
placeholder="Masukan nama status" placeholder="Masukan nama status"
readOnly={props.readOnly} readOnly={props.readOnly}
onChange={handleInputChange}
/> />
</Form.Item> </div>
<Form.Item <div style={{ marginBottom: 12 }}>
name="description" <Text strong>Status Color</Text>
label={<Text strong>Description</Text>} <Text style={{ color: 'red' }}> *</Text>
rules={[{ required: true, message: 'Silakan masukkan deskripsi!' }]} <Input
> name="status_color"
value={formData.status_color}
placeholder="Masukan warna status (e.g., hijau, #00ff00)"
readOnly={props.readOnly}
onChange={handleInputChange}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Description</Text>
<Text style={{ color: 'red' }}> *</Text>
<TextArea <TextArea
name="status_description"
value={formData.status_description}
placeholder="Masukan deskripsi" placeholder="Masukan deskripsi"
readOnly={props.readOnly} readOnly={props.readOnly}
rows={4} rows={4}
onChange={handleInputChange}
/> />
</Form.Item> </div>
</Form>
</Modal> </Modal>
); );
}; };

View File

@@ -1,68 +1,152 @@
import React from 'react'; import React, { memo, useState, useEffect } from 'react';
import { Card, Button, Row, Col, Typography, Space, ConfigProvider } from 'antd'; import { Space, ConfigProvider, Button, Row, Col, Card, Input, Segmented, Table, Pagination } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
SearchOutlined,
AppstoreOutlined,
TableOutlined,
} from '@ant-design/icons';
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import { deleteStatus, getAllStatuss } from '../../../../api/master-status';
const { Title, Text } = Typography; const ListStatus = memo(function ListStatus(props) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState('card');
const [trigerFilter, setTrigerFilter] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
const navigate = useNavigate();
const ListStatus = ({ const fetchData = async (page = 1, pageSize = 10) => {
setActionMode, setLoading(true);
handleEdit, try {
handleDelete, const params = new URLSearchParams();
handlePreview, params.append('page', page);
data, params.append('limit', pageSize);
}) => { if (searchValue) {
params.append('search', searchValue);
const getCardStyle = (statusName) => { }
let color; const response = await getAllStatuss(params);
switch (statusName.toLowerCase()) { setData(response.data || []);
case 'done': setPagination(prev => ({ ...prev, total: response.paging?.total || 0, current: page, pageSize: pageSize }));
color = '#52c41a'; // green } catch (error) {
break; console.error("Failed to fetch status data:", error);
case 'warning': setData([]);
color = '#faad14'; // orange } finally {
break; setLoading(false);
case 'alarm':
color = '#f5222d'; // red
break;
case 'critical':
color = '#000000'; // black
break;
default:
color = '#d9d9d9'; // default antd border color
} }
return { border: `2px solid ${color}` };
}; };
const getTitleStyle = (statusName) => { useEffect(() => {
let backgroundColor; const token = localStorage.getItem('token');
switch (statusName.toLowerCase()) { if (!token) {
case 'done': navigate('/signin');
backgroundColor = '#52c41a'; // green return;
break;
case 'warning':
backgroundColor = '#faad14'; // orange
break;
case 'alarm':
backgroundColor = '#f5222d'; // red
break;
case 'critical':
backgroundColor = '#000000'; // black
break;
default:
backgroundColor = 'transparent';
} }
return { fetchData(pagination.current, pagination.pageSize);
backgroundColor, }, [props.actionMode, trigerFilter, navigate]);
color: '#fff',
padding: '2px 8px', const doFilter = () => {
borderRadius: '4px', setTrigerFilter(prev => !prev);
display: 'inline-block'
}; };
const handleSearch = (value) => {
setSearchValue(value);
setPagination(prev => ({ ...prev, current: 1 })); // Reset to first page on search
doFilter();
};
const handlePaginationChange = (page, pageSize) => {
fetchData(page, pageSize);
};
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 Hapus',
message: `Status "${record.status_name}" akan dihapus?`,
onConfirm: () => handleDelete(record.status_id),
});
};
const handleDelete = async (status_id) => {
try {
const response = await deleteStatus(status_id);
if (response.statusCode === 200) {
NotifAlert({ icon: 'success', title: 'Berhasil', message: 'Data Status berhasil dihapus.' });
doFilter();
} else {
NotifAlert({ icon: 'error', title: 'Gagal', message: response?.message || 'Gagal Menghapus Data' });
}
} catch (error) {
NotifAlert({ icon: 'error', title: 'Error', message: error.message });
}
};
const columns = [
{ title: 'Number', dataIndex: 'status_number', key: 'status_number', width: '15%' },
{ title: 'Name', dataIndex: 'status_name', key: 'status_name', width: '25%' },
{ title: 'Description', dataIndex: 'status_description', key: 'status_description', width: '40%' },
{
title: 'Aksi', key: 'aksi', align: 'center', width: '20%',
render: (_, record) => (
<Space>
<Button type="text" icon={<EyeOutlined />} onClick={() => showPreviewModal(record)} />
<Button type="text" icon={<EditOutlined />} onClick={() => showEditModal(record)} />
<Button danger type="text" icon={<DeleteOutlined />} onClick={() => showDeleteDialog(record)} />
</Space>
),
},
];
const getCardStyle = (color) => {
return { border: `2px solid ${color || '#d9d9d9'}`, height: '100%' };
};
const getTitleStyle = (color) => {
return { backgroundColor: color || 'transparent', color: '#fff', padding: '2px 8px', borderRadius: '4px', display: 'inline-block' };
}; };
return ( return (
<div style={{ padding: 24, minHeight: 360 }}> <Card>
<Row justify="end" style={{ marginBottom: 16 }}> <Row justify="space-between" align="middle" gutter={[16, 16]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Search by status name..."
onSearch={handleSearch}
allowClear
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }}
>
Search
</Button>
}
size="large"
/>
</Col>
<Col> <Col>
<ConfigProvider <ConfigProvider
theme={{ theme={{
@@ -78,37 +162,64 @@ const ListStatus = ({
}, },
}} }}
> >
<Button <Button icon={<PlusOutlined />} onClick={showAddModal} size="large">
icon={<PlusOutlined />}
onClick={() => setActionMode('add')}
>
Tambah Data Tambah Data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Col> </Col>
</Row> </Row>
<Row style={{ marginTop: 16 }}>
<Col>
<Segmented
options={[{ value: 'card', icon: <AppstoreOutlined /> }, { value: 'table', icon: <TableOutlined /> }]}
value={viewMode}
onChange={setViewMode}
/>
</Col>
</Row>
<div style={{ marginTop: 24 }}>
{viewMode === 'card' ? (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{data.map(item => ( {data.map(item => (
<Col xs={24} sm={12} md={8} lg={6} key={item.key}> <Col xs={24} sm={12} md={8} lg={6} key={item.status_id}>
<Card <Card
title={<span style={getTitleStyle(item.statusName)}>{item.statusName}</span>} title={<span style={getTitleStyle(item.status_color)}>{item.status_name}</span>}
style={getCardStyle(item.statusName)} style={getCardStyle(item.status_color)}
actions={[ actions={[
<Space size="middle" style={{ display: 'flex', justifyContent: 'center' }}> <EyeOutlined key="preview" onClick={() => showPreviewModal(item)} />,
<Button style={{ border: '1px solid #1890ff', color: '#1890ff', borderRadius: '6px', padding: '4px 8px' }} icon={<EyeOutlined />} onClick={() => handlePreview(item)} /> <EditOutlined key="edit" onClick={() => showEditModal(item)} />,
<Button style={{ border: '1px solid #faad14', color: '#faad14', borderRadius: '6px', padding: '4px 8px' }} icon={<EditOutlined />} onClick={() => handleEdit(item)} /> <DeleteOutlined key="delete" onClick={() => showDeleteDialog(item)} />,
<Button danger style={{ border: '1px solid red', borderRadius: '6px', padding: '4px 8px' }} icon={<DeleteOutlined />} onClick={() => handleDelete(item)} />
</Space>
]} ]}
> >
<p><Text strong>Code:</Text> {item.statusCode}</p> <p><b>Number:</b> {item.status_number}</p>
<p><Text strong>Description:</Text> {item.description}</p> <p><b>Description:</b> {item.status_description}</p>
</Card> </Card>
</Col> </Col>
))} ))}
</Row> </Row>
) : (
<>
<Table
columns={columns}
dataSource={data.map(item => ({ ...item, key: item.status_id }))}
pagination={false}
loading={loading}
/>
<Pagination
style={{ marginTop: 16, textAlign: 'right' }}
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={handlePaginationChange}
showSizeChanger
/>
</>
)}
</div> </div>
</Card>
); );
}; });
export default ListStatus; export default ListStatus;

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Modal, Input, Typography, Button, ConfigProvider, Switch } from 'antd'; import { Modal, Input, Typography, Button, ConfigProvider, Switch } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createUnit, updateUnit, getAllUnit } from '../../../../api/master-unit'; import { createUnit, updateUnit, getAllUnit } from '../../../../api/master-unit';
import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography; const { Text } = Typography;
@@ -26,71 +27,48 @@ const DetailUnit = (props) => {
const handleSave = async () => { const handleSave = async () => {
setConfirmLoading(true); setConfirmLoading(true);
// Validasi required fields const validationRules = [
if (!FormData.unit_name || FormData.unit_name.trim() === '') { { field: 'unit_name', label: 'Name', required: true },
];
if (
validateRun(FormData, validationRules, (errorMessages) => {
NotifOk({ NotifOk({
icon: 'warning', icon: 'warning',
title: 'Peringatan', title: 'Peringatan',
message: 'Kolom Name Tidak Boleh Kosong', message: errorMessages,
}); });
setConfirmLoading(false); setConfirmLoading(false);
})
)
return; return;
}
try { try {
if (FormData.unit_id) {
// Update existing unit
const payload = { const payload = {
name: FormData.unit_name, unit_name: FormData.unit_name,
is_active: FormData.is_active, is_active: FormData.is_active,
}; };
const response = await updateUnit(FormData.unit_id, payload); const response = FormData.unit_id
console.log('updateUnit response:', response); ? await updateUnit(FormData.unit_id, payload)
: await createUnit(payload);
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) { if (response.statusCode === 200 || response.statusCode === 201) {
// Get unit_code from response const unitCode = response.data?.unit_code || FormData.unit_code || 'N/A';
const unitCode = response.data?.unit_code || 'N/A'; const action = FormData.unit_id ? 'diubah' : 'ditambahkan';
NotifOk({ NotifOk({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
message: `Data Unit "${unitCode} - ${FormData.unit_name}" berhasil ditambahkan.`, message: `Data Unit "${unitCode} - ${payload.unit_name}" berhasil ${action}.`,
}); });
props.setActionMode('list'); props.setActionMode('list');
} else { } else {
NotifAlert({ NotifAlert({
icon: 'error', icon: 'error',
title: 'Gagal', title: 'Gagal',
message: response.message || 'Gagal menambahkan data Unit.', message: response.message || 'Gagal menyimpan data Unit.',
}); });
} }
}
} catch (error) { } catch (error) {
console.error('Save Unit Error:', error); console.error('Save Unit Error:', error);
NotifAlert({ NotifAlert({
@@ -98,9 +76,9 @@ const DetailUnit = (props) => {
title: 'Error', title: 'Error',
message: error.message || 'Terjadi kesalahan saat menyimpan data.', message: error.message || 'Terjadi kesalahan saat menyimpan data.',
}); });
} } finally {
setConfirmLoading(false); setConfirmLoading(false);
}
}; };
const handleInputChange = (e) => { const handleInputChange = (e) => {