feat: enhance status management with validation and improved UI components
This commit is contained in:
@@ -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,67 +37,24 @@ 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>
|
||||||
<ListStatus
|
{actionMode === 'list' &&
|
||||||
setActionMode={setActionMode}
|
<ListStatus
|
||||||
handleEdit={handleEdit}
|
setActionMode={setActionMode}
|
||||||
handleDelete={handleDelete}
|
setSelectedData={setSelectedData}
|
||||||
handlePreview={handlePreview}
|
actionMode={actionMode}
|
||||||
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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IndexStatus;
|
export default IndexStatus;
|
||||||
|
|||||||
@@ -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 {
|
setConfirmLoading(true);
|
||||||
const values = await form.validateFields();
|
|
||||||
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)
|
||||||
NotifOk({
|
: await createStatus(payload);
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: `Data Status "${payload.statusName}" berhasil ${
|
|
||||||
payload.key ? 'diubah' : 'ditambahkan'
|
|
||||||
}.`,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
|
const action = formData.status_id ? 'diubah' : 'ditambahkan';
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: `Data Status "${payload.status_name}" berhasil ${action}.`,
|
||||||
|
});
|
||||||
|
props.setActionMode('list');
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
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);
|
setConfirmLoading(false);
|
||||||
props.setActionMode('list');
|
|
||||||
form.resetFields();
|
|
||||||
} catch (errorInfo) {
|
|
||||||
console.log('Failed:', errorInfo);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
<Button onClick={handleCancel}>Batal</Button>
|
||||||
theme={{
|
<Button type="primary" loading={confirmLoading} onClick={handleSave}>
|
||||||
token: { colorPrimary: '#23A55A' },
|
Simpan
|
||||||
components: {
|
</Button>
|
||||||
Button: {
|
|
||||||
defaultBg: 'white',
|
|
||||||
defaultColor: '#23A55A',
|
|
||||||
defaultBorderColor: '#23A55A',
|
|
||||||
defaultHoverColor: 'white',
|
|
||||||
defaultHoverBg: '#23A55A',
|
|
||||||
defaultHoverBorderColor: '#23A55A',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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}>
|
|
||||||
Simpan
|
|
||||||
</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}
|
||||||
<InputNumber
|
onChange={handleStatusToggle}
|
||||||
placeholder="Masukan code status"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
<Text style={{ marginLeft: 8 }}>{formData.is_active ? 'Active' : 'Inactive'}</Text>
|
||||||
<Form.Item
|
</div>
|
||||||
name="statusName"
|
</div>
|
||||||
label={<Text strong>Status Name</Text>}
|
<div style={{ marginBottom: 12 }}>
|
||||||
rules={[{ required: true, message: 'Silakan masukkan nama status!' }]}
|
<Text strong>Status Number</Text>
|
||||||
>
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
<Input
|
<InputNumber
|
||||||
placeholder="Masukan nama status"
|
name="status_number"
|
||||||
readOnly={props.readOnly}
|
value={formData.status_number}
|
||||||
/>
|
placeholder="Masukan nomor status"
|
||||||
</Form.Item>
|
readOnly={props.readOnly}
|
||||||
<Form.Item
|
style={{ width: '100%' }}
|
||||||
name="description"
|
onChange={handleInputNumberChange}
|
||||||
label={<Text strong>Description</Text>}
|
/>
|
||||||
rules={[{ required: true, message: 'Silakan masukkan deskripsi!' }]}
|
</div>
|
||||||
>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<TextArea
|
<Text strong>Status Name</Text>
|
||||||
placeholder="Masukan deskripsi"
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
readOnly={props.readOnly}
|
<Input
|
||||||
rows={4}
|
name="status_name"
|
||||||
/>
|
value={formData.status_name}
|
||||||
</Form.Item>
|
placeholder="Masukan nama status"
|
||||||
</Form>
|
readOnly={props.readOnly}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Status Color</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<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
|
||||||
|
name="status_description"
|
||||||
|
value={formData.status_description}
|
||||||
|
placeholder="Masukan deskripsi"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
rows={4}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 gutter={[16, 16]}>
|
|
||||||
{data.map(item => (
|
<Row style={{ marginTop: 16 }}>
|
||||||
<Col xs={24} sm={12} md={8} lg={6} key={item.key}>
|
<Col>
|
||||||
<Card
|
<Segmented
|
||||||
title={<span style={getTitleStyle(item.statusName)}>{item.statusName}</span>}
|
options={[{ value: 'card', icon: <AppstoreOutlined /> }, { value: 'table', icon: <TableOutlined /> }]}
|
||||||
style={getCardStyle(item.statusName)}
|
value={viewMode}
|
||||||
actions={[
|
onChange={setViewMode}
|
||||||
<Space size="middle" style={{ display: 'flex', justifyContent: 'center' }}>
|
/>
|
||||||
<Button style={{ border: '1px solid #1890ff', color: '#1890ff', borderRadius: '6px', padding: '4px 8px' }} icon={<EyeOutlined />} onClick={() => handlePreview(item)} />
|
</Col>
|
||||||
<Button style={{ border: '1px solid #faad14', color: '#faad14', borderRadius: '6px', padding: '4px 8px' }} icon={<EditOutlined />} onClick={() => handleEdit(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><Text strong>Description:</Text> {item.description}</p>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
{viewMode === 'card' ? (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{data.map(item => (
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6} key={item.status_id}>
|
||||||
|
<Card
|
||||||
|
title={<span style={getTitleStyle(item.status_color)}>{item.status_name}</span>}
|
||||||
|
style={getCardStyle(item.status_color)}
|
||||||
|
actions={[
|
||||||
|
<EyeOutlined key="preview" onClick={() => showPreviewModal(item)} />,
|
||||||
|
<EditOutlined key="edit" onClick={() => showEditModal(item)} />,
|
||||||
|
<DeleteOutlined key="delete" onClick={() => showDeleteDialog(item)} />,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p><b>Number:</b> {item.status_number}</p>
|
||||||
|
<p><b>Description:</b> {item.status_description}</p>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ListStatus;
|
export default ListStatus;
|
||||||
@@ -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,70 +27,47 @@ 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 },
|
||||||
NotifOk({
|
];
|
||||||
icon: 'warning',
|
|
||||||
title: 'Peringatan',
|
if (
|
||||||
message: 'Kolom Name Tidak Boleh Kosong',
|
validateRun(FormData, validationRules, (errorMessages) => {
|
||||||
});
|
NotifOk({
|
||||||
setConfirmLoading(false);
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
message: errorMessages,
|
||||||
|
});
|
||||||
|
setConfirmLoading(false);
|
||||||
|
})
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (FormData.unit_id) {
|
const payload = {
|
||||||
// Update existing unit
|
unit_name: FormData.unit_name,
|
||||||
const payload = {
|
is_active: FormData.is_active,
|
||||||
name: FormData.unit_name,
|
};
|
||||||
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) {
|
if (response.statusCode === 200 || response.statusCode === 201) {
|
||||||
// Get updated data to show unit_code in notification
|
const unitCode = response.data?.unit_code || FormData.unit_code || 'N/A';
|
||||||
const unitCode = response.data?.unit_code || FormData.unit_code;
|
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 diubah.`,
|
message: `Data Unit "${unitCode} - ${payload.unit_name}" berhasil ${action}.`,
|
||||||
});
|
});
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: response.message || 'Gagal mengubah data Unit.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Create new unit
|
NotifAlert({
|
||||||
const payload = {
|
icon: 'error',
|
||||||
name: FormData.unit_name,
|
title: 'Gagal',
|
||||||
is_active: FormData.is_active,
|
message: response.message || 'Gagal menyimpan data Unit.',
|
||||||
};
|
});
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Save Unit Error:', error);
|
console.error('Save Unit Error:', error);
|
||||||
@@ -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) => {
|
||||||
@@ -298,4 +276,4 @@ const DetailUnit = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DetailUnit;
|
export default DetailUnit;
|
||||||
Reference in New Issue
Block a user