update ui brand device

This commit is contained in:
2025-12-04 01:17:25 +07:00
parent 1bc98de564
commit f22e120204
15 changed files with 3132 additions and 1349 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Divider,
Typography,
@@ -10,53 +10,54 @@ import {
Col,
Card,
Spin,
Table,
Tag,
Space,
Input,
} from 'antd';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { EyeOutlined, EditOutlined, DeleteOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
import TableList from '../../../components/Global/TableList';
import { ConfigProvider } from 'antd';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { createBrand } from '../../../api/master-brand';
import BrandForm from './component/BrandForm';
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
import SolutionForm from './component/SolutionForm';
import FormActions from './component/FormActions';
import ListErrorCode from './component/ListErrorCode';
import { useErrorCodeLogic } from './hooks/errorCode';
import { useSolutionLogic } from './hooks/solution';
import { EditOutlined, DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic';
import { useBrandForm } from '../../../context/BrandFormContext';
const { Title } = Typography;
const { Step } = Steps;
const AddBrandDevice = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandForm] = Form.useForm();
const [errorCodeForm] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
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 [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const [loading, setLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [searchText, setSearchText] = useState('');
const [trigerFilter, setTrigerFilter] = useState(false);
const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic(
errorCodeForm,
[]
);
// Context integration
const {
brandId,
brandInfo,
setBrandInfo,
tempErrorCodes,
addErrorCode,
updateErrorCode,
deleteErrorCode,
prepareSubmissionData,
validateForm,
resetForm,
} = useBrandForm();
// Use step from query parameter or context
const tab = searchParams.get('tab');
const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0);
const {
solutionFields,
@@ -64,7 +65,6 @@ const AddBrandDevice = () => {
solutionStatuses,
solutionsToDelete,
firstSolutionValid,
checkFirstSolutionValid,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
@@ -74,6 +74,354 @@ const AddBrandDevice = () => {
setSolutionsForExistingRecord,
} = useSolutionLogic(solutionForm);
// Navigation functions
const handleNextStep = async () => {
try {
await brandForm.validateFields();
setCurrentStep(1);
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
});
}
};
const handlePrevStep = () => {
setCurrentStep(0);
};
const handleCancel = () => {
navigate('/master/brand-device');
};
const handleAddErrorCode = () => {
navigate(`/master/brand-device/add/error-code/add`);
};
const handleEditErrorCodeNavigate = (record) => {
const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId;
if (errorCodeId) {
navigate(`/master/brand-device/add/error-code/edit/${errorCodeId}`);
}
};
const handleDeleteErrorCode = (record) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi Hapus',
message: `Apakah Anda yakin ingin menghapus error code "${record.error_code}"?`,
onConfirm: () => {
const tempId = record.tempId || `existing_${record.error_code_id}`;
deleteErrorCode(tempId, false); // false = soft delete
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!',
});
setTrigerFilter(prev => !prev);
},
onCancel: () => {}
});
};
const handlePreviewErrorCode = (record) => {
console.log('Preview error code:', record);
};
const handleSearch = () => {
setSearchText(searchValue);
setTrigerFilter((prev) => !prev);
};
const handleSearchClear = () => {
setSearchValue('');
setSearchText('');
setTrigerFilter((prev) => !prev);
};
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
setBrandInfo(allValues);
}, [setBrandInfo]);
const getErrorCodesData = async (params) => {
try {
const search = params.get('search') || '';
const page = parseInt(params.get('page')) || 1;
const limit = parseInt(params.get('limit')) || 10;
const allErrorCodes = tempErrorCodes.filter(ec => ec.status !== 'deleted');
let filteredData = allErrorCodes;
if (searchText) {
filteredData = allErrorCodes.filter(ec =>
ec.error_code.toLowerCase().includes(searchText.toLowerCase()) ||
ec.error_code_name.toLowerCase().includes(searchText.toLowerCase())
);
}
const startIndex = 0;
const endIndex = startIndex + limit;
const paginatedData = filteredData.slice(startIndex, endIndex);
return {
data: paginatedData,
pagination: {
current_page: page,
current_limit: limit,
total_limit: filteredData.length,
total_page: Math.ceil(filteredData.length / limit),
}
};
} catch (error) {
console.error('Error getting error codes data:', error);
return {
data: [],
pagination: {
current_page: 1,
current_limit: 10,
total_limit: 0,
total_page: 0,
}
};
}
};
// Error code columns
const errorCodeColumns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Error Code',
dataIndex: 'error_code',
key: 'error_code',
width: '20%',
render: (text, record) => (
<Space>
{text}
{record.status === 'new' && <Tag color="green">New</Tag>}
</Space>
),
},
{
title: 'Error Name',
dataIndex: 'error_code_name',
key: 'error_code_name',
width: '25%',
},
{
title: 'Description',
dataIndex: 'error_code_description',
key: 'error_code_description',
width: '30%',
ellipsis: true,
},
{
title: 'Actions',
key: 'actions',
width: '20%',
render: (_, record) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showPreviewModal(record)}
size="small"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => showEditModal(record)}
size="small"
/>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => showDeleteDialog(record)}
size="small"
/>
</Space>
),
},
];
// Query params for table
const queryParams = useMemo(() => {
const params = new URLSearchParams();
params.set('page', '1');
params.set('limit', '10');
if (searchValue) {
params.set('search', searchValue);
}
return params;
}, [searchValue]);
const handleFinish = async () => {
setConfirmLoading(true);
try {
// Validate form using context
const validation = validateForm();
if (!validation.isValid) {
return;
}
const submissionData = prepareSubmissionData(1);
const response = await createBrand(submissionData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Brand Device berhasil ditambahkan.',
});
resetForm();
navigate('/master/brand-device');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menambahkan Brand Device',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
<div style={{ position: 'relative' }}>
{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,
borderRadius: '8px',
}}
>
<Spin size="large" />
</div>
)}
<BrandForm
form={brandForm}
onValuesChange={handleBrandFormValuesChange}
isEdit={false}
showSparepartSection={true}
/>
</div>
);
}
if (currentStep === 1) {
return (
<Card>
<Row>
<Col xs={24}>
<Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Search error codes..."
value={searchText}
onChange={(e) => {
const value = e.target.value;
setSearchText(value);
setSearchValue(value);
if (value === '') {
setTrigerFilter((prev) => !prev);
}
}}
onSearch={handleSearch}
allowClear={{
clearIcon: <span onClick={handleSearchClear}></span>,
}}
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Search
</Button>
}
size="large"
/>
</Col>
<Col>
<Space wrap size="small">
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={handleAddErrorCode}
size="large"
>
Add Error Code
</Button>
</ConfigProvider>
</Space>
</Col>
</Row>
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList
mobile
cardColor={'#42AAFF'}
header={'error_code'}
showPreviewModal={handlePreviewErrorCode}
showEditModal={handleEditErrorCodeNavigate}
showDeleteDialog={handleDeleteErrorCode}
getData={getErrorCodesData}
queryParams={queryParams}
columns={errorCodeColumns(handlePreviewErrorCode, handleEditErrorCodeNavigate, handleDeleteErrorCode)}
triger={trigerFilter}
/>
</Col>
</Row>
</Card>
);
}
return null;
};
useEffect(() => {
setBreadcrumbItems([
{
@@ -99,390 +447,6 @@ const AddBrandDevice = () => {
]);
}, [setBreadcrumbItems, navigate]);
const handleCancel = () => {
navigate('/master/brand-device');
};
const handleNextStep = async () => {
try {
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({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
});
}
};
const handleFinish = async () => {
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 || '#ad4141ff',
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 createBrand(brandData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Brand Device berhasil ditambahkan.',
});
navigate('/master/brand-device');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menambahkan Brand Device',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
};
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);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
} else {
resetSolutionFields();
}
};
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 formElement = document.querySelector('.ant-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const handleAddErrorCode = async () => {
try {
const errorCodeValues = await errorCodeForm.validateFields();
const solutionData = getSolutionData();
// 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',
message: 'Setiap error code harus memiliki minimal 1 solution!',
});
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 = {
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 || '',
is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: solutionData,
errorCodeIcon: errorCodeIcon,
key: editingErrorCodeKey || `temp-${Date.now()}`,
};
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);
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!',
});
}
};
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
});
setErrorCodeIcon(null);
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handleDeleteErrorCode = async (key) => {
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return;
}
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
setErrorCodes(updatedErrorCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!',
});
};
const handleCreateNewErrorCode = () => {
resetErrorCodeForm();
resetSolutionFields();
setErrorCodeIcon(null);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handleErrorCodeIconUpload = (iconData) => {
setErrorCodeIcon(iconData);
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
<BrandForm
form={brandForm}
formData={formData}
onValuesChange={(changedValues, allValues) =>
setFormData((prev) => ({ ...prev, ...allValues }))
}
isEdit={false}
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={setSelectedSparepartIds}
showSparepartSection={true}
/>
);
}
if (currentStep === 1) {
return (
<>
<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"
initialValues={{
status: true,
}}
>
<ErrorCodeSimpleForm
errorCodeForm={errorCodeForm}
isErrorCodeFormReadOnly={isErrorCodeFormReadOnly}
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={handleErrorCodeIconUpload}
onErrorCodeIconRemove={handleErrorCodeIconRemove}
onAddErrorCode={handleAddErrorCode}
/>
</Form>
</Card>
</Col>
<Col span={6}>
<Card
title={
<Title level={5} style={{ margin: 0 }}>
Solutions
</Title>
}
size="small"
>
<Form
form={solutionForm}
layout="vertical"
initialValues={{
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}
/>
</Form>
</Card>
</Col>
<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>
</>
);
}
return null;
};
return (
<Card>
<Title level={4} style={{ margin: '0 0 24px 0' }}>
@@ -524,15 +488,48 @@ const AddBrandDevice = () => {
</div>
</div>
<Divider />
<FormActions
currentStep={currentStep}
onPreviousStep={() => setCurrentStep(currentStep - 1)}
onNextStep={handleNextStep}
onSave={handleFinish}
onCancel={handleCancel}
confirmLoading={confirmLoading}
isEditMode={false}
/>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
<Button onClick={handleCancel}>
Cancel
</Button>
{currentStep === 1 && (
<Button
onClick={handlePrevStep}
style={{ marginLeft: 8 }}
>
Back to Brand Info
</Button>
)}
</div>
<div>
{currentStep === 0 && (
<Button
type="primary"
onClick={handleNextStep}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Next to Error Codes
</Button>
)}
{currentStep === 1 && (
<Button
type="primary"
onClick={handleFinish}
loading={confirmLoading}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Save Brand Device
</Button>
)}
</div>
</div>
</Card>
);
};