repair: brandDevice add edit page

This commit is contained in:
2025-12-12 16:54:30 +07:00
parent 2ff50342e8
commit ea3adf40cc
5 changed files with 107 additions and 578 deletions

View File

@@ -21,7 +21,6 @@ import IndexShift from './pages/master/shift/IndexShift';
// Brand device
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice';
import AddEditErrorCode from './pages/master/brandDevice/AddEditErrorCode';
import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice';
import ViewFilePage from './pages/master/brandDevice/ViewFilePage';
@@ -118,9 +117,6 @@ const App = () => {
path="brand-device/view/temp/files/:fileName"
element={<ViewFilePage />}
/>
<Route path="brand-device/:brandId/error-code/add" element={<AddEditErrorCode />} />
<Route path="brand-device/:brandId/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
<Route path="brand-device/add/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
</Route>
<Route path="/report" element={<ProtectedRoute />}>

View File

@@ -10,22 +10,18 @@ import {
Col,
Card,
Spin,
Tag,
Space,
Input,
ConfigProvider,
} from 'antd';
import { EyeOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById, createBrand, createErrorCode, getErrorCodesByBrandId, updateErrorCode, deleteErrorCode, deleteBrand } from '../../../api/master-brand';
import { getFileUrl } from '../../../api/file-uploads';
import { SendRequest } from '../../../components/Global/ApiRequest';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import SolutionForm from './component/SolutionForm';
import SparepartSelect from './component/SparepartSelect';
import ListErrorCode from './component/ListErrorCode';
import FormActions from './component/FormActions';
const { Title } = Typography;
const { Step } = Steps;
@@ -129,7 +125,7 @@ const AddBrandDevice = () => {
const isFileType = solution.type_solution && solution.type_solution !== 'text';
newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text';
newSolutionStatuses[fieldKey] = solution.is_active !== false;
newSolutionStatuses[fieldKey] = solution.is_active;
let fileObject = null;
if (isFileType && (solution.path_solution || solution.path_document)) {
@@ -149,7 +145,7 @@ const AddBrandDevice = () => {
name: solution.solution_name || '',
type: isFileType ? 'file' : 'text',
text: solution.text_solution || '',
status: solution.is_active !== false,
status: solution.is_active,
file: fileObject,
fileUpload: fileObject,
path_solution: solution.path_solution || solution.path_document || null,
@@ -311,7 +307,7 @@ const AddBrandDevice = () => {
error_code_name: record.error_code_name || '',
error_code_description: record.error_code_description || '',
error_code_color: record.error_code_color || '#000000',
status: record.is_active !== false,
status: record.is_active,
});
if (record.path_icon) {
@@ -1129,13 +1125,23 @@ const AddBrandDevice = () => {
return (
<Card>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
<div style={{ position: 'relative' }}>
{loading && (
<ConfigProvider
theme={{
components: {
Switch: {
colorPrimary: '#23A55A',
colorPrimaryHover: '#23A55A',
},
},
}}
>
<Card>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
<div style={{ position: 'relative' }}>
{loading && (
<div
style={{
position: 'absolute',
@@ -1209,7 +1215,8 @@ const AddBrandDevice = () => {
)}
</div>
</div>
</Card>
</Card>
</ConfigProvider>
);
};

View File

@@ -1,467 +0,0 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import {
Card,
Typography,
Button,
Form,
Row,
Col,
Spin,
Upload,
} from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { getErrorCodeById, createErrorCode, updateErrorCode } from '../../../api/master-brand';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import ErrorCodeForm from './component/ErrorCodeForm';
import SolutionForm from './component/SolutionForm';
import { useSolutionLogic } from './hooks/solution';
import SparepartSelect from './component/SparepartSelect';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
const { Title } = Typography;
const AddEditErrorCode = () => {
const navigate = useNavigate();
const { brandId: routeBrandId, errorCodeId } = useParams();
const { setBreadcrumbItems } = useBreadcrumb();
const location = useLocation();
const currentBrandId = routeBrandId;
const isFromAddBrand = location.pathname.includes('/master/brand-device/') && location.pathname.includes('/error-code/') &&
(location.pathname.includes('/add') || (location.pathname.includes('/edit/') && !location.pathname.includes('/edit/')));
const [errorCodeForm] = Form.useForm();
const [solutionForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [isEdit, setIsEdit] = useState(false);
const [fileList, setFileList] = useState([]);
const {
solutionFields,
solutionTypes,
solutionStatuses,
solutionsToDelete,
firstSolutionValid,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
handleSolutionStatusChange,
resetSolutionFields,
getSolutionData,
setSolutionsForExistingRecord,
} = useSolutionLogic(solutionForm);
useEffect(() => {
const isEditMode = errorCodeId && errorCodeId !== 'add';
setIsEdit(isEditMode);
if (!isEditMode) {
resetSolutionFields();
}
setBreadcrumbItems([
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span>
},
{
title: (
<span
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => navigate('/master/brand-device')}
>
Brand Device
</span>
),
},
{
title: (
<span
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`)}
>
Edit Brand Device
</span>
),
},
{
title: (
<span style={{ fontSize: '14px', fontWeight: 'bold' }}>
{isEditMode ? 'Edit Error Code' : 'Add Error Code'}
</span>
),
},
]);
if (isEditMode && errorCodeId) {
const tempId = errorCodeId.startsWith('existing_') ? errorCodeId : `existing_${errorCodeId}`;
loadExistingErrorCode(tempId);
}
}, [currentBrandId, errorCodeId, navigate, setBreadcrumbItems]);
const loadExistingErrorCode = async (tempId) => {
try {
setLoading(true);
let errorIdToUse = tempId;
if (tempId.startsWith('existing_')) {
errorIdToUse = tempId.replace('existing_', '');
}
const errorCodeResponse = await getErrorCodeById(errorIdToUse);
if (errorCodeResponse && errorCodeResponse.statusCode === 200) {
const errorData = errorCodeResponse.data;
if (errorData) {
errorCodeForm.setFieldsValue({
error_code: errorData.error_code,
error_code_name: errorData.error_code_name || '',
error_code_description: errorData.error_code_description || '',
error_code_color: errorData.error_code_color || '#000000',
status: errorData.is_active !== false,
});
if (errorData.path_icon) {
setErrorCodeIcon({
name: errorData.path_icon.split('/').pop(),
uploadPath: errorData.path_icon,
url: errorData.path_icon,
});
}
if (errorData.solution && errorData.solution.length > 0) {
setSolutionsForExistingRecord(errorData.solution, solutionForm);
}
if (errorData.spareparts && errorData.spareparts.length > 0) {
const sparepartIds = errorData.spareparts.map(sp => sp.sparepart_id);
setSelectedSparepartIds(sparepartIds);
}
}
} else {
errorCodeForm.setFieldsValue({
error_code: '',
error_code_name: '',
error_code_description: '',
error_code_color: '#000000',
status: true,
});
NotifAlert({
icon: 'warning',
title: 'Peringatan',
message: 'Error code not found. Creating new error code.',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Failed to load error code data',
});
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
await errorCodeForm.validateFields();
const solutionData = getSolutionData();
if (!solutionData || solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap lengkapi minimal 1 solution',
});
return;
}
const invalidSolutions = solutionData.filter(solution => {
if (solution.type_solution === 'text') {
return !solution.text_solution || solution.text_solution.trim() === '';
} else if (solution.type_solution !== 'text') {
return !solution.path_solution || solution.path_solution.trim() === '';
}
return false;
});
if (invalidSolutions.length > 0) {
const invalidNames = invalidSolutions.map(s => s.solution_name).join(', ');
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: `Harap lengkapi solution berikut:\n${invalidSolutions.map(s =>
`- ${s.solution_name}: ${s.type_solution === 'text' ? 'Text solution wajib diisi' : 'File wajib diupload'}`
).join('\n')}`,
});
return;
}
const errorCodeValues = errorCodeForm.getFieldsValue();
setConfirmLoading(true);
try {
const payload = {
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?.path_icon || errorCodeIcon?.uploadPath || '',
is_active: errorCodeValues.status !== undefined ? errorCodeValues.status : true,
solution: solutionData || [],
spareparts: selectedSparepartIds || []
};
if (!isEdit) {
payload.error_code = errorCodeValues.error_code;
}
let response;
if (isEdit && errorCodeId) {
let cleanErrorCodeId = errorCodeId;
if (errorCodeId.startsWith('existing_')) {
cleanErrorCodeId = errorCodeId.replace('existing_', '');
}
response = await updateErrorCode(currentBrandId, cleanErrorCodeId, payload);
} else {
response = await createErrorCode(currentBrandId, payload);
}
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: isEdit ? 'Error Code berhasil diupdate!' : 'Error Code berhasil ditambahkan!',
});
if (isFromAddBrand) {
navigate(`/master/brand-device/add`);
} else {
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`, {
state: { refreshErrorCodes: true }
});
}
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menyimpan error code',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan error code. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal menyimpan error code. Silakan coba lagi.',
});
}
};
const handleCancel = () => {
if (isFromAddBrand) {
navigate(`/master/brand-device/add`);
} else {
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
}
};
const handleErrorCodeIconUpload = (iconData) => {
if (!iconData || !iconData.uploadPath) {
return null;
}
const formattedIconData = {
name: iconData.name,
uploadPath: iconData.uploadPath,
url: iconData.uploadPath,
};
setErrorCodeIcon(formattedIconData);
return formattedIconData;
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const handleSolutionFileUpload = (fileObject) => {
};
const resetForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
});
setErrorCodeIcon(null);
resetSolutionFields();
setSelectedSparepartIds([]);
};
return (
<Card>
{/* Header */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24
}}>
<Title level={4} style={{ margin: 0 }}>
{isEdit ? 'Edit Error Code' : 'Add Error Code'}
</Title>
<Button
icon={<ArrowLeftOutlined />}
onClick={handleCancel}
>
Back to Brand Device
</Button>
</div>
{/* Content */}
<div style={{ position: 'relative', minHeight: 500 }}>
{loading && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.7)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
}}
>
<Spin size="large" />
</div>
)}
<Row gutter={[24, 24]}>
{/* Error Code Form */}
<Col xs={24} lg={8}>
<Card
title="Error Code Details"
size="small"
style={{ height: 'fit-content' }}
>
<Form
form={errorCodeForm}
layout="vertical"
initialValues={{
status: true,
error_code_color: '#000000',
}}
>
<ErrorCodeForm
errorCodeForm={errorCodeForm}
isErrorCodeFormReadOnly={false}
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={handleErrorCodeIconUpload}
onErrorCodeIconRemove={handleErrorCodeIconRemove}
isEdit={isEdit}
/>
</Form>
</Card>
</Col>
{/* Solutions Form */}
<Col xs={24} lg={8}>
<Card
title="Solutions"
size="small"
style={{ height: 'fit-content' }}
>
<Form
form={solutionForm}
layout="vertical"
initialValues={{}}
>
<SolutionForm
solutionForm={solutionForm}
solutionFields={solutionFields}
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
firstSolutionValid={firstSolutionValid}
checkFirstSolutionValid={() => {
return checkFirstSolutionValid();
}}
onAddSolutionField={handleAddSolutionField}
onRemoveSolutionField={handleRemoveSolutionField}
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
onSolutionFileUpload={handleSolutionFileUpload}
onFileView={(fileData) => {
if (fileData && fileData.url) {
window.open(fileData.url, '_blank');
}
}}
isReadOnly={false}
/>
</Form>
</Card>
</Col>
{/* Sparepart Selection */}
<Col xs={24} lg={8}>
<Card
title="Spareparts"
size="small"
style={{ height: 'fit-content' }}
>
<SparepartSelect
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={setSelectedSparepartIds}
isReadOnly={false}
brandId={currentBrandId}
/>
</Card>
</Col>
</Row>
{/* Save Button */}
<div style={{ marginTop: 24, textAlign: 'right' }}>
<Button
type="primary"
loading={confirmLoading}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
onClick={handleSave}
>
{isEdit ? 'Update Error Code' : 'Save Error Code'}
</Button>
</div>
</div>
</Card>
);
};
export default AddEditErrorCode;

View File

@@ -13,17 +13,17 @@ import {
Tag,
Space,
Input,
ConfigProvider
} from 'antd';
import { EyeOutlined, EditOutlined, DeleteOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById, getErrorCodesByBrandId, getErrorCodeById, deleteErrorCode, updateErrorCode as updateErrorCodeAPI, createErrorCode as createErrorCodeAPI } from '../../../api/master-brand';
import { getBrandById, updateBrand, getErrorCodesByBrandId, getErrorCodeById, deleteErrorCode, updateErrorCode as updateErrorCodeAPI, createErrorCode as createErrorCodeAPI } from '../../../api/master-brand';
import { getFileUrl } from '../../../api/file-uploads';
import { SendRequest } from '../../../components/Global/ApiRequest';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import SolutionForm from './component/SolutionForm';
import FormActions from './component/FormActions';
import SparepartSelect from './component/SparepartSelect';
import ListErrorCode from './component/ListErrorCode';
@@ -57,6 +57,7 @@ const EditBrandDevice = () => {
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
const [currentSolutionData, setCurrentSolutionData] = useState([]);
const [confirmLoading, setConfirmLoading] = useState(false);
const getSolutionData = () => {
if (!solutionForm) return [];
@@ -115,7 +116,7 @@ const EditBrandDevice = () => {
const isFileType = solution.type_solution && solution.type_solution !== 'text';
newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text';
newSolutionStatuses[fieldKey] = solution.is_active !== false;
newSolutionStatuses[fieldKey] = solution.is_active;
let fileObject = null;
if (isFileType && (solution.path_solution || solution.path_document)) {
@@ -135,7 +136,7 @@ const EditBrandDevice = () => {
name: solution.solution_name || '',
type: isFileType ? 'file' : 'text',
text: solution.text_solution || '',
status: solution.is_active !== false,
status: solution.is_active,
file: fileObject,
fileUpload: fileObject,
path_solution: solution.path_solution || solution.path_document || null,
@@ -387,17 +388,64 @@ const EditBrandDevice = () => {
const handleNextStep = async () => {
try {
await brandForm.validateFields();
const currentBrandId = id;
if (currentBrandId) {
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
setConfirmLoading(true);
const brandValues = brandForm.getFieldsValue();
if (!brandValues.brand_name || brandValues.brand_name.trim() === '') {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Brand Name wajib diisi!',
});
return;
}
if (!brandValues.brand_manufacture || brandValues.brand_manufacture.trim() === '') {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Manufacturer wajib diisi!',
});
return;
}
const brandApiData = {
brand_name: brandValues.brand_name.trim(),
brand_type: brandValues.brand_type || '',
brand_manufacture: brandValues.brand_manufacture.trim(),
brand_model: brandValues.brand_model || '',
is_active: brandValues.is_active !== undefined ? brandValues.is_active : true
};
const response = await updateBrand(id, brandApiData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Brand device berhasil diupdate.',
});
const currentBrandId = id;
if (currentBrandId) {
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
}
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal mengupdate brand device',
});
}
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal mengupdate brand device',
});
} finally {
setConfirmLoading(false);
}
};
@@ -433,6 +481,7 @@ const EditBrandDevice = () => {
const handleSaveErrorCode = async () => {
try {
setConfirmLoading(true);
const errorCodeValues = await errorCodeForm.validateFields();
const solutionData = getSolutionData();
@@ -542,6 +591,8 @@ const EditBrandDevice = () => {
title: 'Perhatian',
message: error.message || 'Harap isi semua kolom wajib!',
});
} finally {
setConfirmLoading(false);
}
};
@@ -1165,6 +1216,7 @@ const EditBrandDevice = () => {
type="primary"
size="large"
onClick={handleSaveErrorCode}
loading={confirmLoading}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
@@ -1197,13 +1249,23 @@ const EditBrandDevice = () => {
};
return (
<Card>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
{renderStepContent()}
<Divider />
<ConfigProvider
theme={{
components: {
Switch: {
colorPrimary: '#23A55A',
colorPrimaryHover: '#23A55A',
},
},
}}
>
<Card>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
{renderStepContent()}
<Divider />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
{currentStep === 1 && (
@@ -1220,7 +1282,7 @@ const EditBrandDevice = () => {
<Button
type="primary"
onClick={handleNextStep}
loading={loading}
loading={confirmLoading}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
@@ -1243,7 +1305,8 @@ const EditBrandDevice = () => {
)}
</div>
</div>
</Card>
</Card>
</ConfigProvider>
);
};

View File

@@ -1,70 +0,0 @@
import React from 'react';
import { Button, ConfigProvider } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
const FormActions = ({
currentStep,
onPreviousStep,
onNextStep,
onSave,
onCancel,
confirmLoading,
isEditMode = false,
showCancelButton = true
}) => {
return (
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
{showCancelButton && (
<Button onClick={onCancel}>Batal</Button>
)}
{currentStep > 0 && (
<Button onClick={onPreviousStep} style={{ marginRight: 8 }}>
Kembali
</Button>
)}
</ConfigProvider>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
{currentStep < 1 && (
<Button loading={confirmLoading} onClick={onNextStep}>
Lanjut
</Button>
)}
{currentStep === 1 && (
<Button loading={confirmLoading} onClick={onSave}>
{isEditMode ? 'Update' : 'Simpan'}
</Button>
)}
</ConfigProvider>
</div>
);
};
export default FormActions;