Merge pull request 'lavoce' (#20) from lavoce into main

Reviewed-on: #20
This commit is contained in:
2025-11-19 01:05:19 +00:00
7 changed files with 963 additions and 361 deletions

9
src/api/notification.jsx Normal file
View File

@@ -0,0 +1,9 @@
import { SendRequest } from '../components/Global/ApiRequest';
export const getAllNotification = async () => {
const response = await SendRequest({
method: 'get',
prefix: 'notification',
});
return response.data;
};

View File

@@ -1,6 +1,18 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { Divider, Typography, Button, Steps, Form, Row, Col, Card, Spin, Modal, ConfigProvider } from 'antd'; import {
Divider,
Typography,
Button,
Steps,
Form,
Row,
Col,
Card,
Spin,
Modal,
ConfigProvider,
} from 'antd';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById, updateBrand } from '../../../api/master-brand'; import { getBrandById, updateBrand } from '../../../api/master-brand';
@@ -48,12 +60,10 @@ const EditBrandDevice = () => {
const [solutionForm] = Form.useForm(); const [solutionForm] = Form.useForm();
const [sparepartForm] = Form.useForm(); const [sparepartForm] = Form.useForm();
const { const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic(
errorCodeFields, errorCodeForm,
addErrorCode, fileList
removeErrorCode, );
editErrorCode,
} = useErrorCodeLogic(errorCodeForm, fileList);
const { const {
solutionFields, solutionFields,
@@ -83,14 +93,14 @@ const EditBrandDevice = () => {
// Handlers for sparepart image upload // Handlers for sparepart image upload
const handleSparepartImageUpload = (fieldKey, imageData) => { const handleSparepartImageUpload = (fieldKey, imageData) => {
setSparepartImages(prev => ({ setSparepartImages((prev) => ({
...prev, ...prev,
[fieldKey]: imageData [fieldKey]: imageData,
})); }));
}; };
const handleSparepartImageRemove = (fieldKey) => { const handleSparepartImageRemove = (fieldKey) => {
setSparepartImages(prev => { setSparepartImages((prev) => {
const newImages = { ...prev }; const newImages = { ...prev };
delete newImages[fieldKey]; delete newImages[fieldKey];
return newImages; return newImages;
@@ -159,7 +169,8 @@ const EditBrandDevice = () => {
path_icon: ec.path_icon || '', path_icon: ec.path_icon || '',
status: ec.is_active, status: ec.is_active,
solution: ec.solution || [], solution: ec.solution || [],
errorCodeIcon: ec.path_icon ? { errorCodeIcon: ec.path_icon
? {
name: 'icon', name: 'icon',
uploadPath: ec.path_icon, uploadPath: ec.path_icon,
url: (() => { url: (() => {
@@ -168,8 +179,9 @@ const EditBrandDevice = () => {
const filename = pathParts.slice(1).join('/'); const filename = pathParts.slice(1).join('/');
return getFileUrl(folder, filename); return getFileUrl(folder, filename);
})(), })(),
type_solution: 'image' type_solution: 'image',
} : null, }
: null,
})) }))
: []; : [];
@@ -332,7 +344,7 @@ const EditBrandDevice = () => {
// Load sparepart images // Load sparepart images
const newSparepartImages = {}; const newSparepartImages = {};
record.sparepart.forEach(sparepart => { record.sparepart.forEach((sparepart) => {
if (sparepart.sparepart_image) { if (sparepart.sparepart_image) {
newSparepartImages[sparepart.id || sparepart.key] = sparepart.sparepart_image; newSparepartImages[sparepart.id || sparepart.key] = sparepart.sparepart_image;
} }
@@ -367,7 +379,7 @@ const EditBrandDevice = () => {
// Load sparepart images // Load sparepart images
const newSparepartImages = {}; const newSparepartImages = {};
record.sparepart.forEach(sparepart => { record.sparepart.forEach((sparepart) => {
if (sparepart.sparepart_image) { if (sparepart.sparepart_image) {
newSparepartImages[sparepart.id || sparepart.key] = sparepart.sparepart_image; newSparepartImages[sparepart.id || sparepart.key] = sparepart.sparepart_image;
} }
@@ -381,25 +393,61 @@ const EditBrandDevice = () => {
} }
}; };
const handleAddErrorCode = (newErrorCode) => { const handleAddErrorCode = async () => {
// Include the current icon in the error code try {
const errorCodeWithIcon = { // Validate error code form
...newErrorCode, const errorCodeValues = await errorCodeForm.validateFields();
errorCodeIcon: errorCodeIcon
// Get solution data from solution form
const solutionData = getSolutionData();
// Get sparepart data from sparepart form
const sparepartData = getSparepartData();
if (solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!',
});
return;
}
// Create complete error code object
const newErrorCode = {
error_code: errorCodeValues.error_code,
error_code_name: errorCodeValues.error_code_name,
error_code_description: errorCodeValues.error_code_description,
error_code_color: errorCodeValues.error_code_color || '#000000',
path_icon: errorCodeIcon?.uploadPath || '',
status: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: solutionData,
sparepart: sparepartData,
errorCodeIcon: errorCodeIcon,
key: editingErrorCodeKey || `temp-${Date.now()}`,
}; };
let updatedErrorCodes; let updatedErrorCodes;
if (editingErrorCodeKey) { if (editingErrorCodeKey) {
updatedErrorCodes = errorCodes.map((item) => // Update existing error code
item.key === editingErrorCodeKey ? errorCodeWithIcon : item updatedErrorCodes = errorCodes.map((item) => {
); if (item.key === editingErrorCodeKey) {
return {
...item,
...newErrorCode,
error_code_id: item.error_code_id || newErrorCode.error_code_id,
};
}
return item;
});
NotifOk({ NotifOk({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
message: 'Error code berhasil diupdate!', message: 'Error code berhasil diupdate!',
}); });
} else { } else {
updatedErrorCodes = [...errorCodes, errorCodeWithIcon]; // Add new error code
updatedErrorCodes = [...errorCodes, newErrorCode];
NotifOk({ NotifOk({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
@@ -408,7 +456,18 @@ const EditBrandDevice = () => {
} }
setErrorCodes(updatedErrorCodes); setErrorCodes(updatedErrorCodes);
// Delay form reset to prevent data loss
setTimeout(() => {
resetErrorCodeForm(); resetErrorCodeForm();
}, 100);
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!',
});
}
}; };
const resetErrorCodeForm = () => { const resetErrorCodeForm = () => {
@@ -552,7 +611,11 @@ const EditBrandDevice = () => {
</Col> </Col>
<Col span={8}> <Col span={8}>
<Card <Card
title={<Title level={5} style={{ margin: 0 }}>Solutions</Title>} title={
<Title level={5} style={{ margin: 0 }}>
Solutions
</Title>
}
size="small" size="small"
> >
<Form <Form
@@ -581,7 +644,11 @@ const EditBrandDevice = () => {
</Col> </Col>
<Col span={8}> <Col span={8}>
<Card <Card
title={<Title level={5} style={{ margin: 0 }}>Spareparts</Title>} title={
<Title level={5} style={{ margin: 0 }}>
Spareparts
</Title>
}
size="small" size="small"
> >
<Form <Form

View File

@@ -28,11 +28,11 @@ const BrandForm = ({ form, formData, onValuesChange, isEdit = false }) => {
<Form.Item label="Brand Code" name="brand_code"> <Form.Item label="Brand Code" name="brand_code">
<Input <Input
placeholder={isEdit ? 'Brand Code Auto Fill' : 'Brand Code'} placeholder={'Auto Fill Brand Code'}
disabled={isEdit} disabled={true}
style={{ style={{
backgroundColor: isEdit ? '#f5f5f5' : 'white', backgroundColor: '#f5f5f5',
cursor: isEdit ? 'not-allowed' : 'text' cursor: 'not-allowed'
}} }}
/> />
</Form.Item> </Form.Item>

View File

@@ -157,12 +157,12 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
icon: 'question', icon: 'question',
title: 'Konfirmasi', title: 'Konfirmasi',
message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?', message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
onConfirm: () => handleDelete(param.brand_id), onConfirm: () => handleDelete(param.brand_id, param.brand_name),
onCancel: () => {}, onCancel: () => {},
}); });
}; };
const handleDelete = async (brand_id) => { const handleDelete = async (brand_id, brand_name) => {
try { try {
const response = await deleteBrand(brand_id); const response = await deleteBrand(brand_id);
@@ -170,7 +170,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
NotifOk({ NotifOk({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
message: response.message || 'Data Brand Device berhasil dihapus.', message: `Brand ${brand_name} deleted successfully.`,
}); });
doFilter(); // Refresh data doFilter(); // Refresh data
} else { } else {

View File

@@ -1,5 +1,16 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Divider, Typography, Switch, Space, Card, Upload, message } from 'antd'; import {
Form,
Input,
Button,
Divider,
Typography,
Switch,
Space,
Card,
Upload,
message,
} from 'antd';
import { PlusOutlined, DeleteOutlined, UploadOutlined } from '@ant-design/icons'; import { PlusOutlined, DeleteOutlined, UploadOutlined } from '@ant-design/icons';
import { uploadFile } from '../../../../api/file-uploads'; import { uploadFile } from '../../../../api/file-uploads';
@@ -15,7 +26,7 @@ const SparepartForm = ({
onSparepartImageUpload, onSparepartImageUpload,
onSparepartImageRemove, onSparepartImageRemove,
sparepartImages = {}, sparepartImages = {},
isReadOnly = false isReadOnly = false,
}) => { }) => {
const [fieldStatuses, setFieldStatuses] = useState({}); const [fieldStatuses, setFieldStatuses] = useState({});
@@ -32,7 +43,7 @@ const SparepartForm = ({
useEffect(() => { useEffect(() => {
// Update field statuses when form changes // Update field statuses when form changes
const newStatuses = {}; const newStatuses = {};
sparepartFields.forEach(field => { sparepartFields.forEach((field) => {
newStatuses[field.key] = getFieldValue(field.key); newStatuses[field.key] = getFieldValue(field.key);
}); });
setFieldStatuses(newStatuses); setFieldStatuses(newStatuses);
@@ -55,15 +66,19 @@ const SparepartForm = ({
try { try {
const fileExtension = file.name.split('.').pop().toLowerCase(); const fileExtension = file.name.split('.').pop().toLowerCase();
const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension); const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(
fileExtension
);
const fileType = isImageFile ? 'image' : 'pdf'; const fileType = isImageFile ? 'image' : 'pdf';
const folder = 'images'; const folder = 'images';
const uploadResponse = await uploadFile(file, folder); const uploadResponse = await uploadFile(file, folder);
const imagePath = uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || ''; const imagePath =
uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || '';
if (imagePath) { if (imagePath) {
onSparepartImageUpload && onSparepartImageUpload(fieldKey, { onSparepartImageUpload &&
onSparepartImageUpload(fieldKey, {
name: file.name, name: file.name,
uploadPath: imagePath, uploadPath: imagePath,
fileExtension, fileExtension,
@@ -106,48 +121,40 @@ const SparepartForm = ({
size="small" size="small"
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
title={ title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Text strong>Sparepart {index + 1}</Text> <Text strong>Sparepart {index + 1}</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Form.Item name={[field.name, 'status']} valuePropName="checked" noStyle> {!isReadOnly && sparepartFields.length > 1 && (
<Switch <div style={{ textAlign: 'right' }}>
disabled={isReadOnly} <Button
size="small" type="text"
onChange={(checked) => { danger
onSparepartStatusChange && onSparepartStatusChange(field.key, checked); icon={<DeleteOutlined />}
setFieldStatuses(prev => ({ ...prev, [field.key]: checked })); onClick={() => onRemoveSparepartField(field.key)}
}}
style={{
backgroundColor: fieldStatuses[field.key] ? '#23A55A' : '#bfbfbf'
}}
/> />
</Form.Item> </div>
<Text style={{ fontSize: 12, color: '#666' }}> )}
{fieldStatuses[field.key] ? 'Active' : 'Inactive'}
</Text>
</div> </div>
</div> </div>
} }
> >
<Form <Form layout="vertical" style={{ border: 'none' }}>
layout="vertical"
style={{ border: 'none' }}
>
{/* Sparepart Name */} {/* Sparepart Name */}
<Form.Item <Form.Item
name={[field.name, 'name']} name={[field.name, 'name']}
rules={[{ required: true, message: 'Sparepart name wajib diisi!' }]} rules={[{ required: true, message: 'Sparepart name wajib diisi!' }]}
> >
<Input <Input placeholder="Enter sparepart name" disabled={isReadOnly} />
placeholder="Enter sparepart name"
disabled={isReadOnly}
/>
</Form.Item> </Form.Item>
{/* Description */} {/* Description */}
<Form.Item <Form.Item name={[field.name, 'description']}>
name={[field.name, 'description']}
>
<Input.TextArea <Input.TextArea
placeholder="Enter sparepart description (optional)" placeholder="Enter sparepart description (optional)"
rows={2} rows={2}
@@ -169,14 +176,26 @@ const SparepartForm = ({
</Button> </Button>
</Upload> </Upload>
) : ( ) : (
<div style={{ padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4 }}> <div
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
>
<Text type="secondary">No upload allowed</Text> <Text type="secondary">No upload allowed</Text>
</div> </div>
)} )}
{sparepartImages[field.key] && ( {sparepartImages[field.key] && (
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<img <img
src={sparepartImages[field.key].uploadPath} src={sparepartImages[field.key].uploadPath}
alt="Sparepart Image" alt="Sparepart Image"
@@ -189,10 +208,16 @@ const SparepartForm = ({
}} }}
/> />
<div> <div>
<Text style={{ fontSize: 12 }}>{sparepartImages[field.key].name}</Text> <Text style={{ fontSize: 12 }}>
{sparepartImages[field.key].name}
</Text>
<br /> <br />
<Text type="secondary" style={{ fontSize: 10 }}> <Text type="secondary" style={{ fontSize: 10 }}>
Size: {(sparepartImages[field.key].size / 1024).toFixed(1)} KB Size:{' '}
{(
sparepartImages[field.key].size / 1024
).toFixed(1)}{' '}
KB
</Text> </Text>
</div> </div>
{!isReadOnly && ( {!isReadOnly && (
@@ -209,24 +234,6 @@ const SparepartForm = ({
</div> </div>
)} )}
</Form.Item> </Form.Item>
{/* Delete Button */}
{!isReadOnly && sparepartFields.length > 1 && (
<div style={{ textAlign: 'right' }}>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => onRemoveSparepartField(field.key)}
style={{
borderColor: '#ff4d4f',
color: '#ff4d4f'
}}
>
Remove
</Button>
</div>
)}
</Form> </Form>
</Card> </Card>
))} ))}
@@ -243,14 +250,6 @@ const SparepartForm = ({
</Button> </Button>
</Form.Item> </Form.Item>
)} )}
{!isReadOnly && (
<div style={{ marginTop: 16 }}>
<Text type="secondary">
* Add at least one sparepart for this error code.
</Text>
</div>
)}
</Form> </Form>
</div> </div>
); );

View File

@@ -1,7 +1,7 @@
import React, { memo, useState, useEffect } from 'react'; import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb'; import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
import { Form, Typography } from 'antd'; import { Typography } from 'antd';
import ListNotification from './component/ListNotification'; import ListNotification from './component/ListNotification';
import DetailNotification from './component/DetailNotification'; import DetailNotification from './component/DetailNotification';
@@ -10,7 +10,6 @@ const { Text } = Typography;
const IndexNotification = memo(function IndexNotification() { const IndexNotification = memo(function IndexNotification() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
const [form] = Form.useForm();
const [actionMode, setActionMode] = useState('list'); const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null); const [selectedData, setSelectedData] = useState(null);
@@ -36,19 +35,14 @@ const IndexNotification = memo(function IndexNotification() {
useEffect(() => { useEffect(() => {
if (actionMode === 'preview') { if (actionMode === 'preview') {
setIsModalVisible(true); setIsModalVisible(true);
if (selectedData) {
form.setFieldsValue(selectedData);
}
} else { } else {
setIsModalVisible(false); setIsModalVisible(false);
form.resetFields();
} }
}, [actionMode, selectedData, form]); }, [actionMode]);
const handleCancel = () => { const handleCancel = () => {
setActionMode('list'); setActionMode('list');
setSelectedData(null); setSelectedData(null);
form.resetFields();
}; };
return ( return (
@@ -62,7 +56,6 @@ const IndexNotification = memo(function IndexNotification() {
<DetailNotification <DetailNotification
visible={isModalVisible} visible={isModalVisible}
onCancel={handleCancel} onCancel={handleCancel}
form={form}
selectedData={selectedData} selectedData={selectedData}
/> />
</React.Fragment> </React.Fragment>

File diff suppressed because it is too large Load Diff