repair: brandDevice sparepart integration

This commit is contained in:
2025-12-02 11:10:36 +07:00
parent 1c2ddca9d4
commit 1797058526
15 changed files with 1544 additions and 1279 deletions

View File

@@ -9,7 +9,7 @@ import {
Row,
Col,
Card,
ConfigProvider,
Spin,
Table,
Tag,
Space,
@@ -19,44 +19,44 @@ import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { createBrand } from '../../../api/master-brand';
import BrandForm from './component/BrandForm';
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
import ErrorCodeListModal from './component/ErrorCodeListModal';
import FormActions from './component/FormActions';
import SolutionForm from './component/SolutionForm';
import SparepartForm from './component/SparepartForm';
import FormActions from './component/FormActions';
import ListErrorCode from './component/ListErrorCode';
import { useErrorCodeLogic } from './hooks/errorCode';
import { useSolutionLogic } from './hooks/solution';
import { useSparepartLogic } from './hooks/sparepart';
import { uploadFile, getFolderFromFileType } from '../../../api/file-uploads';
import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { EditOutlined, DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic';
const { Title } = Typography;
const { Step } = Steps;
const defaultData = {
brand_name: '',
brand_type: '',
brand_model: '',
brand_manufacture: '',
is_active: true,
brand_code: '',
};
const AddBrandDevice = () => {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandForm] = Form.useForm();
const [errorCodeForm] = Form.useForm();
const [solutionForm] = Form.useForm();
const [sparepartForm] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [fileList, setFileList] = useState([]);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState(defaultData);
const [formData, setFormData] = useState({
brand_name: '',
brand_type: '',
brand_model: '',
brand_manufacture: '',
is_active: true,
});
const [errorCodes, setErrorCodes] = useState([]);
const [pendingErrorCodes, setPendingErrorCodes] = useState([]);
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [solutionForm] = Form.useForm();
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic(
errorCodeForm,
[]
);
const {
solutionFields,
@@ -64,47 +64,21 @@ const AddBrandDevice = () => {
solutionStatuses,
solutionsToDelete,
firstSolutionValid,
checkFirstSolutionValid,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
handleSolutionStatusChange,
resetSolutionFields,
checkFirstSolutionValid,
getSolutionData,
setSolutionsForExistingRecord,
} = useSolutionLogic(solutionForm);
// For spareparts, we'll use the local state directly since it's just an array of IDs
const handleSparepartChange = (values) => {
setSelectedSparepartIds(values || []);
};
const resetSparepartFields = () => {
setSelectedSparepartIds([]);
};
const getSparepartData = () => {
return selectedSparepartIds;
};
const setSparepartsForExistingRecord = (sparepartData) => {
if (!sparepartData) {
setSelectedSparepartIds([]);
return;
}
if (Array.isArray(sparepartData)) {
setSelectedSparepartIds(sparepartData);
} else if (typeof sparepartData === 'object' && sparepartData.spareparts) {
setSelectedSparepartIds(sparepartData.spareparts || []);
} else {
setSelectedSparepartIds(sparepartData.map(sp => sp.sparepart_id || sp.brand_sparepart_id || sp.id).filter(id => id));
}
};
useEffect(() => {
setBreadcrumbItems([
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span> },
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span>
},
{
title: (
<span
@@ -131,7 +105,16 @@ const AddBrandDevice = () => {
const handleNextStep = async () => {
try {
await brandForm.validateFields();
const currentFormData = await brandForm.validateFields();
setFormData({
brand_name: currentFormData.brand_name,
brand_type: currentFormData.brand_type || '',
brand_model: currentFormData.brand_model || '',
brand_manufacture: currentFormData.brand_manufacture || '',
is_active: currentFormData.is_active,
});
setCurrentStep(1);
} catch (error) {
NotifAlert({
@@ -145,47 +128,33 @@ const AddBrandDevice = () => {
const handleFinish = async () => {
setConfirmLoading(true);
try {
// Validation: Ensure at least one error code
if (errorCodes.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setidaknya tambahkan 1 error code!',
});
setConfirmLoading(false);
return;
}
const transformedErrorCodes = errorCodes.map((ec) => ({
const transformedErrorCodes = pendingErrorCodes.length > 0 ? pendingErrorCodes.map(ec => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_name: ec.error_code_name,
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
error_code_color: ec.error_code_color || '#ad4141ff',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map((sol) => ({
solution: (ec.solution || []).map(sol => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active !== false,
})),
}));
is_active: sol.is_active
}))
})) : [];
const sparepartData = getSparepartData();
const finalFormData = {
const brandData = {
brand_name: formData.brand_name,
brand_type: formData.brand_type || '',
brand_model: formData.brand_model || '',
brand_manufacture: formData.brand_manufacture,
brand_manufacture: formData.brand_manufacture || '',
is_active: formData.is_active,
spareparts: sparepartData,
spareparts: selectedSparepartIds,
error_code: transformedErrorCodes,
};
console.log('Final form data:', finalFormData); // Debug log
const response = await createBrand(finalFormData);
const response = await createBrand(brandData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
@@ -202,7 +171,6 @@ const AddBrandDevice = () => {
});
}
} catch (error) {
console.error('Finish Error:', error);
NotifAlert({
icon: 'error',
title: 'Gagal',
@@ -221,60 +189,46 @@ const AddBrandDevice = () => {
error_code_color: record.error_code_color,
status: record.status,
});
setFileList(record.fileList || []);
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(null);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
}
if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart);
} else {
resetSolutionFields();
}
};
const handleEditErrorCode = (record) => {
// Prevent infinite loop
if (editingErrorCodeKey === record.key) {
return;
}
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color || '#000000',
status: record.status !== false,
error_code_color: record.error_code_color,
status: record.status,
});
setFileList(record.fileList || []);
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
// Reset solution fields first
resetSolutionFields();
// Then load new solutions
setTimeout(() => {
setSolutionsForExistingRecord(record.solution, solutionForm);
}, 0);
} else {
resetSolutionFields();
setSolutionsForExistingRecord(record.solution, solutionForm);
}
if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart);
const formElement = document.querySelector('.ant-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const handleAddErrorCode = async () => {
try {
const formValues = errorCodeForm.getFieldsValue();
const errorCodeValues = await errorCodeForm.validateFields();
const solutionData = getSolutionData();
// Validation
if (!formValues.error_code || !formValues.error_code_name) {
// Validate error code fields
if (!errorCodeValues.error_code || !errorCodeValues.error_code_name) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
@@ -283,10 +237,8 @@ const AddBrandDevice = () => {
return;
}
// Validate at least 1 solution
const solutions = getSolutionData();
if (solutions.length === 0) {
// Validate solution data
if (!solutionData || solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
@@ -295,31 +247,48 @@ const AddBrandDevice = () => {
return;
}
// Validate each solution has name
const invalidSolution = solutionData.find(sol => !sol.solution_name || sol.solution_name.trim() === '');
if (invalidSolution) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap solution harus memiliki nama!',
});
return;
}
const newErrorCode = {
key: Date.now(),
error_code: formValues.error_code,
error_code_name: formValues.error_code_name || '',
error_code_description: formValues.error_code_description || '',
error_code_color: formValues.error_code_color || '#000000',
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: formValues.status !== false,
is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: solutionData,
errorCodeIcon: errorCodeIcon,
solution: solutions,
key: editingErrorCodeKey || `temp-${Date.now()}`,
};
let updatedPendingErrorCodes;
if (editingErrorCodeKey) {
const updatedCodes = errorCodes.map((item) =>
item.key === editingErrorCodeKey ? newErrorCode : item
);
setErrorCodes(updatedCodes);
updatedPendingErrorCodes = pendingErrorCodes.map((item) => {
if (item.key === editingErrorCodeKey) {
return {
...item,
...newErrorCode,
error_code_id: item.error_code_id || newErrorCode.error_code_id,
};
}
return item;
});
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil diupdate!',
});
} else {
const updatedCodes = [...errorCodes, newErrorCode];
setErrorCodes(updatedCodes);
updatedPendingErrorCodes = [...pendingErrorCodes, newErrorCode];
NotifOk({
icon: 'success',
title: 'Berhasil',
@@ -327,15 +296,17 @@ const AddBrandDevice = () => {
});
}
// Reset all forms
resetErrorCodeForm();
resetSolutionFields();
setPendingErrorCodes(updatedPendingErrorCodes);
setErrorCodes(updatedPendingErrorCodes);
setTimeout(() => {
resetErrorCodeForm();
}, 100);
} catch (error) {
console.error('Error adding error code:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal menambahkan error code',
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!',
});
}
};
@@ -344,22 +315,14 @@ const AddBrandDevice = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text',
});
setFileList([]);
setErrorCodeIcon(null);
resetSolutionFields();
resetSparepartFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handleCreateNewErrorCode = () => {
resetErrorCodeForm();
};
const handleDeleteErrorCode = (key) => {
const handleDeleteErrorCode = async (key) => {
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
@@ -369,7 +332,8 @@ const AddBrandDevice = () => {
return;
}
setErrorCodes(errorCodes.filter((item) => item.key !== key));
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
setErrorCodes(updatedErrorCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
@@ -377,75 +341,12 @@ const AddBrandDevice = () => {
});
};
const handleFileView = (pathSolution, fileType) => {
const filePath = pathSolution || '';
if (!filePath) return;
const parts = filePath.split('/');
if (parts.length < 2) return;
const [folder, filename] = parts;
const encodedFileName = encodeURIComponent(filename);
const navigationPath = `/master/brand-device/view/temp/files/${folder}/${encodedFileName}`;
navigate(navigationPath);
};
const handleSolutionFileUpload = async (file) => {
try {
const isAllowedType = [
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
].includes(file.type);
if (!isAllowedType) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
});
return;
}
const fileExtension = file.name.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
const fileType = isImage ? 'image' : 'pdf';
const folder = getFolderFromFileType(fileType);
const uploadResponse = await uploadFile(file, folder);
const actualPath = uploadResponse.data?.path_solution || '';
if (actualPath) {
file.uploadPath = actualPath;
file.solution_name = file.name;
file.solutionId = solutionFields[0];
file.type_solution = fileType;
setFileList((prevList) => [...prevList, file]);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `${file.name} berhasil diupload!`,
});
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: `Gagal mengupload ${file.name}`,
});
}
} catch (error) {
console.error('Error uploading file:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
});
}
};
const handleFileRemove = (file) => {
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
const handleCreateNewErrorCode = () => {
resetErrorCodeForm();
resetSolutionFields();
setErrorCodeIcon(null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handleErrorCodeIconUpload = (iconData) => {
@@ -466,6 +367,9 @@ const AddBrandDevice = () => {
setFormData((prev) => ({ ...prev, ...allValues }))
}
isEdit={false}
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={setSelectedSparepartIds}
showSparepartSection={true}
/>
);
}
@@ -473,10 +377,22 @@ const AddBrandDevice = () => {
if (currentStep === 1) {
return (
<>
<Row gutter={16} style={{ marginBottom: 24 }}>
{/* Error Code Form Column */}
<Col span={8}>
<Card size="small" title="Error Code">
<Row gutter={24}>
<Col span={6}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
{isErrorCodeFormReadOnly
? editingErrorCodeKey
? 'View Error Code'
: 'Error Code Form'
: editingErrorCodeKey
? 'Edit Error Code'
: 'Error Code'}
</Title>
}
size="small"
>
<Form
form={errorCodeForm}
layout="vertical"
@@ -495,10 +411,15 @@ const AddBrandDevice = () => {
</Form>
</Card>
</Col>
{/* Solution Form Column */}
<Col span={8}>
<Card size="small" title="Solutions">
<Col span={6}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
Solutions
</Title>
}
size="small"
>
<Form
form={solutionForm}
layout="vertical"
@@ -513,132 +434,46 @@ const AddBrandDevice = () => {
solutionFields={solutionFields}
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
fileList={fileList}
solutionsToDelete={solutionsToDelete}
firstSolutionValid={firstSolutionValid}
onAddSolutionField={handleAddSolutionField}
onRemoveSolutionField={handleRemoveSolutionField}
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
onSolutionFileUpload={handleSolutionFileUpload}
onFileView={handleFileView}
checkFirstSolutionValid={checkFirstSolutionValid}
isReadOnly={isErrorCodeFormReadOnly}
/>
</Form>
</Card>
</Col>
<Col span={8}>
<Card size="small" title="Spareparts">
<Form
form={sparepartForm}
layout="vertical"
>
<SparepartForm
sparepartForm={sparepartForm}
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={handleSparepartChange}
isReadOnly={isErrorCodeFormReadOnly}
/>
</Form>
</Card>
</Col>
{/* Error Codes Table Column */}
<Col span={24} style={{ marginTop: 16 }}>
<Card size="small" title={`Error Codes (${errorCodes.length})`}>
<Table
dataSource={errorCodes}
columns={[
{
title: 'Error Code',
dataIndex: 'error_code',
key: 'error_code',
width: '25%',
},
{
title: 'Sol',
key: 'Sol',
width: '10%',
align: 'center',
render: (_, record) => {
const solutionCount = record.solution
? record.solution.length
: 0;
return (
<Tag
color={solutionCount > 0 ? 'green' : 'red'}
>
{solutionCount} Sol
</Tag>
);
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: '20%',
align: 'center',
render: (_, { status }) => (
<Tag color={status ? 'green' : 'red'}>
{status ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Action',
key: 'action',
align: 'center',
width: '15%',
render: (_, record) => (
<Space>
<Button
type="text"
style={{ borderColor: '#1890ff' }}
size="small"
icon={
<EyeOutlined
style={{ color: '#1890ff' }}
/>
}
onClick={() =>
handlePreviewErrorCode(record)
}
/>
<Button
type="text"
style={{ borderColor: '#faad14' }}
size="small"
icon={
<EditOutlined
style={{ color: '#faad14' }}
/>
}
onClick={() => handleEditErrorCode(record)}
/>
<Button
type="text"
danger
style={{ borderColor: 'red' }}
size="small"
icon={<DeleteOutlined />}
onClick={() =>
handleDeleteErrorCode(record.key)
}
/>
</Space>
),
},
]}
rowKey="key"
pagination={{
pageSize: 10,
showSizeChanger: false,
hideOnSinglePage: true,
}}
size="small"
scroll={{ y: 300 }}
<Col span={12}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
Error Codes ({errorCodes.length})
</Title>
}
size="small"
>
<ListErrorCode
errorCodes={errorCodes}
loading={loading}
onPreview={handlePreviewErrorCode}
onEdit={handleEditErrorCode}
onDelete={handleDeleteErrorCode}
/>
<div style={{ marginTop: 16, textAlign: 'center' }}>
<Button
type="dashed"
onClick={handleCreateNewErrorCode}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A'
}}
>
+ Add New Error Code
</Button>
</div>
</Card>
</Col>
</Row>
@@ -657,7 +492,37 @@ const AddBrandDevice = () => {
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
<div style={{ marginTop: 24 }}>{renderStepContent()}</div>
<div style={{ position: 'relative' }}>
{loading && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(0.8px)',
filter: 'blur(0.5px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
borderRadius: '8px',
}}
>
<Spin size="large" />
</div>
)}
<div
style={{
filter: loading ? 'blur(0.5px)' : 'none',
transition: 'filter 0.3s ease',
}}
>
{renderStepContent()}
</div>
</div>
<Divider />
<FormActions
currentStep={currentStep}
@@ -672,4 +537,4 @@ const AddBrandDevice = () => {
);
};
export default AddBrandDevice;
export default AddBrandDevice;

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import {
Divider,
Typography,
@@ -10,11 +10,6 @@ import {
Col,
Card,
Spin,
Modal,
ConfigProvider,
Table,
Tag,
Space,
} from 'antd';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
@@ -23,55 +18,59 @@ import { getFileUrl } from '../../../api/file-uploads';
import BrandForm from './component/BrandForm';
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
import SolutionForm from './component/SolutionForm';
import SparepartForm from './component/SparepartForm';
import ErrorCodeListModal from './component/ErrorCodeListModal';
import FormActions from './component/FormActions';
import { useErrorCodeLogic } from './hooks/errorCode';
import ListErrorCode from './component/ListErrorCode';
import { useSolutionLogic } from './hooks/solution';
import { useSparepartLogic } from './hooks/sparepart';
import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic';
const { Title } = Typography;
const { Step } = Steps;
const defaultData = {
brand_name: '',
brand_type: '',
brand_model: '',
brand_manufacture: '',
is_active: true,
brand_code: '',
};
const EditBrandDevice = () => {
const navigate = useNavigate();
const { id } = useParams();
const location = useLocation();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandForm] = Form.useForm();
const [errorCodeForm] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [fileList, setFileList] = useState([]);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState(defaultData);
const [errorCodes, setErrorCodes] = useState([]);
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [solutionForm] = Form.useForm();
const [sparepartForm] = Form.useForm();
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [formData, setFormData] = useState({
brand_name: '',
brand_type: '',
brand_model: '',
brand_manufacture: '',
is_active: true,
});
const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic(
errorCodeForm,
fileList
);
// Custom hooks for edit mode
const {
confirmLoading,
setConfirmLoading,
currentStep,
setCurrentStep,
loading,
setLoading,
errorCodes,
setErrorCodes,
pendingErrorCodes,
setPendingErrorCodes,
editingErrorCodeKey,
setEditingErrorCodeKey,
isErrorCodeFormReadOnly,
setIsErrorCodeFormReadOnly,
handleAddErrorCode,
handleDeleteErrorCode,
} = useBrandDeviceLogic(true, id);
const {
solutionFields,
solutionTypes,
solutionStatuses,
solutionsToDelete,
firstSolutionValid,
checkFirstSolutionValid,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
@@ -81,34 +80,6 @@ const EditBrandDevice = () => {
setSolutionsForExistingRecord,
} = useSolutionLogic(solutionForm);
// For spareparts, we'll use the local state directly since it's just an array of IDs
const handleSparepartChange = (values) => {
setSelectedSparepartIds(values || []);
};
const resetSparepartFields = () => {
setSelectedSparepartIds([]);
};
const getSparepartData = () => {
return selectedSparepartIds;
};
const setSparepartsForExistingRecord = (sparepartData) => {
if (!sparepartData) {
setSelectedSparepartIds([]);
return;
}
if (Array.isArray(sparepartData)) {
setSelectedSparepartIds(sparepartData);
} else if (typeof sparepartData === 'object' && sparepartData.spareparts) {
setSelectedSparepartIds(sparepartData.spareparts || []);
} else {
setSelectedSparepartIds(sparepartData.map(sp => sp.sparepart_id || sp.brand_sparepart_id || sp.id).filter(id => id));
}
};
useEffect(() => {
const fetchBrandData = async () => {
const token = localStorage.getItem('token');
@@ -117,15 +88,10 @@ const EditBrandDevice = () => {
return;
}
const savedPhase =
location.state?.phase || localStorage.getItem(`brand_device_edit_${id}_last_phase`);
if (savedPhase) {
setCurrentStep(parseInt(savedPhase));
localStorage.removeItem(`brand_device_edit_${id}_last_phase`);
}
setBreadcrumbItems([
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span> },
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span>
},
{
title: (
<span
@@ -153,15 +119,14 @@ const EditBrandDevice = () => {
const brandData = response.data;
const newFormData = {
brand_name: brandData.brand_name,
brand_type: brandData.brand_type,
brand_model: brandData.brand_model,
brand_manufacture: brandData.brand_manufacture,
brand_type: brandData.brand_type || '',
brand_model: brandData.brand_model || '',
brand_manufacture: brandData.brand_manufacture || '',
is_active: brandData.is_active,
brand_code: brandData.brand_code,
};
const existingErrorCodes = brandData.error_code
? brandData.error_code.map((ec, index) => ({
? brandData.error_code.map((ec) => ({
key: `existing-${ec.error_code_id}`,
error_code_id: ec.error_code_id,
error_code: ec.error_code,
@@ -171,10 +136,9 @@ const EditBrandDevice = () => {
path_icon: ec.path_icon || '',
status: ec.is_active,
solution: ec.solution || [],
sparepart: ec.sparepart || [],
errorCodeIcon: ec.path_icon
? {
name: ec.path_icon.split('/').pop(), // Ambil nama file dari path
name: ec.path_icon.split('/').pop(),
uploadPath: ec.path_icon,
url: (() => {
const pathParts = ec.path_icon.split('/');
@@ -191,13 +155,13 @@ const EditBrandDevice = () => {
setFormData(newFormData);
brandForm.setFieldsValue(newFormData);
setErrorCodes(existingErrorCodes);
setPendingErrorCodes(existingErrorCodes);
// Set the selected sparepart IDs if available in the response
if (response.data.spareparts) {
// Extract the IDs from the spareparts objects
const sparepartIds = response.data.spareparts.map(sp => sp.sparepart_id);
if (brandData.spareparts && brandData.spareparts.length > 0) {
const sparepartIds = brandData.spareparts.map(sp => sp.sparepart_id);
setSelectedSparepartIds(sparepartIds);
setSparepartsForExistingRecord(sparepartIds);
} else {
setSelectedSparepartIds([]);
}
} else {
NotifAlert({
@@ -218,16 +182,20 @@ const EditBrandDevice = () => {
};
fetchBrandData();
}, [id, setBreadcrumbItems, navigate, brandForm, location]);
const handleCancel = () => {
localStorage.removeItem(`brand_device_edit_${id}_temp_data`);
navigate('/master/brand-device');
};
}, [id, setBreadcrumbItems, navigate, brandForm]);
const handleNextStep = async () => {
try {
await brandForm.validateFields();
const currentFormData = await brandForm.validateFields();
setFormData({
brand_name: currentFormData.brand_name,
brand_type: currentFormData.brand_type || '',
brand_model: currentFormData.brand_model || '',
brand_manufacture: currentFormData.brand_manufacture || '',
is_active: currentFormData.is_active,
});
setCurrentStep(1);
} catch (error) {
NotifAlert({
@@ -238,67 +206,53 @@ const EditBrandDevice = () => {
}
};
const handleCancel = () => {
navigate('/master/brand-device');
};
const handleFinish = async () => {
const currentFormData = formData;
if (!currentFormData.brand_name || currentFormData.brand_name.trim() === '' ||
!currentFormData.brand_manufacture || currentFormData.brand_manufacture.trim() === '') {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap lengkapi semua field wajib diisi (Brand Name dan Manufacturer)!',
});
return;
}
setConfirmLoading(true);
try {
// Get current solution data from forms
const currentSolutionData = getSolutionData();
const finalFormData = {
brand_name: formData.brand_name,
brand_type: formData.brand_type || '',
brand_model: formData.brand_model || '',
brand_manufacture: formData.brand_manufacture,
is_active: formData.is_active,
error_code: errorCodes.map((ec) => {
// If editing current error code, get latest data from forms
if (ec.key === editingErrorCodeKey) {
return {
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.errorCodeIcon?.uploadPath || ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: currentSolutionData.map((sol) => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active !== false,
})),
};
}
// Return existing data for other error codes
return {
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.errorCodeIcon?.uploadPath || ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map((sol) => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active !== false,
})),
};
}),
const brandUpdateData = {
brand_name: currentFormData.brand_name,
brand_type: currentFormData.brand_type || '',
brand_model: currentFormData.brand_model || '',
brand_manufacture: currentFormData.brand_manufacture || '',
is_active: currentFormData.is_active,
spareparts: selectedSparepartIds,
error_code: pendingErrorCodes.length > 0 ? pendingErrorCodes.map(ec => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name,
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
what_action_to_take: ec.what_action_to_take || '',
solution: (ec.solution || []).map(sol => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active
}))
})) : []
};
const sparepartData = getSparepartData();
const updatedFinalFormData = {
...finalFormData,
spareparts: sparepartData,
};
const response = await updateBrand(id, updatedFinalFormData);
const response = await updateBrand(id, brandUpdateData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
localStorage.removeItem(`brand_device_edit_${id}_temp_data`);
NotifOk({
icon: 'success',
title: 'Berhasil',
@@ -323,70 +277,50 @@ const EditBrandDevice = () => {
}
};
const handlePreviewErrorCode = (record) => {
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(record.key);
// Load solutions to solution form
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
} else {
resetSolutionFields();
}
// Load spareparts to sparepart form
if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart);
} else {
resetSparepartFields();
}
const handleErrorCodeIconUpload = (iconData) => {
setErrorCodeIcon(iconData);
};
const handleEditErrorCode = (record) => {
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
status: true,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setErrorCodeIcon(null);
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
// Load solutions to solution form
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
}
// Load spareparts to sparepart form
if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart);
}
const formElement = document.querySelector('.ant-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
setEditingErrorCodeKey(null);
};
const handleAddErrorCode = async () => {
try {
// Validate error code form
const errorCodeValues = await errorCodeForm.validateFields();
const handleCreateNewErrorCode = () => {
resetErrorCodeForm();
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
// Get solution data from solution form
// Local wrapper for handleAddErrorCode from useBrandDeviceLogic
const handleAddErrorCodeLocal = async () => {
try {
const errorCodeValues = await errorCodeForm.validateFields();
const solutionData = getSolutionData();
if (solutionData.length === 0) {
// Validate error code fields
if (!errorCodeValues.error_code || !errorCodeValues.error_code_name) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Error code dan error name wajib diisi!',
});
return;
}
// Validate solution data
if (!solutionData || solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
@@ -395,27 +329,32 @@ const EditBrandDevice = () => {
return;
}
// Get sparepart data from sparepart form
const sparepartData = getSparepartData();
// Validate each solution has name
const invalidSolution = solutionData.find(sol => !sol.solution_name || sol.solution_name.trim() === '');
if (invalidSolution) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap solution harus memiliki nama!',
});
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,
is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: solutionData,
...(sparepartData && sparepartData.length > 0 && { sparepart: sparepartData }),
errorCodeIcon: errorCodeIcon,
key: editingErrorCodeKey || `temp-${Date.now()}`,
};
let updatedErrorCodes;
let updatedPendingErrorCodes;
if (editingErrorCodeKey) {
// Update existing error code
updatedErrorCodes = errorCodes.map((item) => {
updatedPendingErrorCodes = pendingErrorCodes.map((item) => {
if (item.key === editingErrorCodeKey) {
return {
...item,
@@ -431,8 +370,7 @@ const EditBrandDevice = () => {
message: 'Error code berhasil diupdate!',
});
} else {
// Add new error code
updatedErrorCodes = [...errorCodes, newErrorCode];
updatedPendingErrorCodes = [...pendingErrorCodes, newErrorCode];
NotifOk({
icon: 'success',
title: 'Berhasil',
@@ -440,9 +378,9 @@ const EditBrandDevice = () => {
});
}
setErrorCodes(updatedErrorCodes);
setPendingErrorCodes(updatedPendingErrorCodes);
setErrorCodes(updatedPendingErrorCodes);
// Delay form reset to prevent data loss
setTimeout(() => {
resetErrorCodeForm();
}, 100);
@@ -455,92 +393,45 @@ const EditBrandDevice = () => {
}
};
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
const handlePreviewErrorCode = (record) => {
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text',
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setFileList([]);
setErrorCodeIcon(null);
resetSolutionFields();
resetSparepartFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
} else {
resetSolutionFields();
}
};
const handleDeleteErrorCode = async (key) => {
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return;
const handleEditErrorCode = (record) => {
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
}
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
setErrorCodes(updatedErrorCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!',
});
};
const handleCreateNewErrorCode = () => {
resetErrorCodeForm();
resetSolutionFields();
resetSparepartFields();
setErrorCodeIcon(null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handleErrorCodeIconUpload = (iconData) => {
setErrorCodeIcon(iconData);
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const handleFileView = (pathSolution, fileType) => {
localStorage.setItem(`brand_device_edit_${id}_last_phase`, currentStep.toString());
const tempData = {
errorCodes: errorCodes,
fileList: fileList,
solutionFields: solutionFields,
solutionTypes: solutionTypes,
solutionStatuses: solutionStatuses,
editingErrorCodeKey: editingErrorCodeKey,
isErrorCodeFormReadOnly: isErrorCodeFormReadOnly,
solutionsToDelete: Array.from(solutionsToDelete),
currentSolutionData: window.currentSolutionData || {},
};
localStorage.setItem(`brand_device_edit_${id}_temp_data`, JSON.stringify(tempData));
const filePath = pathSolution || '';
if (!filePath) return;
const parts = filePath.split('/');
if (parts.length < 2) return;
const [folder, filename] = parts;
const encodedFileName = encodeURIComponent(filename);
const navigationPath = `/master/brand-device/edit/${id}/files/${folder}/${encodedFileName}`;
navigate(navigationPath);
};
const handleSolutionFileUpload = (file) => {
setFileList((prevList) => [...prevList, file]);
};
const handleFileRemove = (file) => {
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
const formElement = document.querySelector('.ant-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const renderStepContent = () => {
@@ -553,6 +444,9 @@ const EditBrandDevice = () => {
setFormData((prev) => ({ ...prev, ...allValues }))
}
isEdit={true}
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={setSelectedSparepartIds}
showSparepartSection={true}
/>
);
}
@@ -561,7 +455,7 @@ const EditBrandDevice = () => {
return (
<>
<Row gutter={24}>
<Col span={8}>
<Col span={6}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
@@ -589,12 +483,12 @@ const EditBrandDevice = () => {
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={handleErrorCodeIconUpload}
onErrorCodeIconRemove={handleErrorCodeIconRemove}
onAddErrorCode={handleAddErrorCode}
onAddErrorCode={handleAddErrorCodeLocal}
/>
</Form>
</Card>
</Col>
<Col span={8}>
<Col span={6}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
@@ -610,140 +504,53 @@ const EditBrandDevice = () => {
solution_status_0: true,
solution_type_0: 'text',
}}
onValuesChange={checkFirstSolutionValid}
>
<SolutionForm
solutionForm={solutionForm}
solutionFields={solutionFields}
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
firstSolutionValid={firstSolutionValid}
onAddSolutionField={handleAddSolutionField}
onRemoveSolutionField={handleRemoveSolutionField}
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
checkFirstSolutionValid={checkFirstSolutionValid}
isReadOnly={isErrorCodeFormReadOnly}
onFileUpload={handleSolutionFileUpload}
onFileView={handleFileView}
/>
</Form>
</Card>
</Col>
<Col span={8}>
<Col span={12}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
Spareparts
Error Codes ({errorCodes.length})
</Title>
}
size="small"
>
<Form
form={sparepartForm}
layout="vertical"
>
<SparepartForm
sparepartForm={sparepartForm}
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={handleSparepartChange}
isReadOnly={isErrorCodeFormReadOnly}
/>
</Form>
</Card>
</Col>
<Col span={24} style={{ marginTop: 16 }}>
<Card size="small" title={`Error Codes (${errorCodes.length})`}>
<Table
dataSource={errorCodes}
columns={[
{
title: 'Error Code',
dataIndex: 'error_code',
key: 'error_code',
width: '25%',
},
{
title: 'Sol',
key: 'Sol',
width: '10%',
align: 'center',
render: (_, record) => {
const solutionCount = record.solution
? record.solution.length
: 0;
return (
<Tag
color={solutionCount > 0 ? 'green' : 'red'}
>
{solutionCount} Sol
</Tag>
);
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: '20%',
align: 'center',
render: (_, { status }) => (
<Tag color={status ? 'green' : 'red'}>
{status ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Action',
key: 'action',
align: 'center',
width: '15%',
render: (_, record) => (
<Space>
<Button
type="text"
style={{ borderColor: '#1890ff' }}
size="small"
icon={
<EyeOutlined
style={{ color: '#1890ff' }}
/>
}
onClick={() =>
handlePreviewErrorCode(record)
}
/>
<Button
type="text"
style={{ borderColor: '#faad14' }}
size="small"
icon={
<EditOutlined
style={{ color: '#faad14' }}
/>
}
onClick={() => handleEditErrorCode(record)}
/>
<Button
type="text"
danger
style={{ borderColor: 'red' }}
size="small"
icon={<DeleteOutlined />}
onClick={() =>
handleDeleteErrorCode(record.key)
}
/>
</Space>
),
},
]}
rowKey="key"
pagination={{
pageSize: 10,
showSizeChanger: false,
hideOnSinglePage: true,
}}
size="small"
scroll={{ y: 300 }}
<ListErrorCode
errorCodes={errorCodes}
loading={loading}
onPreview={handlePreviewErrorCode}
onEdit={handleEditErrorCode}
onDelete={handleDeleteErrorCode}
/>
<div style={{ marginTop: 16, textAlign: 'center' }}>
<Button
type="dashed"
onClick={handleCreateNewErrorCode}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A'
}}
>
+ Add New Error Code
</Button>
</div>
</Card>
</Col>
</Row>
@@ -807,4 +614,4 @@ const EditBrandDevice = () => {
);
};
export default EditBrandDevice;
export default EditBrandDevice;

View File

@@ -52,7 +52,6 @@ const ViewBrandDevice = () => {
});
}
} catch (error) {
console.error('Fetch Brand Device Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',

View File

@@ -26,17 +26,7 @@ const ViewFilePage = () => {
const [pdfBlobUrl, setPdfBlobUrl] = useState(null);
const [pdfLoading, setPdfLoading] = useState(false);
// Debug: Log URL parameters and location
const isFromEdit = window.location.pathname.includes('/edit/');
console.log('ViewFilePage URL Parameters:', {
id,
fileType,
fileName,
allParams: params,
windowLocation: window.location.pathname,
urlParts: window.location.pathname.split('/'),
isFromEdit
});
let fallbackId = id;
let fallbackFileType = fileType;
@@ -45,7 +35,6 @@ const ViewFilePage = () => {
if (!fileName || !fileType || !id) {
const urlParts = window.location.pathname.split('/');
// console.log('URL Parts from pathname:', urlParts);
const viewIndex = urlParts.indexOf('view');
const editIndex = urlParts.indexOf('edit');
@@ -55,13 +44,6 @@ const ViewFilePage = () => {
fallbackId = urlParts[actionIndex + 1];
fallbackFileType = urlParts[actionIndex + 3];
fallbackFileName = decodeURIComponent(urlParts[actionIndex + 4]);
console.log('Fallback extraction:', {
fallbackId,
fallbackFileType,
fallbackFileName,
actionType: viewIndex !== -1 ? 'view' : 'edit'
});
}
}
@@ -95,12 +77,9 @@ const ViewFilePage = () => {
const folder = getFolderFromFileType('pdf');
try {
const blobData = await getFile(folder, decodedFileName);
console.log('PDF blob data received:', blobData);
const blobUrl = window.URL.createObjectURL(blobData);
setPdfBlobUrl(blobUrl);
console.log('PDF blob URL created successfully:', blobUrl);
} catch (pdfError) {
console.error('Error loading PDF:', pdfError);
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
setPdfBlobUrl(null);
} finally {
@@ -110,7 +89,6 @@ const ViewFilePage = () => {
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setError('Failed to load data');
setLoading(false);
}
@@ -160,7 +138,7 @@ const ViewFilePage = () => {
const targetPhase = savedPhase ? parseInt(savedPhase) : 1;
console.log('ViewFilePage handleBack - Edit mode:', {
console.log({
savedPhase,
targetPhase,
id: fallbackId || id
@@ -345,12 +323,10 @@ const ViewFilePage = () => {
const folder = getFolderFromFileType('pdf');
getFile(folder, actualFileName)
.then(blobData => {
console.log('Retry PDF blob data:', blobData);
const blobUrl = window.URL.createObjectURL(blobData);
setPdfBlobUrl(blobUrl);
})
.catch(error => {
console.error('Error retrying PDF load:', error);
setError('Failed to load PDF file: ' + (error.message || error));
setPdfBlobUrl(null);
})

View File

@@ -1,76 +1,122 @@
import React from 'react';
import { Form, Input, Row, Col, Typography, Switch } from 'antd';
import React, { useState } from 'react';
import { Form, Input, Row, Col, Typography, Switch, Button, Card, Divider } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import SingleSparepartSelect from './SingleSparepartSelect';
const { Text } = Typography;
const BrandForm = ({ form, formData, onValuesChange, isEdit = false }) => {
const BrandForm = ({
form,
formData,
onValuesChange,
isEdit = false,
selectedSparepartIds = [],
onSparepartChange,
showSparepartSection = false
}) => {
const isActive = Form.useWatch('is_active', form) ?? formData.is_active ?? true;
const [showSparepart, setShowSparepart] = useState(showSparepartSection);
return (
<Form
layout="vertical"
form={form}
onValuesChange={onValuesChange}
initialValues={formData}
>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="is_active" valuePropName="checked" noStyle>
<Switch
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>
{isActive ? 'Running' : 'Offline'}
</Text>
</div>
</Form.Item>
<div>
<Form
layout="vertical"
form={form}
onValuesChange={onValuesChange}
initialValues={formData}
>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="is_active" valuePropName="checked" noStyle>
<Switch
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>
{isActive ? 'Running' : 'Offline'}
</Text>
</div>
</Form.Item>
<Form.Item label="Brand Code" name="brand_code">
<Input
placeholder={'Auto Fill Brand Code'}
disabled={true}
<Form.Item label="Brand Code" name="brand_code">
<Input
placeholder={'Auto Fill Brand Code'}
disabled={true}
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed'
}}
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Brand Name"
name="brand_name"
rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Manufacturer"
name="brand_manufacture"
rules={[{ required: true, message: 'Manufacturer wajib diisi!' }]}
>
<Input placeholder="Enter Manufacturer" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Brand Type" name="brand_type">
<Input placeholder="Enter Brand Type (Optional)" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Model" name="brand_model">
<Input placeholder="Enter Model (Optional)" />
</Form.Item>
</Col>
</Row>
</Form>
<Divider />
{/* Add Sparepart Button */}
<div style={{ marginTop: 16 }}>
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => setShowSparepart(!showSparepart)}
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed'
width: '100%',
borderStyle: 'dashed',
marginBottom: showSparepart ? 16 : 0
}}
/>
</Form.Item>
>
{showSparepart ? 'Hide Sparepart' : 'Add Sparepart'}
</Button>
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Brand Name"
name="brand_name"
rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Manufacturer"
name="brand_manufacture"
rules={[{ required: true, message: 'Manufacturer wajib diisi!' }]}
>
<Input placeholder="Enter Manufacturer" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Brand Type" name="brand_type">
<Input placeholder="Enter Brand Type (Optional)" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Model" name="brand_model">
<Input placeholder="Enter Model (Optional)" />
</Form.Item>
</Col>
</Row>
</Form>
{/* Sparepart Selection Section */}
{showSparepart && (
<Card
title="Brand Spareparts"
size="small"
style={{ marginTop: 16 }}
>
<SingleSparepartSelect
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={onSparepartChange}
isReadOnly={false}
/>
</Card>
)}
</div>
);
};

View File

@@ -1,200 +0,0 @@
import React, { useState } from 'react';
import { Modal, Table, Button, Space, message, Tag, ConfigProvider } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
const ErrorCodeListModal = ({
visible,
onClose,
errorCodes,
loading,
onPreview,
onEdit,
onDelete,
onAddNew,
}) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const columns = [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Error Code',
dataIndex: 'error_code',
key: 'error_code',
width: '15%',
},
{
title: 'Error Name',
dataIndex: 'error_code_name',
key: 'error_code_name',
width: '30%',
render: (text) => text || '-',
},
{
title: 'Description',
dataIndex: 'error_code_description',
key: 'error_code_description',
width: '25%',
render: (text) => text || '-',
ellipsis: true,
},
{
title: 'Solutions',
key: 'solutions',
width: '10%',
align: 'center',
render: (_, record) => {
const solutionCount = record.solution ? record.solution.length : 0;
return <Tag color={solutionCount > 0 ? 'green' : 'red'}>{solutionCount} Sol</Tag>;
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: '10%',
align: 'center',
render: (_, { status }) => (
<Tag color={status ? 'green' : 'red'}>{status ? 'Active' : 'Inactive'}</Tag>
),
},
{
title: 'Action',
key: 'action',
align: 'center',
width: '15%',
render: (_, record) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => onPreview(record)}
style={{
color: '#23A55A',
borderColor: '#23A55A',
}}
size="small"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEdit(record)}
style={{
color: '#faad14',
borderColor: '#faad14',
}}
size="small"
/>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
style={{
borderColor: '#ff4d4f',
color: '#ff4d4f',
}}
size="small"
/>
</Space>
),
},
];
const handleDelete = (record) => {
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return;
}
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi',
message: `Apakah anda yakin hapus error code "${
record.error_code_name || record.error_code
}" ?`,
onConfirm: () => {
setConfirmLoading(true);
onDelete(record.key);
setConfirmLoading(false);
},
onCancel: () => {},
});
};
return (
<Modal
title={
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>Daftar Error Codes</span>
<ConfigProvider
theme={{
token: { colorBgContainer: '#23a55ade' },
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onAddNew}
>
Add New Error Code
</Button>
</ConfigProvider>
</div>
}
open={visible}
onCancel={onClose}
closable={false}
maskClosable={false}
width={1200}
footer={[
<Button key="close" onClick={onClose}>
Close
</Button>,
]}
>
<Table
columns={columns}
dataSource={errorCodes}
loading={loading || confirmLoading}
rowKey="key"
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`,
}}
scroll={{ x: 1000 }}
size="small"
/>
</Modal>
);
};
export default ErrorCodeListModal;

View File

@@ -54,7 +54,6 @@ const ErrorCodeSimpleForm = ({
message.error(`Failed to upload ${file.name}`);
}
} catch (error) {
console.error('Error uploading icon:', error);
message.error(`Failed to upload ${file.name}`);
}
};
@@ -122,7 +121,7 @@ const ErrorCodeSimpleForm = ({
/>
</Form.Item>
<Form.Item noStyle style={{ width: '70%', paddingLeft: 8 }}>
<Form.Item noStyle style={{ width: '70%', paddingLeft: 8, }}>
{!isErrorCodeFormReadOnly ? (
<Upload
beforeUpload={handleIconUpload}
@@ -130,7 +129,18 @@ const ErrorCodeSimpleForm = ({
accept="image/*"
style={{ width: '100%' }}
>
<Button icon={<UploadOutlined />} style={{ width: '100%' }}>
<Button
icon={<UploadOutlined />}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
Upload Icon
</Button>
</Upload>

View File

@@ -181,7 +181,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
});
}
} catch (error) {
console.error('Delete Brand Device Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',

View File

@@ -0,0 +1,549 @@
import React, { useState, useEffect } from 'react';
import { Select, Card, Typography, Tag, Spin, Empty, Button, Image, Row, Col, Modal } from 'antd';
import { PlusOutlined, DeleteOutlined, CheckOutlined, EyeOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { getAllSparepart } from '../../../../api/sparepart';
import dayjs from 'dayjs';
const { Text, Title } = Typography;
const { Option } = Select;
const SingleSparepartSelect = ({
selectedSparepartIds = [],
onSparepartChange,
isReadOnly = false
}) => {
const [spareparts, setSpareparts] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedSpareparts, setSelectedSpareparts] = useState([]);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [previewModal, setPreviewModal] = useState({ visible: false, sparepart: null });
useEffect(() => {
fetchSpareparts();
}, []);
useEffect(() => {
if (selectedSparepartIds && selectedSparepartIds.length > 0) {
const fullSelectedSpareparts = spareparts.filter(sp =>
selectedSparepartIds.includes(sp.sparepart_id)
);
setSelectedSpareparts(fullSelectedSpareparts);
} else {
setSelectedSpareparts([]);
}
}, [selectedSparepartIds, spareparts]);
const fetchSpareparts = async () => {
setLoading(true);
try {
const params = new URLSearchParams();
params.set('limit', '1000');
const response = await getAllSparepart(params);
if (response && (response.statusCode === 200 || response.data)) {
const sparepartData = response.data?.data || response.data || [];
setSpareparts(sparepartData);
} else {
setSpareparts([
{
sparepart_id: 1,
sparepart_name: 'Compressor Oil Filter',
sparepart_description: 'Oil filter for compressor',
sparepart_foto: null,
sparepart_code: 'SP-001',
sparepart_merk: 'Brand A',
sparepart_model: 'Model X',
is_active: true,
stock_quantity: 50
}
]);
}
} catch (error) {
setSpareparts([
{
sparepart_id: 1,
sparepart_name: 'Compressor Oil Filter',
sparepart_description: 'Oil filter for compressor',
sparepart_foto: null,
sparepart_code: 'SP-001',
sparepart_merk: 'Brand A',
sparepart_model: 'Model X',
is_active: true,
stock_quantity: 50
}
]);
} finally {
setLoading(false);
}
};
const handleSparepartSelect = (sparepartId) => {
const selectedSparepart = spareparts.find(sp => sp.sparepart_id === sparepartId);
if (selectedSparepart) {
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepartId);
if (!isAlreadySelected) {
const newSelectedSpareparts = [...selectedSpareparts, selectedSparepart];
setSelectedSpareparts(newSelectedSpareparts);
const newSelectedIds = newSelectedSpareparts.map(sp => sp.sparepart_id);
onSparepartChange(newSelectedIds);
}
}
setDropdownOpen(false);
};
const handleRemoveSparepart = (sparepartId) => {
const newSelectedSpareparts = selectedSpareparts.filter(sp => sp.sparepart_id !== sparepartId);
setSelectedSpareparts(newSelectedSpareparts);
const newSelectedIds = newSelectedSpareparts.map(sp => sp.sparepart_id);
onSparepartChange(newSelectedIds);
};
const handlePreviewSparepart = (sparepart) => {
setPreviewModal({ visible: true, sparepart });
};
const handlePreviewModalClose = () => {
setPreviewModal({ visible: false, sparepart: null });
};
const renderSparepartCard = (sparepart, isSelected = false) => {
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id);
let imgSrc;
if (sparepart.sparepart_foto) {
if (sparepart.sparepart_foto.startsWith('http')) {
imgSrc = sparepart.sparepart_foto;
} else {
const fileName = sparepart.sparepart_foto.split('/').pop();
if (fileName === 'defaultSparepartImg.jpg') {
imgSrc = `/assets/defaultSparepartImg.jpg`;
} else {
const token = localStorage.getItem('token');
const baseURL = import.meta.env.VITE_API_SERVER || '';
imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
}
}
} else {
imgSrc = 'https://via.placeholder.com/150';
}
return (
<Col xs={24} sm={12} md={8} lg={6} key={sparepart.sparepart_id}>
<Card
hoverable={!isSelected && !isReadOnly && !isAlreadySelected}
style={{
borderRadius: '8px',
overflow: 'hidden',
border: isSelected ? '2px solid #1890ff' : isAlreadySelected ? '2px solid #52c41a' : '1px solid #E0E0E0',
}}
bodyStyle={{ padding: 0 }}
onClick={!isSelected && !isReadOnly && !isAlreadySelected ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
actions={[
// Preview action (selalu available)
<EyeOutlined
key="preview"
style={{ color: '#1890ff' }}
onClick={(e) => {
e.stopPropagation();
handlePreviewSparepart(sparepart);
}}
/>,
// Delete action (hanya untuk selected items)
isSelected && !isReadOnly && (
<DeleteOutlined
key="delete"
style={{ color: '#ff1818' }}
onClick={(e) => {
e.stopPropagation();
handleRemoveSparepart(sparepart.sparepart_id);
}}
/>
),
].filter(Boolean)}
>
<Row>
<Col span={8}>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '16px 8px',
height: '100%',
}}
>
{sparepart.sparepart_item_type && (
<Tag
color="blue"
style={{
marginBottom: '8px',
}}
>
{sparepart.sparepart_item_type}
</Tag>
)}
<div
style={{
backgroundColor: '#f0f0f0',
width: '100%',
paddingTop: '100%',
position: 'relative',
borderRadius: '4px',
overflow: 'hidden',
}}
>
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}>
<img
src={imgSrc}
alt={sparepart.sparepart_name || 'Sparepart'}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
onError={(e) => {
e.target.src = 'https://via.placeholder.com/150';
}}
/>
</div>
{isAlreadySelected && (
<div
style={{
position: 'absolute',
top: 6,
right: 6,
backgroundColor: '#52c41a',
borderRadius: '50%',
width: 16,
height: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
}}
>
<CheckOutlined style={{ color: 'white', fontSize: 8 }} />
</div>
)}
</div>
</div>
</Col>
<Col span={16}>
<div
style={{
padding: '16px',
height: '100%',
}}
>
{/* Title dengan proper hierarchy */}
<Title
level={5}
style={{
margin: 0,
marginBottom: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontSize: '16px',
}}
>
{sparepart.sparepart_name || sparepart.name || 'Unnamed'}
</Title>
{/* Stock Information */}
<Text type="secondary" style={{ fontSize: '14px', display: 'block', marginBottom: '8px' }}>
Available Stock: {sparepart.sparepart_stock || '0'}
</Text>
<div style={{ margin: '8px 0' }} />
{/* Code */}
<div style={{ marginBottom: '8px' }}>
<Text
code
style={{
fontSize: '12px',
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px'
}}
>
{sparepart.sparepart_code || 'No code'}
</Text>
</div>
{/* Brand/Model/Unit Information */}
{(sparepart.sparepart_merk || sparepart.sparepart_model || sparepart.sparepart_unit) && (
<div style={{ fontSize: '12px', color: '#666', marginBottom: '8px' }}>
{sparepart.sparepart_merk && (
<div>Brand: {sparepart.sparepart_merk}</div>
)}
{sparepart.sparepart_model && (
<div>Model: {sparepart.sparepart_model}</div>
)}
{sparepart.sparepart_unit && (
<div>Unit: {sparepart.sparepart_unit}</div>
)}
</div>
)}
{/* Last Updated */}
{sparepart.updated_at && (
<Text
type="secondary"
style={{
fontSize: '11px',
marginTop: '8px',
display: 'block',
}}
>
Last updated: {dayjs(sparepart.updated_at).format('DD MMM YYYY')}
</Text>
)}
</div>
</Col>
</Row>
</Card>
</Col>
);
};
return (
<>
<div>
{!isReadOnly && (
<div style={{ marginBottom: 16 }}>
<Select
placeholder="Search and select sparepart..."
style={{ width: '100%' }}
loading={loading}
onSelect={handleSparepartSelect}
value={null}
showSearch
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
open={dropdownOpen}
onDropdownVisibleChange={setDropdownOpen}
suffixIcon={<PlusOutlined />}
>
{spareparts
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
.map((sparepart) => (
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
<div>
<Text strong>{sparepart.sparepart_name || sparepart.name || 'Unnamed'}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>
({sparepart.sparepart_code || 'No code'})
</Text>
</div>
</Option>
))}
</Select>
</div>
)}
<div>
{selectedSpareparts.length > 0 ? (
<div>
<Title level={5} style={{ marginBottom: 16 }}>
Selected Spareparts ({selectedSpareparts.length})
</Title>
<Row gutter={[12, 12]}>
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
</Row>
</div>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No spareparts selected"
style={{ margin: '20px 0' }}
/>
)}
</div>
</div>
{/* Preview Modal */}
<Modal
title="Sparepart Details"
open={previewModal.visible}
onCancel={handlePreviewModalClose}
footer={[
<Button key="close" onClick={handlePreviewModalClose}>
Close
</Button>
]}
width={800}
centered
bodyStyle={{ padding: '24px' }}
>
{previewModal.sparepart && (
<Row gutter={[16, 16]}>
<Col span={8}>
<div style={{ textAlign: 'center' }}>
<div
style={{
backgroundColor: '#f0f0f0',
width: '200px',
height: '200px',
margin: '0 auto 16px',
position: 'relative',
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid #E0E0E0',
}}
>
{previewModal.sparepart.sparepart_foto ? (
<img
src={previewModal.sparepart.sparepart_foto.startsWith('http')
? previewModal.sparepart.sparepart_foto
: `${import.meta.env.VITE_API_SERVER || ''}/file-uploads/images/${encodeURIComponent(previewModal.sparepart.sparepart_foto.split('/').pop())}?token=${encodeURIComponent(localStorage.getItem('token') || '')}`
}
alt={previewModal.sparepart.sparepart_name || 'Sparepart'}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
onError={(e) => {
e.target.src = 'https://via.placeholder.com/200x200/d9d9d9/666666?text=No+Image';
}}
/>
) : (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
color: '#999'
}}>
No Image
</div>
)}
</div>
{previewModal.sparepart.sparepart_item_type && (
<Tag color="blue" style={{ marginTop: '8px' }}>
{previewModal.sparepart.sparepart_item_type}
</Tag>
)}
</div>
</Col>
<Col span={16}>
<div>
<Title level={4} style={{ marginBottom: '16px' }}>
{previewModal.sparepart.sparepart_name || 'Unnamed'}
</Title>
<div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
Code:
</Text>
<Text style={{ fontSize: '16px', marginLeft: '8px' }}>
{previewModal.sparepart.sparepart_code || 'N/A'}
</Text>
</div>
{previewModal.sparepart.sparepart_description && (
<div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '14px', color: '#262626' }}>
Description:
</Text>
<Text style={{ fontSize: '14px', marginLeft: '8px' }}>
{previewModal.sparepart.sparepart_description}
</Text>
</div>
)}
<div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '14px', color: '#262626' }}>
Status:
</Text>
<Tag
color={previewModal.sparepart.is_active ? 'green' : 'red'}
style={{ marginLeft: '8px', fontSize: '13px' }}
>
{previewModal.sparepart.is_active ? 'Active' : 'Inactive'}
</Tag>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '14px', color: '#262626' }}>
Stock:
</Text>
<Text style={{ fontSize: '14px', marginLeft: '8px' }}>
{previewModal.sparepart.sparepart_stock || '0'}
{previewModal.sparepart_unit ? ` ${previewModal.sparepart.unit}` : ' units'}
</Text>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: '16px' }}>
{previewModal.sparepart.sparepart_merk && (
<Col span={8}>
<div>
<Text strong style={{ fontSize: '13px', color: '#262626' }}>
Brand:
</Text>
<Text style={{ fontSize: '13px', marginLeft: '8px' }}>
{previewModal.sparepart.sparepart_merk}
</Text>
</div>
</Col>
)}
{previewModal.sparepart.sparepart_model && (
<Col span={8}>
<div>
<Text strong style={{ fontSize: '13px', color: '#262626' }}>
Model:
</Text>
<Text style={{ fontSize: '13px', marginLeft: '8px' }}>
{previewModal.sparepart.sparepart_model}
</Text>
</div>
</Col>
)}
{previewModal.sparepart.sparepart_unit && (
<Col span={8}>
<div>
<Text strong style={{ fontSize: '13px', color: '#262626' }}>
Unit:
</Text>
<Text style={{ fontSize: '13px', marginLeft: '8px' }}>
{previewModal.sparepart.sparepart_unit}
</Text>
</div>
</Col>
)}
</Row>
{previewModal.sparepart.updated_at && (
<div style={{ marginTop: '24px', paddingTop: '16px', borderTop: '1px solid #f0f0f0' }}>
<Text type="secondary" style={{ fontSize: '13px' }}>
Last updated: {dayjs(previewModal.sparepart.updated_at).format('DD MMMM YYYY, HH:mm')}
</Text>
</div>
)}
</div>
</Col>
</Row>
)}
</Modal>
</>
);
};
export default SingleSparepartSelect;

View File

@@ -22,25 +22,6 @@ const SolutionFieldNew = ({
onFileView,
fileList = []
}) => {
const [currentStatus, setCurrentStatus] = useState(solutionStatus ?? true);
// Watch form values
const getFieldValue = () => {
try {
const form = document.querySelector(`[data-field="${fieldName}"]`)?.form;
if (form) {
const formData = new FormData(form);
return formData.get(`${fieldName}.status`) === 'on';
}
return currentStatus;
} catch {
return currentStatus;
}
};
useEffect(() => {
setCurrentStatus(solutionStatus ?? true);
}, [solutionStatus]);
const handleFileUpload = async (file) => {
try {
const isAllowedType = [
@@ -86,7 +67,6 @@ const SolutionFieldNew = ({
});
}
} catch (error) {
console.error('Error uploading file:', error);
NotifAlert({
icon: 'error',
title: 'Error',
@@ -104,8 +84,9 @@ const SolutionFieldNew = ({
>
<TextArea
placeholder="Enter solution text"
rows={3}
rows={2}
disabled={isReadOnly}
style={{ fontSize: 12 }}
/>
</Form.Item>
);
@@ -129,9 +110,10 @@ const SolutionFieldNew = ({
<Button
icon={<UploadOutlined />}
disabled={isReadOnly}
style={{ width: '100%' }}
size="small"
style={{ width: '100%', fontSize: 12 }}
>
Upload File (PDF/Image)
Upload File
</Button>
</Upload>
</Form.Item>
@@ -174,64 +156,92 @@ const SolutionFieldNew = ({
return (
<div style={{
border: '1px solid #d9d9d9',
borderRadius: 8,
padding: 16,
marginBottom: 16,
borderRadius: 6,
padding: 12,
marginBottom: 12,
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Text strong>Solution #{index + 1}</Text>
<Space>
<Form.Item
name={[fieldName, 'name']}
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
style={{ margin: 0, width: 200 }}
>
<Input
placeholder="Solution name"
disabled={isReadOnly}
/>
</Form.Item>
<div style={{
marginBottom: 8,
gap: 8
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Text strong style={{
fontSize: 12,
color: '#262626',
display: 'block'
}}>
Solution #{index + 1}
</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Form.Item name={[fieldName, 'status']} valuePropName="checked" noStyle>
<Switch
disabled={isReadOnly}
onChange={(checked) => {
onStatusChange(fieldKey, checked);
setCurrentStatus(checked);
}}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Form.Item name={[fieldName, 'status']} valuePropName="checked" noStyle>
<Switch
size="small"
disabled={isReadOnly}
onChange={(checked) => {
onStatusChange(fieldKey, checked);
}}
style={{
backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf'
}}
/>
</Form.Item>
<Text style={{
fontSize: 11,
color: '#666',
whiteSpace: 'nowrap'
}}>
{solutionStatus ? 'Active' : 'Inactive'}
</Text>
</div>
{canRemove && !isReadOnly && (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={onRemove}
style={{
backgroundColor: currentStatus ? '#23A55A' : '#bfbfbf'
fontSize: 12,
padding: '2px 4px',
height: '24px'
}}
/>
</Form.Item>
<Text style={{ fontSize: 12, color: '#666' }}>
{currentStatus ? 'Active' : 'Inactive'}
</Text>
)}
</div>
</div>
{canRemove && !isReadOnly && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={onRemove}
/>
)}
</Space>
<Form.Item
name={[fieldName, 'name']}
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
style={{ margin: 0 }}
>
<Input
placeholder="Solution name"
disabled={isReadOnly}
size="default"
style={{ fontSize: 13 }}
/>
</Form.Item>
</div>
<Form.Item
name={[fieldName, 'type']}
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
style={{ marginBottom: 8 }}
initialValue={solutionType || 'text'}
>
<Radio.Group
onChange={(e) => onTypeChange(fieldKey, e.target.value)}
disabled={isReadOnly}
size="small"
defaultValue={solutionType || 'text'}
>
<Radio value="text">Text Solution</Radio>
<Radio value="file">File Solution</Radio>
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
</Radio.Group>
</Form.Item>

View File

@@ -23,53 +23,65 @@ const SolutionForm = ({
onAddSolution,
}) => {
return (
<div>
<div style={{ marginBottom: 0 }}>
<Form
form={solutionForm}
layout="vertical"
initialValues={{
solution_status_0: true,
solution_type_0: 'text',
solution_items: [{
status: true,
type: 'text',
}]
}}
style={{
marginBottom: 0
}}
>
<Divider orientation="left">Solution Items</Divider>
{solutionFields.map((field, index) => (
<SolutionFieldNew
key={field.key}
fieldKey={field.key}
fieldName={field.name}
index={index}
solutionType={solutionTypes[field.key]}
solutionStatus={solutionStatuses[field.key]}
onTypeChange={onSolutionTypeChange}
onStatusChange={onSolutionStatusChange}
onRemove={() => onRemoveSolutionField(field.key)}
onFileUpload={onSolutionFileUpload}
onFileView={onFileView}
fileList={fileList}
isReadOnly={isReadOnly}
canRemove={solutionFields.length > 1}
/>
))}
<div style={{
maxHeight: '400px',
overflowY: 'auto',
paddingRight: '8px'
}}>
{solutionFields.map((field, index) => (
<SolutionFieldNew
key={field.key}
fieldKey={field.key}
fieldName={field.name}
index={index}
solutionType={solutionTypes[field.key]}
solutionStatus={solutionStatuses[field.key]}
onTypeChange={onSolutionTypeChange}
onStatusChange={onSolutionStatusChange}
onRemove={() => onRemoveSolutionField(field.key)}
onFileUpload={onSolutionFileUpload}
onFileView={onFileView}
fileList={fileList}
isReadOnly={isReadOnly}
canRemove={solutionFields.length > 1}
/>
))}
</div>
{!isReadOnly && (
<>
<Form.Item>
<Form.Item style={{ marginBottom: 8 }}>
<Button
type="dashed"
onClick={onAddSolutionField}
icon={<PlusOutlined />}
style={{ width: '100%' }}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
height: '32px',
fontSize: '12px'
}}
>
+ Add Solution
Add More Sollution
</Button>
</Form.Item>
<div style={{ marginTop: 16 }}>
<Text type="secondary">
* At least one solution is required for each error code.
</Text>
</div>
</>
)}
</Form>

View File

@@ -1,15 +1,17 @@
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Image, Typography, Tag, Space, Spin, Button, Empty } from 'antd';
import { Card, Row, Col, Image, Typography, Tag, Space, Spin, Button, Empty, message } from 'antd';
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons';
import { getAllSparepart } from '../../../../api/sparepart';
import { addSparepartToBrand, removeSparepartFromBrand } from '../../../../api/master-brand';
const { Text, Title } = Typography;
const SparepartCardSelect = ({
selectedSparepartIds = [],
const SparepartCardSelect = ({
selectedSparepartIds = [],
onSparepartChange,
isLoading: externalLoading = false,
isReadOnly = false
isReadOnly = false,
brandId = null
}) => {
const [spareparts, setSpareparts] = useState([]);
const [loading, setLoading] = useState(false);
@@ -62,7 +64,6 @@ const SparepartCardSelect = ({
]);
}
} catch (error) {
console.error('Error loading spareparts:', error);
// Default mock data
setSpareparts([
{
@@ -105,14 +106,45 @@ const SparepartCardSelect = ({
sp.sparepart_model?.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleSparepartToggle = (sparepartId) => {
const handleSparepartToggle = async (sparepartId) => {
if (isReadOnly) return;
const newSelectedIds = selectedSparepartIds.includes(sparepartId)
? selectedSparepartIds.filter(id => id !== sparepartId)
: [...selectedSparepartIds, sparepartId];
onSparepartChange(newSelectedIds);
const isCurrentlySelected = selectedSparepartIds.includes(sparepartId);
// If brandId is provided, save immediately to database
if (brandId) {
try {
setLoading(true);
if (isCurrentlySelected) {
// Remove from database
await removeSparepartFromBrand(brandId, sparepartId);
message.success('Sparepart removed from brand successfully');
} else {
// Add to database
await addSparepartToBrand(brandId, sparepartId);
message.success('Sparepart added to brand successfully');
}
// Update local state
const newSelectedIds = isCurrentlySelected
? selectedSparepartIds.filter(id => id !== sparepartId)
: [...selectedSparepartIds, sparepartId];
onSparepartChange(newSelectedIds);
} catch (error) {
message.error(error.message || 'Failed to update sparepart');
} finally {
setLoading(false);
}
} else {
// If no brandId (add mode), just update local state
const newSelectedIds = isCurrentlySelected
? selectedSparepartIds.filter(id => id !== sparepartId)
: [...selectedSparepartIds, sparepartId];
onSparepartChange(newSelectedIds);
}
};
const isSelected = (sparepartId) => selectedSparepartIds.includes(sparepartId);

View File

@@ -21,6 +21,7 @@ export const useSolutionLogic = (solutionForm) => {
solutionForm.setFieldValue(['solution_items', newKey, 'name'], '');
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
solutionForm.setFieldValue(['solution_items', newKey, 'text'], '');
solutionForm.setFieldValue(['solution_items', newKey, 'status'], true);
}, 0);
};
@@ -57,8 +58,10 @@ export const useSolutionLogic = (solutionForm) => {
// Reset form values
solutionForm.resetFields();
solutionForm.setFieldsValue({
solution_status_0: true,
solution_type_0: 'text',
solution_items: [{
status: true,
type: 'text',
}]
});
};

View File

@@ -1,141 +0,0 @@
import { useState, useCallback } from 'react';
export const useSparepartLogic = (sparepartForm) => {
const [sparepartFields, setSparepartFields] = useState([]);
const [sparepartTypes, setSparepartTypes] = useState({});
const [sparepartStatuses, setSparepartStatuses] = useState({});
const [sparepartsToDelete, setSparepartsToDelete] = useState(new Set());
const handleAddSparepartField = useCallback(() => {
const newKey = Date.now();
const newField = {
key: newKey,
name: sparepartFields.length,
isCreated: true,
};
setSparepartFields(prev => [...prev, newField]);
setSparepartTypes(prev => ({
...prev,
[newKey]: 'required'
}));
setSparepartStatuses(prev => ({
...prev,
[newKey]: true
}));
}, [sparepartFields.length]);
const handleRemoveSparepartField = useCallback((key) => {
setSparepartFields(prev => prev.filter(field => field.key !== key));
setSparepartTypes(prev => {
const newTypes = { ...prev };
delete newTypes[key];
return newTypes;
});
setSparepartStatuses(prev => {
const newStatuses = { ...prev };
delete newStatuses[key];
return newStatuses;
});
// Add to delete list if it's not a new field
setSparepartsToDelete(prev => new Set([...prev, key]));
}, []);
const handleSparepartTypeChange = useCallback((key, type) => {
setSparepartTypes(prev => ({
...prev,
[key]: type
}));
}, []);
const handleSparepartStatusChange = useCallback((key, status) => {
setSparepartStatuses(prev => ({
...prev,
[key]: status
}));
}, []);
const resetSparepartFields = useCallback(() => {
setSparepartFields([]);
setSparepartTypes({});
setSparepartStatuses({});
setSparepartsToDelete(new Set());
}, []);
const getSparepartData = useCallback(() => {
if (!sparepartForm) return [];
const values = sparepartForm.getFieldsValue();
const data = [];
sparepartFields.forEach((field, index) => {
const fieldData = {
sparepart_id: values[`sparepart_id_${field.name}`],
sparepart_name: values[`sparepart_name_${field.name}`],
sparepart_description: values[`sparepart_description_${field.name}`],
status: values[`sparepart_status_${field.name}`],
type: sparepartTypes[field.key] || 'required',
};
// Only add if required fields are filled
if (fieldData.sparepart_id) {
data.push(fieldData);
}
});
return data;
}, [sparepartForm, sparepartFields, sparepartTypes]);
const setSparepartsForExistingRecord = useCallback((sparepartData, form) => {
resetSparepartFields();
if (!sparepartData || !Array.isArray(sparepartData)) {
return;
}
const newFields = sparepartData.map((sp, index) => ({
key: sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`,
name: index,
isCreated: false,
}));
setSparepartFields(newFields);
// Set form values for existing spareparts
setTimeout(() => {
const formValues = {};
sparepartData.forEach((sp, index) => {
const sparepartId = sp.brand_sparepart_id || sp.sparepart_id || sp.sparepart_name;
formValues[`sparepart_id_${index}`] = sparepartId;
formValues[`sparepart_status_${index}`] = sp.is_active ?? sp.status ?? true;
formValues[`sparepart_description_${index}`] = sp.brand_sparepart_description || sp.description || sp.sparepart_name;
setSparepartTypes(prev => ({
...prev,
[sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`]: sp.type || sp.sparepart_type || 'required'
}));
setSparepartStatuses(prev => ({
...prev,
[sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`]: sp.is_active ?? sp.status ?? true
}));
});
form.setFieldsValue(formValues);
}, 0);
}, [resetSparepartFields]);
return {
sparepartFields,
sparepartTypes,
sparepartStatuses,
sparepartsToDelete,
handleAddSparepartField,
handleRemoveSparepartField,
handleSparepartTypeChange,
handleSparepartStatusChange,
resetSparepartFields,
getSparepartData,
setSparepartsForExistingRecord,
};
};

View File

@@ -0,0 +1,298 @@
import { useState } from 'react';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
export const useBrandDeviceLogic = (isEditMode = false, brandId = null) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [errorCodes, setErrorCodes] = useState([]);
const [pendingErrorCodes, setPendingErrorCodes] = useState([]);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const handleCancel = () => {
};
const handleNextStep = async (validateForm, setFormData, currentFormData) => {
try {
const validatedFormData = await validateForm();
setFormData({
brand_name: validatedFormData.brand_name,
brand_type: validatedFormData.brand_type || '',
brand_model: validatedFormData.brand_model || '',
brand_manufacture: validatedFormData.brand_manufacture || '',
is_active: validatedFormData.is_active,
});
setCurrentStep(1);
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
});
return false;
}
};
const handleFinish = async (
formData,
selectedSparepartIds,
apiCall,
successMessage,
navigatePath
) => {
setConfirmLoading(true);
try {
const transformedErrorCodes = pendingErrorCodes.length > 0 ? pendingErrorCodes.map(ec => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name,
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map(sol => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active
}))
})) : (isEditMode ? errorCodes.map(ec => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name,
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map(sol => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active
}))
})) : []);
const brandData = {
brand_name: formData.brand_name,
brand_type: formData.brand_type || '',
brand_model: formData.brand_model || '',
brand_manufacture: formData.brand_manufacture || '',
is_active: formData.is_active,
spareparts: selectedSparepartIds,
error_code: transformedErrorCodes,
};
const response = await apiCall(brandId, brandData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || successMessage,
});
navigate(navigatePath);
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menyimpan data.',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
};
const handleAddErrorCode = async (
validateErrorCodeForm,
getSolutionData,
resetErrorCodeForm
) => {
try {
const errorCodeValues = await validateErrorCodeForm();
const solutionData = getSolutionData();
if (solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!',
});
return false;
}
const newErrorCode = {
key: editingErrorCodeKey || `temp-${Date.now()}`,
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: errorCodeValues.path_icon || '',
is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: solutionData,
};
let updatedPendingErrorCodes;
if (editingErrorCodeKey) {
updatedPendingErrorCodes = pendingErrorCodes.map((item) => {
if (item.key === editingErrorCodeKey) {
return {
...item,
...newErrorCode,
error_code_id: item.error_code_id || newErrorCode.error_code_id,
};
}
return item;
});
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil diupdate!',
});
} else {
updatedPendingErrorCodes = [...pendingErrorCodes, newErrorCode];
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil ditambahkan!',
});
}
setPendingErrorCodes(updatedPendingErrorCodes);
setErrorCodes(updatedPendingErrorCodes);
setTimeout(() => {
resetErrorCodeForm();
}, 100);
return true;
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!',
});
return false;
}
};
const handleDeleteErrorCode = (key) => {
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return false;
}
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
setErrorCodes(updatedErrorCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!',
});
return true;
};
const handleCreateNewErrorCode = (resetErrorCodeForm, resetSolutionFields) => {
resetErrorCodeForm();
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handlePreviewErrorCode = (
record,
setErrorCodeIcon,
setIsErrorCodeFormReadOnly,
setEditingErrorCodeKey,
setSolutionsForExistingRecord,
resetSolutionFields,
solutionForm
) => {
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
} else {
resetSolutionFields();
}
};
const handleEditErrorCode = (
record,
setErrorCodeIcon,
setIsErrorCodeFormReadOnly,
setEditingErrorCodeKey,
setSolutionsForExistingRecord,
resetSolutionFields,
solutionForm
) => {
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
error_code_color: record.error_code_color,
status: record.status,
});
setErrorCodeIcon(record.errorCodeIcon || null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
}
const formElement = document.querySelector('.ant-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return {
// State
confirmLoading,
setConfirmLoading,
currentStep,
setCurrentStep,
loading,
setLoading,
errorCodes,
setErrorCodes,
pendingErrorCodes,
setPendingErrorCodes,
editingErrorCodeKey,
setEditingErrorCodeKey,
isErrorCodeFormReadOnly,
setIsErrorCodeFormReadOnly,
// Handlers
handleCancel,
handleNextStep,
handleFinish,
handleAddErrorCode,
handleDeleteErrorCode,
handleCreateNewErrorCode,
handlePreviewErrorCode,
handleEditErrorCode,
};
};