integration api brandDevice, file upload in brand device

This commit is contained in:
2025-10-28 11:07:54 +07:00
parent 39d8be10cc
commit 47f7c7b682
15 changed files with 3044 additions and 430 deletions

View File

@@ -14,9 +14,13 @@ import IndexDevice from './pages/master/device/IndexDevice';
import IndexTag from './pages/master/tag/IndexTag';
import IndexUnit from './pages/master/unit/IndexUnit';
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
import IndexStatus from './pages/master/status/IndexStatus';
import IndexShift from './pages/master/shift/IndexShift';
// Brand device
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice';
import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice';
import ViewFilePage from './pages/master/brandDevice/ViewFilePage';
// Jadwal Shift
import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift';
@@ -74,6 +78,11 @@ const App = () => {
<Route path="unit" element={<IndexUnit />} />
<Route path="brand-device" element={<IndexBrandDevice />} />
<Route path="brand-device/add" element={<AddBrandDevice />} />
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
<Route path="brand-device/view/:id" element={<ViewBrandDevice />} />
<Route path="brand-device/edit/:id/files/:fileType/:fileName" element={<ViewFilePage />} />
<Route path="brand-device/view/:id/files/:fileType/:fileName" element={<ViewFilePage />} />
<Route path="brand-device/view/temp/files/:fileName" element={<ViewFilePage />} />
<Route path="plant-sub-section" element={<IndexPlantSubSection />} />
<Route path="shift" element={<IndexShift />} />
<Route path="status" element={<IndexStatus />} />

126
src/api/file-uploads.jsx Normal file
View File

@@ -0,0 +1,126 @@
import { SendRequest } from '../components/Global/ApiRequest';
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_SERVER;
// Get file from uploads directory
const getFile = async (folder, filename) => {
const response = await axios.get(`${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`, {
responseType: 'blob'
});
return response.data;
};
// Download file as blob with proper handling
const downloadFile = async (folder, filename) => {
try {
const response = await getFile(folder, filename);
const blob = new Blob([response], {
type: 'application/octet-stream'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return { success: true, filename };
} catch (error) {
console.error('Error downloading file:', error);
throw error;
}
};
// Get file info (metadata)
const getFileInfo = async (folder, filename) => {
const response = await SendRequest({
method: 'head',
prefix: `file-uploads/${folder}/${encodeURIComponent(filename)}`
});
if (response.error) {
throw new Error(response.message);
}
return {
contentType: response.headers?.['content-type'],
contentLength: response.headers?.['content-length'],
lastModified: response.headers?.['last-modified'],
filename: filename,
folder: folder
};
};
// Get file URL for iframe
const getFileUrl = (folder, filename) => {
const token = localStorage.getItem('token');
if (token) {
return `${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}?token=${encodeURIComponent(token)}`;
}
return `${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`;
};
// Check if file exists
const checkFileExists = async (folder, filename) => {
const response = await SendRequest({
method: 'head',
prefix: `file-uploads/${folder}/${encodeURIComponent(filename)}`
});
if (response.error && response.statusCode === 404) {
return false;
} else if (response.error) {
throw new Error(response.message);
}
return true;
};
const getFileType = (filename) => {
const ext = filename.split('.').pop().toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const pdfExtensions = ['pdf'];
if (imageExtensions.includes(ext)) {
return 'image';
} else if (pdfExtensions.includes(ext)) {
return 'pdf';
}
return 'unknown';
};
// Upload file to server
const uploadFile = async (file, folder) => {
const formData = new FormData();
formData.append('file', file);
formData.append('folder', folder);
const response = await SendRequest({
method: 'post',
prefix: 'file-uploads',
params: formData
});
return response.data;
};
const getFolderFromFileType = (fileType) => {
return fileType === 'pdf' ? 'pdf' : 'images';
};
export {
getFile,
downloadFile,
getFileInfo,
getFileUrl,
checkFileExists,
getFileType,
getFolderFromFileType,
uploadFile
};

View File

@@ -1,22 +1,26 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Input, Divider, Typography, Switch, Button, Steps, Form, message, Table, Row, Col, Modal, Card, Tag, Upload, ConfigProvider, Space } from 'antd';
import { PlusOutlined, UploadOutlined, EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { Divider, Typography, Button, Steps, Form, Row, Col, Card } from 'antd';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { createBrand } from '../../../api/master-brand';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import ErrorCodeTable from './component/ListErrorCode';
import FormActions from './component/FormActions';
import { useErrorCodeLogic } from './hooks/errorCode';
import { uploadFile, getFolderFromFileType } from '../../../api/file-uploads';
const { Text, Title } = Typography;
const { Title } = Typography;
const { Step } = Steps;
// Mock API for Error Codes (can be moved to a separate file later)
const mockErrorCodeApi = {
errorCodes: [],
createErrorCode: async (data) => {
const newId = mockErrorCodeApi.errorCodes.length > 0 ? Math.max(...mockErrorCodeApi.errorCodes.map(ec => ec.error_code_id)) + 1 : 1;
const newErrorCode = { ...data, error_code_id: newId };
mockErrorCodeApi.errorCodes.push(newErrorCode);
return { statusCode: 201, data: newErrorCode };
},
const defaultData = {
brand_name: '',
brand_type: '',
brand_model: '',
brand_manufacture: '',
is_active: true,
brand_code: '',
};
const AddBrandDevice = () => {
@@ -27,29 +31,37 @@ const AddBrandDevice = () => {
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [fileList, setFileList] = useState([]);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); // State untuk mengontrol form read-only
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); // State untuk melacak item yang sedang diedit
// State untuk preview file
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
// Watch for form values changes to update the switch color
const statusValue = Form.useWatch('status', errorCodeForm);
const defaultData = {
brandName: '',
brandType: '',
model: '',
manufacturer: '',
status: true,
brand_code: '',
description: '',
};
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState(defaultData);
const [errorCodes, setErrorCodes] = useState([]);
const {
solutionFields,
solutionTypes,
solutionStatuses,
firstSolutionValid,
solutionsToDelete,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
handleSolutionStatusChange,
resetSolutionFields,
checkFirstSolutionValid,
setSolutionsForExistingRecord
} = useErrorCodeLogic(errorCodeForm, fileList);
useEffect(() => {
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' }}>Tambah Brand Device</span> }
]);
}, [setBreadcrumbItems, navigate]);
const handleCancel = () => {
navigate('/master/brand-device');
};
@@ -59,290 +71,294 @@ const AddBrandDevice = () => {
await brandForm.validateFields();
setCurrentStep(1);
} catch (error) {
console.log('Validate Failed:', error);
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' });
}
};
const handleFinish = async () => {
if (errorCodes.length === 0) {
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Silakan tambahkan minimal satu error code.' });
return;
}
setConfirmLoading(true);
try {
const finalFormData = { ...formData, status: formData.status ? 'Active' : 'Inactive' };
console.log("Saving brand device:", finalFormData);
await new Promise((resolve) => setTimeout(resolve, 500));
const newBrandDeviceId = Date.now();
console.log("Brand device saved with ID:", newBrandDeviceId);
const transformedErrorCodes = errorCodes.map(ec => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
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
}))
}));
console.log("Saving error codes:", errorCodes);
for (const errorCode of errorCodes) {
if (errorCode.another_solution === 'image' && errorCode.image) {
console.log(`Uploading image for error code ${errorCode.error_code}:`, errorCode.image.name);
}
await mockErrorCodeApi.createErrorCode({
...errorCode,
brand_device_id: newBrandDeviceId
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: transformedErrorCodes.length > 0 ? transformedErrorCodes : [
{
error_code: "DEFAULT",
error_code_name: "Default Error Code",
error_code_description: "Default error description",
is_active: true,
solution: [
{
solution_name: "Default Solution",
type_solution: "text",
text_solution: "Default solution text",
path_solution: "",
is_active: true
}
]
}
]
};
const response = await createBrand(finalFormData);
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',
});
console.log("Saved error code:", errorCode.error_code);
}
setConfirmLoading(false);
NotifOk({ icon: 'success', title: 'Berhasil', message: 'Brand Device dan Error Code berhasil disimpan.' });
navigate('/master/brand-device');
} catch (error) {
setConfirmLoading(false);
console.error("Failed to save data:", error);
NotifAlert({
icon: "error",
title: "Gagal",
message: "Gagal menyimpan data. Silakan coba lagi.",
message: error.message || "Gagal menyimpan data. Silakan coba lagi.",
});
} finally {
setConfirmLoading(false);
}
};
const handlePreviewErrorCode = (record) => {
errorCodeForm.setFieldsValue(record); // Isi form dengan data record
setFileList(record.fileList || []); // Muat file jika ada
setIsErrorCodeFormReadOnly(true); // Jadikan form read-only
setEditingErrorCodeKey(null); // Bukan dalam mode edit
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
status: record.status,
});
setFileList(record.fileList || []);
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(null);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, errorCodeForm);
}
};
const handleEditErrorCode = (record) => {
errorCodeForm.setFieldsValue(record); // Isi form dengan data record
setFileList(record.fileList || []); // Muat file jika ada
setIsErrorCodeFormReadOnly(false); // Aktifkan form untuk diedit
setEditingErrorCodeKey(record.key); // Tandai item ini sebagai yang sedang diedit
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
status: record.status,
});
setFileList(record.fileList || []);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, errorCodeForm);
}
};
const handleAddErrorCode = async () => {
try {
const values = await errorCodeForm.validateFields();
const newErrorCode = {
...values,
status: values.status === undefined ? true : values.status,
fileList: fileList,
key: editingErrorCodeKey || `temp-${Date.now()}` // Gunakan key yang ada jika edit, jika tidak buat baru
};
if (editingErrorCodeKey) {
setErrorCodes(errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item));
message.success('Error code berhasil diupdate');
} else {
setErrorCodes([...errorCodes, newErrorCode]);
message.success('Error code berhasil ditambahkan');
}
errorCodeForm.resetFields();
setFileList([]);
} catch (error) {
console.log('Validate Failed:', error);
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' });
const handleAddErrorCode = async (newErrorCode) => {
if (editingErrorCodeKey) {
const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
setErrorCodes(updatedCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil diupdate!'
});
} else {
const updatedCodes = [...errorCodes, newErrorCode];
setErrorCodes(updatedCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil ditambahkan!'
});
}
setIsErrorCodeFormReadOnly(false); // Reset status read-only
setEditingErrorCodeKey(null); // Reset key item yang diedit
resetErrorCodeForm();
};
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text'
});
setFileList([]);
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handleCreateNewErrorCode = () => {
resetErrorCodeForm();
};
const handleDeleteErrorCode = (key) => {
setErrorCodes(errorCodes.filter(item => item.key !== key));
message.success('Error code berhasil dihapus');
};
// Fungsi untuk mengubah file menjadi base64 untuk preview gambar
const getBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
// Fungsi untuk menangani preview file dari komponen Upload
const handleUploadPreview = async (file) => {
// Jika file bukan gambar, buka di tab baru
if (!file.type.startsWith('image/')) {
const url = URL.createObjectURL(file.originFileObj || file);
window.open(url, '_blank');
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!'
});
return;
}
// Jika file adalah gambar, tampilkan di modal
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj || file);
}
setPreviewImage(file.url || file.preview);
setPreviewOpen(true);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
setErrorCodes(errorCodes.filter(item => item.key !== key));
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!'
});
};
const uploadProps = {
multiple: true,
accept: '.pdf,.jpg,.jpeg,.png,.gif',
onRemove: (file) => {
const newFileList = fileList.filter(item => item.uid !== file.uid);
setFileList(newFileList);
},
beforeUpload: (file) => {
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) {
message.error(`${file.name} bukan file PDF atau gambar yang diizinkan.`);
return Upload.LIST_IGNORE;
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`
});
return;
}
setFileList(prevList => [...prevList, file]);
return false; // Prevent auto-upload
},
fileList,
onPreview: handleUploadPreview, // Tambahkan handler onPreview
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 errorCodeColumns = [
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
{ title: 'Trouble Description', dataIndex: 'description', key: 'description' },
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status) => (
<Tag color={status ? '#23A55A' : 'red'}>
{status ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space>
<Button type="text" icon={<EyeOutlined />} onClick={() => handlePreviewErrorCode(record)} style={{ color: '#1890ff', borderColor: '#1890ff' }} />
<Button type="text" icon={<EditOutlined />} onClick={() => handleEditErrorCode(record)} style={{ color: '#faad14', borderColor: '#faad14' }} />
<Button danger type="text" icon={<DeleteOutlined />} onClick={() => handleDeleteErrorCode(record.key)} style={{ borderColor: '#ff4d4f' }} />
</Space>
),
},
];
useEffect(() => {
brandForm.setFieldsValue(formData);
}, [formData, brandForm]);
useEffect(() => {
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> },
{ title: <Text strong style={{ fontSize: '14px' }} onClick={() => navigate('/master/brand-device')}>Brand Device</Text> },
{ title: <Text strong style={{ fontSize: '14px' }}>Tambah Brand Device</Text> }
]);
}, [setBreadcrumbItems, navigate]);
const handleFileRemove = (file) => {
const newFileList = fileList.filter(item => item.uid !== file.uid);
setFileList(newFileList);
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
<Form layout="vertical" form={brandForm} onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))} initialValues={formData}>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="status" valuePropName="checked" noStyle>
<Switch
checked={formData.status}
style={{ backgroundColor: formData.status ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>{formData.status ? 'Active' : 'Inactive'}</Text>
</div>
</Form.Item>
<Form.Item label="Brand Code" name="brand_code">
<Input placeholder={'Brand Code Auto Fill'} disabled style={{ backgroundColor: '#f5f5f5', cursor: 'not-allowed' }} />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Brand Name" name="brandName" rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Manufacturer" name="manufacturer" 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="brandType">
<Input placeholder="Enter Brand Type (Optional)" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Model" name="model">
<Input placeholder="Enter Model (Optional)" />
</Form.Item>
</Col>
</Row>
<Form.Item label="Description" name="description">
<Input.TextArea rows={4} placeholder="Enter Description (Optional)" />
</Form.Item>
</Form>
<BrandForm
form={brandForm}
formData={formData}
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
isEdit={false}
/>
);
}
if (currentStep === 1) {
return (
<Row gutter={24}>
<Col span={12}>
<Title level={5} style={{ marginBottom: 16 }}>Tambah Error Code</Title>
<Form form={errorCodeForm} layout="vertical" initialValues={{ status: true }}>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="status" valuePropName="checked" noStyle>
<Switch
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>{statusValue ? 'Active' : 'Inactive'}</Text>
</div>
</Form.Item>
<Form.Item name="error_code" label="Error Code" rules={[{ required: true, message: 'Error Code wajib diisi' }]}>
<Input disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item name="description" label="Trouble Description" rules={[{ required: true, message: 'Trouble Description wajib diisi' }]}>
<Input.TextArea disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item name="what_action_to_take" label="What Action to Take">
<Input.TextArea placeholder="Enter action to take (Optional)" disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item label="Upload File (Opsional)">
<Upload {...uploadProps} disabled={isErrorCodeFormReadOnly}>
<Button icon={<UploadOutlined />} disabled={isErrorCodeFormReadOnly}>
Click to Upload (File or Image)
</Button>
</Upload>
</Form.Item>
<Form.Item style={{ textAlign: 'right', marginTop: 24 }}>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={handleAddErrorCode}
>
{editingErrorCodeKey ? 'Update Error Code' : 'Tambah Error Code'}
</Button>
</ConfigProvider>
</Form.Item>
<Title level={5} style={{ marginBottom: 16 }}>
{isErrorCodeFormReadOnly
? 'View Error Code'
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
}
</Title>
<Form
form={errorCodeForm}
layout="vertical"
initialValues={{ status: true, solution_status_0: true, solution_type_0: 'text' }}
onValuesChange={checkFirstSolutionValid}
>
<ErrorCodeForm
errorCodeForm={errorCodeForm}
isErrorCodeFormReadOnly={isErrorCodeFormReadOnly}
editingErrorCodeKey={editingErrorCodeKey}
solutionFields={solutionFields}
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
fileList={fileList}
solutionsToDelete={solutionsToDelete}
firstSolutionValid={firstSolutionValid}
onAddErrorCode={handleAddErrorCode}
onAddSolutionField={handleAddSolutionField}
onRemoveSolutionField={handleRemoveSolutionField}
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
onSolutionFileUpload={handleSolutionFileUpload}
onFileView={handleFileView}
onCreateNewErrorCode={handleCreateNewErrorCode}
onResetForm={resetErrorCodeForm}
errorCodes={errorCodes}
/>
</Form>
</Col>
<Col span={12}>
<Title level={5}>Daftar Error Code ({errorCodes.length})</Title>
<Table columns={errorCodeColumns} dataSource={errorCodes} rowKey="key" pagination={false} />
<ErrorCodeTable
errorCodes={errorCodes}
loading={loading}
onPreview={handlePreviewErrorCode}
onEdit={handleEditErrorCode}
onDelete={handleDeleteErrorCode}
onFileView={handleFileView}
/>
</Col>
</Row>
);
@@ -352,8 +368,7 @@ const AddBrandDevice = () => {
return (
<Card>
<Title level={4}>Tambah Brand Device</Title>
<Divider />
<Title level={4} style={{ margin: '0 0 24px 0' }}>Tambah Brand Device</Title>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes" />
@@ -362,57 +377,15 @@ const AddBrandDevice = () => {
{renderStepContent()}
</div>
<Divider />
<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',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
{currentStep > 0 && (
<Button onClick={() => setCurrentStep(currentStep - 1)} style={{ marginRight: 8 }}>Kembali</Button>
)}
</ConfigProvider>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652', // A slightly darker shade for hover
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
{currentStep < 1 && (
<Button loading={confirmLoading} onClick={handleNextStep}>Lanjut</Button>
)}
{currentStep === 1 && (
<Button loading={confirmLoading} onClick={handleFinish}>Simpan</Button>
)}
</ConfigProvider>
</div>
{/* Modal untuk preview gambar */}
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={() => setPreviewOpen(false)}
>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
<FormActions
currentStep={currentStep}
onPreviousStep={() => setCurrentStep(currentStep - 1)}
onNextStep={handleNextStep}
onSave={handleFinish}
onCancel={handleCancel}
confirmLoading={confirmLoading}
isEditMode={false}
/>
</Card>
);
};

View File

@@ -0,0 +1,445 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { Divider, Typography, Button, Steps, Form, Row, Col, Card, Spin, Modal } from 'antd';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById, updateBrand } from '../../../api/master-brand';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import ErrorCodeTable from './component/ListErrorCode';
import FormActions from './component/FormActions';
import { useErrorCodeLogic } from './hooks/errorCode';
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 {
solutionFields,
solutionTypes,
solutionStatuses,
firstSolutionValid,
solutionsToDelete,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
handleSolutionStatusChange,
resetSolutionFields,
checkFirstSolutionValid,
setSolutionsForExistingRecord
} = useErrorCodeLogic(errorCodeForm, fileList);
useEffect(() => {
const fetchBrandData = async () => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
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', cursor: 'pointer' }} onClick={() => navigate('/master/brand-device')}>Brand Device</span>
},
{ title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>Edit Brand Device</span> }
]);
try {
setLoading(true);
const response = await getBrandById(id);
if (response && response.statusCode === 200) {
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,
is_active: brandData.is_active,
brand_code: brandData.brand_code,
};
const existingErrorCodes = brandData.error_code ? brandData.error_code.map((ec, index) => ({
key: `existing-${ec.error_code_id}`,
error_code_id: ec.error_code_id,
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
status: ec.is_active,
solution: ec.solution || []
})) : [];
setFormData(newFormData);
brandForm.setFieldsValue(newFormData);
setErrorCodes(existingErrorCodes);
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: response?.message || 'Failed to fetch brand device data',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Failed to fetch brand device data',
});
} finally {
setLoading(false);
}
};
fetchBrandData();
}, [id, setBreadcrumbItems, navigate, brandForm, location]);
const handleCancel = () => {
localStorage.removeItem(`brand_device_edit_${id}_temp_data`);
navigate('/master/brand-device');
};
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 handleFinish = async () => {
setConfirmLoading(true);
try {
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 => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name || '',
error_code_description: ec.error_code_description || '',
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 response = await updateBrand(id, finalFormData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
localStorage.removeItem(`brand_device_edit_${id}_temp_data`);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Brand Device dan Error Codes berhasil diupdate.',
});
navigate('/master/brand-device');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal mengupdate Brand Device',
});
}
} catch (error) {
NotifAlert({
icon: "error",
title: "Gagal",
message: error.message || "Gagal mengupdate 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,
status: record.status,
});
setIsErrorCodeFormReadOnly(true);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, errorCodeForm);
}
};
const handleEditErrorCode = (record) => {
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name,
error_code_description: record.error_code_description,
status: record.status,
});
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.key);
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, errorCodeForm);
}
const formElement = document.querySelector('.ant-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const handleAddErrorCode = (newErrorCode) => {
let updatedErrorCodes;
if (editingErrorCodeKey) {
updatedErrorCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil diupdate!'
});
} else {
updatedErrorCodes = [...errorCodes, newErrorCode];
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil ditambahkan!'
});
}
setErrorCodes(updatedErrorCodes);
resetErrorCodeForm();
};
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text'
});
setFileList([]);
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();
};
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 renderStepContent = () => {
if (currentStep === 0) {
return (
<BrandForm
form={brandForm}
formData={formData}
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
isEdit={true}
/>
);
}
if (currentStep === 1) {
return (
<Row gutter={24}>
<Col span={12}>
<Title level={5} style={{ marginBottom: 16 }}>
{isErrorCodeFormReadOnly
? (editingErrorCodeKey ? 'View Error Code' : 'Error Code Form')
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
}
</Title>
<Form
form={errorCodeForm}
layout="vertical"
initialValues={{ status: true, solution_status_0: true, solution_type_0: 'text' }}
onValuesChange={checkFirstSolutionValid}
>
<ErrorCodeForm
errorCodeForm={errorCodeForm}
isErrorCodeFormReadOnly={isErrorCodeFormReadOnly}
editingErrorCodeKey={editingErrorCodeKey}
solutionFields={solutionFields}
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
fileList={fileList}
solutionsToDelete={solutionsToDelete}
firstSolutionValid={firstSolutionValid}
onAddErrorCode={handleAddErrorCode}
onAddSolutionField={handleAddSolutionField}
onRemoveSolutionField={handleRemoveSolutionField}
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
onSolutionFileUpload={handleSolutionFileUpload}
onFileView={handleFileView}
onCreateNewErrorCode={handleCreateNewErrorCode}
onResetForm={resetErrorCodeForm}
errorCodes={errorCodes}
/>
</Form>
</Col>
<Col span={12}>
<ErrorCodeTable
errorCodes={loading ?
Array.from({ length: 3 }, (_, index) => ({
key: `loading-${index}`,
error_code: 'Loading...',
error_code_name: 'Loading...',
solution: []
})) :
errorCodes
}
loading={loading}
onPreview={handlePreviewErrorCode}
onEdit={handleEditErrorCode}
onDelete={handleDeleteErrorCode}
onFileView={handleFileView}
/>
</Col>
</Row>
);
}
return null;
};
return (
<Card>
<Title level={4} style={{ margin: '0 0 24px 0' }}>Edit Brand Device</Title>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes" />
</Steps>
<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}
onPreviousStep={() => setCurrentStep(currentStep - 1)}
onNextStep={handleNextStep}
onSave={handleFinish}
onCancel={handleCancel}
confirmLoading={confirmLoading}
isEditMode={true}
/>
</Card>
);
};
export default EditBrandDevice;

View File

@@ -0,0 +1,551 @@
import React, { memo, useEffect, useState } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Table, Steps, Collapse, Switch, Skeleton, Spin, Modal } from 'antd';
import { ArrowLeftOutlined, EditOutlined, DeleteOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
import { getBrandById, deleteBrand } from '../../../api/master-brand';
import TableList from '../../../components/Global/TableList';
const { Title, Text } = Typography;
const { Step } = Steps;
const { Panel } = Collapse;
const ViewBrandDevice = () => {
const navigate = useNavigate();
const { id } = useParams();
const location = useLocation();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandData, setBrandData] = useState(null);
const [loading, setLoading] = useState(true);
const [currentStep, setCurrentStep] = useState(0);
const [errorCodesTriger, setErrorCodesTriger] = useState(0);
useEffect(() => {
const fetchBrandData = async () => {
const token = localStorage.getItem('token');
if (token) {
const savedPhase = location.state?.phase || localStorage.getItem(`brand_device_${id}_last_phase`);
if (savedPhase) {
setCurrentStep(parseInt(savedPhase));
localStorage.removeItem(`brand_device_${id}_last_phase`);
}
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> },
{
title: <Text strong style={{ fontSize: '14px' }} onClick={() => navigate('/master/brand-device')}>Brand Device</Text>
},
{ title: <Text strong style={{ fontSize: '14px' }}>View Brand Device</Text> }
]);
try {
setLoading(true);
const response = await getBrandById(id);
if (response && response.statusCode === 200) {
setBrandData(response.data);
setErrorCodesTriger(prev => prev + 1);
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: response?.message || 'Failed to fetch brand device data',
});
}
} catch (error) {
console.error('Fetch Brand Device Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Failed to fetch brand device data',
});
} finally {
setLoading(false);
}
} else {
navigate('/signin');
}
};
fetchBrandData();
}, [id, setBreadcrumbItems, navigate, location.state]);
// const handleEdit = () => {
// navigate(`/master/brand-device/edit/${id}`);
// };
// const handleDelete = () => {
// NotifConfirmDialog({
// icon: 'question',
// title: 'Konfirmasi Hapus',
// message: `Brand Device "${brandData?.brand_name}" akan dihapus?`,
// onConfirm: async () => {
// try {
// const response = await deleteBrand(id);
// if (response && response.statusCode === 200) {
// NotifOk({
// icon: 'success',
// title: 'Berhasil',
// message: response.message || 'Brand Device berhasil dihapus.',
// });
// navigate('/master/brand-device');
// } else {
// NotifAlert({
// icon: 'error',
// title: 'Gagal',
// message: response?.message || 'Gagal menghapus Brand Device',
// });
// }
// } catch (error) {
// console.error('Delete Brand Device Error:', error);
// NotifAlert({
// icon: 'error',
// title: 'Error',
// message: error.message || 'Gagal menghapus Brand Device',
// });
// }
// },
// onCancel: () => {},
// });
// };
// Fungsi untuk membuka file viewer di halaman baru
const handleFileView = (fileName, fileType) => {
console.log('handleFileView called with:', { fileName, fileType });
// Save current phase before navigating to file viewer
localStorage.setItem(`brand_device_${id}_last_phase`, currentStep.toString());
// Extract only the filename without folder prefix
let actualFileName = fileName;
if (fileName && fileName.includes('/')) {
const parts = fileName.split('/');
actualFileName = parts[parts.length - 1]; // Get the last part (actual filename)
}
console.log('Processed filename:', { original: fileName, actual: actualFileName });
const encodedFileName = encodeURIComponent(actualFileName);
const fileTypeParam = fileType === 'image' ? 'image' : 'pdf';
const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`;
console.log('Navigating to:', navigationPath);
navigate(navigationPath);
};
// if (loading) {
// return (
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
// <Spin size="large" />
// </div>
// );
// }
if (!brandData && !loading) {
return <div>Brand Device not found</div>;
}
// Error code table columns configuration
const errorCodeColumns = [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Error Code',
dataIndex: 'error_code',
key: 'error_code',
width: '15%',
render: (text) => text || '-',
},
{
title: 'Error Code Name',
dataIndex: 'error_code_name',
key: 'error_code_name',
width: '20%',
render: (text) => text || '-',
},
{
title: 'Description',
dataIndex: 'error_code_description',
key: 'error_code_description',
width: '25%',
render: (text) => text || '-',
},
{
title: 'Solutions',
dataIndex: 'solution',
key: 'solution',
width: '20%',
render: (solutions) => (
<div>
{solutions && solutions.length > 0 ? (
<div>
<Text type="secondary">{solutions.length} solution(s)</Text>
<div style={{ marginTop: 4 }}>
{solutions.slice(0, 2).map((sol, index) => (
<div key={index} style={{ fontSize: '12px', color: '#666' }}>
{sol.type_solution === 'text' ? (
<span> {sol.solution_name}</span>
) : (
<span> {sol.solution_name} ({sol.type_solution})</span>
)}
</div>
))}
{solutions.length > 2 && (
<div style={{ fontSize: '12px', color: '#999' }}>
...and {solutions.length - 2} more
</div>
)}
</div>
</div>
) : (
<Text type="secondary">No solutions</Text>
)}
</div>
)
},
{
title: 'Status',
dataIndex: 'is_active',
key: 'is_active',
width: '10%',
align: 'center',
render: (_, { is_active }) => (
<Tag color={is_active ? 'green' : 'red'}>
{is_active ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Action',
key: 'action',
align: 'center',
width: '5%',
render: (_, record) => (
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => {
// Show detailed view for this error code
Modal.info({
title: 'Error Code Details',
width: 800,
content: (
<div style={{ marginTop: 16 }}>
<Descriptions bordered column={1} size="small">
<Descriptions.Item label="Error Code">{record.error_code}</Descriptions.Item>
<Descriptions.Item label="Error Code Name">{record.error_code_name}</Descriptions.Item>
<Descriptions.Item label="Description">{record.error_code_description}</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={record.is_active ? 'green' : 'red'}>
{record.is_active ? 'Active' : 'Inactive'}
</Tag>
</Descriptions.Item>
</Descriptions>
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}>
Solutions ({record.solution?.length || 0})
</Title>
{record.solution && record.solution.length > 0 ? (
<Row gutter={[16, 16]}>
{record.solution.map((solution) => (
<Col span={24} key={solution.brand_code_solution_id}>
<Card size="small">
<Row justify="space-between" align="middle">
<Col>
<Space>
{solution.type_solution === 'pdf' ? (
<FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />
) : (
<FileTextOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
)}
<Text strong>{solution.solution_name}</Text>
</Space>
</Col>
<Col>
<Tag color={solution.type_solution === 'pdf' ? 'red' : 'blue'}>
{solution.type_solution.toUpperCase()}
</Tag>
</Col>
</Row>
<div style={{ marginTop: 12 }}>
{solution.type_solution === 'text' ? (
<Text>{solution.text_solution}</Text>
) : (
<Space>
<Text type="secondary">File: {solution.path_document || solution.path_solution}</Text>
{solution.path_document && (
<Button
type="link"
size="small"
onClick={() => {
handleFileView(
(solution.path_document || solution.path_solution || solution.file_upload_name || solution.solution_name || 'Document')?.toString(),
solution.type_solution || 'pdf'
);
}}
>
View Document
</Button>
)}
</Space>
)}
</div>
</Card>
</Col>
))}
</Row>
) : (
<Text type="secondary">No solutions available</Text>
)}
</div>
),
});
}}
style={{ color: '#1890ff' }}
/>
),
},
];
// Mock data function for error codes
const getErrorCodesData = async () => {
const errorCodes = brandData?.error_code || [];
return {
data: errorCodes,
paging: {
current_page: 1,
current_limit: 10,
total_limit: errorCodes.length,
total_page: 1,
}
};
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
<Text strong>Status</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 24 }}>
<div style={{ width: 44, height: 24, backgroundColor: (brandData || {}).is_active ? '#23A55A' : '#bfbfbf', borderRadius: 12, marginRight: 8, position: 'relative' }}>
<div style={{
width: 20,
height: 20,
backgroundColor: 'white',
borderRadius: '50%',
position: 'absolute',
top: 2,
left: (brandData || {}).is_active ? 22 : 2,
transition: 'left 0.3s ease',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
}}></div>
</div>
<Text>{(brandData || {}).is_active ? 'Running' : 'Offline'}</Text>
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>Brand Code</Text>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: '#f5f5f5',
color: '#000000'
}}>
{brandData?.brand_code || (loading ? 'Loading...' : '-')}
</div>
</div>
<Row gutter={16}>
<Col span={12}>
<div style={{ marginBottom: 16 }}>
<Text strong>Brand Name</Text>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: 'white',
minHeight: '32px'
}}>
{brandData?.brand_name || (loading ? 'Loading...' : '-')}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: 16 }}>
<Text strong>Manufacture</Text>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: 'white',
minHeight: '32px'
}}>
{brandData?.brand_manufacture || (loading ? 'Loading...' : '-')}
</div>
</div>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<div style={{ marginBottom: 16 }}>
<Text strong>Brand Type</Text>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: 'white',
minHeight: '32px'
}}>
{brandData?.brand_type || (loading ? 'Loading...' : '-')}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: 16 }}>
<Text strong>Model</Text>
</div>
<div style={{ marginBottom: 24 }}>
<div style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
backgroundColor: 'white',
minHeight: '32px'
}}>
{brandData?.brand_model || (loading ? 'Loading...' : '-')}
</div>
</div>
</Col>
</Row>
</div>
);
}
if (currentStep === 1) {
const errorCodesCount = loading ? 3 : (brandData?.error_code?.length || 0);
return (
<div>
<Title level={5} style={{ marginBottom: 16 }}>
Error Codes ({errorCodesCount})
</Title>
{errorCodesCount > 0 ? (
<TableList
mobile={false}
cardColor={'#42AAFF'}
header={'error_code'}
getData={getErrorCodesData}
queryParams={{}}
columns={errorCodeColumns}
triger={errorCodesTriger}
firstLoad={false}
/>
) : (
!loading && <Text type="secondary">No error codes available</Text>
)}
</div>
);
}
return null;
};
return (
<React.Fragment>
<Card>
<Row justify="space-between" align="middle">
<Col>
<Title level={4} style={{ margin: 0 }}>View Brand Device</Title>
</Col>
<Col>
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/master/brand-device')}
>
Kembali
</Button>
</Space>
</Col>
</Row>
<Divider />
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
{/* Content area with blur overlay during loading */}
<div style={{ position: 'relative', marginTop: 24 }}>
{/* Overlay with blur effect during loading - only on content area */}
{loading && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(0.8px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 5,
borderRadius: '8px'
}}>
<Spin size="large" />
</div>
)}
<div style={{ filter: loading ? 'blur(0.5px)' : 'none', transition: 'filter 0.3s ease' }}>
{renderStepContent()}
</div>
</div>
<Divider />
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
{currentStep > 0 && (
<Button
onClick={() => setCurrentStep(currentStep - 1)}
style={{ marginRight: 8 }}
>
Kembali
</Button>
)}
{currentStep < 1 && (
<Button
type="primary"
onClick={() => setCurrentStep(currentStep + 1)}
style={{ backgroundColor: '#23a55a', borderColor: '#23a55a' }}
>
Lanjut
</Button>
)}
</div>
</Card>
</React.Fragment>
);
};
export default ViewBrandDevice;

View File

@@ -0,0 +1,490 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Button, Typography, Spin, Alert, Space } from 'antd';
import { NotifAlert } from '../../../components/Global/ToastNotif';
import { ArrowLeftOutlined, FilePdfOutlined, FileImageOutlined, DownloadOutlined } from '@ant-design/icons';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById } from '../../../api/master-brand';
import {
downloadFile,
getFile,
getFileUrl,
getFolderFromFileType,
} from '../../../api/file-uploads';
const { Title } = Typography;
const ViewFilePage = () => {
const params = useParams();
const { id, fileType, fileName } = params;
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [brandData, setBrandData] = useState(null);
const [actualFileName, setActualFileName] = useState('');
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;
let fallbackFileName = fileName;
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');
const actionIndex = viewIndex !== -1 ? viewIndex : editIndex;
if (actionIndex !== -1 && urlParts.length > actionIndex + 4) {
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'
});
}
}
useEffect(() => {
setPdfBlobUrl(null);
setPdfLoading(false);
setError(null);
const fetchData = async () => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
return;
}
try {
const actualId = fallbackId || id;
const actualFileName = fallbackFileName || fileName;
const brandResponse = await getBrandById(actualId);
if (brandResponse && brandResponse.statusCode === 200) {
setBrandData(brandResponse.data);
}
const decodedFileName = decodeURIComponent(actualFileName);
setActualFileName(decodedFileName);
const fileExtension = decodedFileName.split('.').pop().toLowerCase();
if (fileExtension === 'pdf') {
setPdfLoading(true);
const folder = getFolderFromFileType('pdf');
try {
const response = await getFile(folder, decodedFileName);
const blobUrl = window.URL.createObjectURL(response.data);
setPdfBlobUrl(blobUrl);
console.log('PDF blob URL created successfully:', blobUrl);
} catch (pdfError) {
console.error('Error loading PDF:', pdfError);
setError('Failed to load PDF file');
} finally {
setPdfLoading(false);
}
}
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setError('Failed to load data');
setLoading(false);
}
};
fetchData();
return () => {
if (pdfBlobUrl) {
window.URL.revokeObjectURL(pdfBlobUrl);
}
};
}, [id, fileName, fileType, navigate]);
useEffect(() => {
if (brandData) {
const breadcrumbItems = [
{ title: <strong style={{ fontSize: '14px' }}> Master</strong> },
{
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate('/master/brand-device')}>Brand Device</strong>
}
];
if (isFromEdit) {
breadcrumbItems.push({
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate(`/master/brand-device/edit/${fallbackId || id}`)}>Edit Brand Device</strong>
});
} else {
breadcrumbItems.push({
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate(`/master/brand-device/view/${fallbackId || id}`)}>View Brand Device</strong>
});
}
breadcrumbItems.push({ title: <strong style={{ fontSize: '14px' }}>View Document</strong> });
setBreadcrumbItems(breadcrumbItems);
}
}, [brandData, id, isFromEdit, fallbackId, navigate, setBreadcrumbItems]);
const handleBack = () => {
if (isFromEdit) {
const savedPhase = localStorage.getItem(`brand_device_edit_${fallbackId || id}_last_phase`);
if (savedPhase) {
localStorage.removeItem(`brand_device_edit_${fallbackId || id}_last_phase`);
}
const targetPhase = savedPhase ? parseInt(savedPhase) : 1;
console.log('ViewFilePage handleBack - Edit mode:', {
savedPhase,
targetPhase,
id: fallbackId || id
});
navigate(`/master/brand-device/edit/${fallbackId || id}`, {
state: { phase: targetPhase, fromFileViewer: true },
replace: true
});
} else {
navigate(`/master/brand-device/view/${fallbackId || id}`, {
state: { phase: 1 },
replace: true
});
}
};
const renderContent = () => {
if (error) {
return (
<Alert
message="Error Loading File"
description={error}
type="error"
showIcon
style={{ margin: '20px 0' }}
/>
);
}
const displayFileName = actualFileName || 'Loading...';
const fileExtension = displayFileName.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
// Show placeholder when loading
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
{isImage ? (
<div style={{
width: '100%',
height: '300px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#999'
}}>
<div>
<FileImageOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<div>Loading image...</div>
</div>
</div>
) : isPdf ? (
<div style={{
width: '100%',
height: '400px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#999'
}}>
<div>
<FilePdfOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<div>Loading PDF...</div>
</div>
</div>
) : (
<div style={{
width: '100%',
height: '200px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#999'
}}>
<div>
<FilePdfOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<div>Loading file...</div>
</div>
</div>
)}
</div>
);
}
if (isImage) {
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<img
src={fileUrl}
alt={actualFileName}
style={{
maxWidth: '100%',
maxHeight: '70vh',
objectFit: 'contain',
border: '1px solid #d9d9d9',
borderRadius: '8px'
}}
onError={() => setError('Failed to load image')}
/>
</div>
);
}
if (isPdf) {
const displayUrl = pdfBlobUrl || fileUrl;
return (
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
{pdfBlobUrl ? (
<iframe
src={pdfBlobUrl}
title={actualFileName}
style={{
width: '100%',
height: '100%',
border: 'none',
borderRadius: '8px'
}}
onError={() => {
setError('Failed to load PDF. Please try downloading the file.');
}}
/>
) : pdfLoading ? (
<div style={{
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: '16px',
backgroundColor: '#f5f5f5'
}}>
<Spin size="large" />
<div style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>Memuat PDF...</div>
<div>Silakan tunggu sebentar</div>
</div>
</div>
) : (
<div style={{
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: '16px',
backgroundColor: '#f5f5f5'
}}>
<FilePdfOutlined style={{ fontSize: '48px', color: '#ff4d4f' }} />
<div style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>PDF tidak dapat dimuat</div>
<div>Silakan download file untuk melihat kontennya</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<Button
type="primary"
onClick={() => {
const folder = getFolderFromFileType(fallbackFileType || fileType);
downloadFile(folder, actualFileName);
}}
icon={<DownloadOutlined />}
>
Download PDF
</Button>
<Button
onClick={() => {
// Retry loading PDF
setPdfLoading(true);
const folder = getFolderFromFileType('pdf');
getFile(folder, actualFileName)
.then(response => {
const blobUrl = window.URL.createObjectURL(response.data);
setPdfBlobUrl(blobUrl);
})
.catch(error => {
console.error('Error retrying PDF load:', error);
setError('Failed to load PDF file');
})
.finally(() => {
setPdfLoading(false);
});
}}
>
Coba Lagi
</Button>
</div>
</div>
)}
</div>
);
}
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<FilePdfOutlined style={{ fontSize: '48px', color: '#ff4d4f', marginBottom: '16px' }} />
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
<div style={{ marginTop: '16px' }}>
<Button type="primary" href={fileUrl} target="_blank" rel="noopener noreferrer">
Buka di Tab Baru
</Button>
</div>
</div>
);
};
const getFileIcon = () => {
const displayFileName = actualFileName || 'Loading...';
const fileExtension = displayFileName.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
if (isImage) return <FileImageOutlined style={{ color: '#1890ff', fontSize: '20px' }} />;
if (isPdf) return <FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '20px' }} />;
return <FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '20px' }} />;
};
const getFileTypeColor = () => {
const displayFileName = actualFileName || 'Loading...';
const fileExtension = displayFileName.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
if (isImage) return '#1890ff';
if (isPdf) return '#ff4d4f';
return '#ff4d4f';
};
return (
<div style={{ padding: '24px', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{getFileIcon()}
<div>
<Title level={4} style={{ margin: 0 }}>
{actualFileName || 'Loading...'}
</Title>
{brandData ? (
<div style={{ color: '#666', fontSize: '14px' }}>
Brand: {brandData.brand_name} | ID: {brandData.brand_id}
</div>
) : (
<div style={{ color: '#666', fontSize: '14px' }}>
Loading brand information...
</div>
)}
</div>
</div>
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={handleBack}
>
Kembali
</Button>
<Button
type="primary"
onClick={() => {
const folder = getFolderFromFileType(fallbackFileType || fileType);
downloadFile(folder, actualFileName);
}}
disabled={loading}
>
Download File
</Button>
</Space>
</div>
{/* File type indicator */}
<div style={{ marginBottom: '16px' }}>
<div style={{
display: 'inline-block',
padding: '4px 12px',
backgroundColor: getFileTypeColor() + '15',
border: `1px solid ${getFileTypeColor()}30`,
borderRadius: '16px',
fontSize: '12px',
fontWeight: 'bold',
color: getFileTypeColor()
}}>
{(fallbackFileType || fileType || 'FILE')?.toUpperCase()}
</div>
</div>
<div style={{ position: 'relative' }}>
{/* Overlay with blur effect during loading */}
{loading && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(0.8px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 5,
borderRadius: '8px'
}}>
<Spin size="large" />
</div>
)}
<div style={{ filter: loading ? 'blur(0.5px)' : 'none', transition: 'filter 0.3s ease' }}>
{renderContent()}
</div>
</div>
</Card>
</div>
);
};
export default ViewFilePage;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { Form, Input, Row, Col, Typography, Switch } from 'antd';
const { Text } = Typography;
const BrandForm = ({ form, formData, onValuesChange, isEdit = false }) => {
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
checked={formData.is_active}
style={{ backgroundColor: formData.is_active ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>
{formData.is_active ? 'Running' : 'Offline'}
</Text>
</div>
</Form.Item>
<Form.Item label="Brand Code" name="brand_code">
<Input
placeholder={isEdit ? 'Brand Code Auto Fill' : 'Brand Code'}
disabled={isEdit}
style={{
backgroundColor: isEdit ? '#f5f5f5' : 'white',
cursor: isEdit ? 'not-allowed' : 'text'
}}
/>
</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>
);
};
export default BrandForm;

View File

@@ -0,0 +1,220 @@
import { Form, Divider, Button, Switch, Input, ConfigProvider, Typography } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { NotifAlert } from '../../../../components/Global/ToastNotif';
import SolutionField from './SolutionField';
const { Text } = Typography;
const ErrorCodeForm = ({
errorCodeForm,
isErrorCodeFormReadOnly,
editingErrorCodeKey,
solutionFields,
solutionTypes,
solutionStatuses,
fileList,
solutionsToDelete,
firstSolutionValid,
onAddErrorCode,
onAddSolutionField,
onRemoveSolutionField,
onSolutionTypeChange,
onSolutionStatusChange,
onSolutionFileUpload,
onFileView,
onCreateNewErrorCode,
onResetForm,
errorCodes
}) => {
const statusValue = Form.useWatch('status', errorCodeForm);
const handleAddErrorCode = async () => {
try {
const values = await errorCodeForm.validateFields();
const solutions = [];
solutionFields.forEach((fieldId) => {
if (solutionsToDelete && solutionsToDelete.has(fieldId)) {
return;
}
const solutionName = values[`solution_name_${fieldId}`];
const textSolution = values[`text_solution_${fieldId}`];
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
const solutionType = values[`solution_type_${fieldId}`] || solutionTypes[fieldId];
if (solutionType === 'text') {
if (textSolution && textSolution.trim()) {
const solutionData = {
solution_name: solutionName || `Solution ${fieldId}`,
type_solution: 'text',
text_solution: textSolution.trim(),
path_solution: '',
is_active: solutionStatuses[fieldId] !== false
};
if (window.currentSolutionData && window.currentSolutionData[fieldId]) {
solutionData.brand_code_solution_id = window.currentSolutionData[fieldId].brand_code_solution_id;
}
solutions.push(solutionData);
}
} else if (solutionType === 'file') {
filesForSolution.forEach((file) => {
const solutionData = {
solution_name: solutionName || file.solution_name || file.name || `Solution ${fieldId}`,
type_solution: file.type_solution || (file.type.startsWith('image/') ? 'image' : 'pdf'),
text_solution: '',
path_solution: file.uploadPath,
is_active: solutionStatuses[fieldId] !== false
};
if (window.currentSolutionData && window.currentSolutionData[fieldId]) {
solutionData.brand_code_solution_id = window.currentSolutionData[fieldId].brand_code_solution_id;
}
solutions.push(solutionData);
});
}
});
if (solutions.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution (text atau file)!'
});
return;
}
const newErrorCode = {
error_code: values.error_code,
error_code_name: values.error_code_name,
error_code_description: values.error_code_description,
status: values.status === undefined ? true : values.status,
solution: solutions,
key: editingErrorCodeKey || `temp-${Date.now()}`
};
onAddErrorCode(newErrorCode);
} catch (error) {
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!' });
}
};
const handleResetForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
solution_status_0: true,
solution_type_0: 'text'
});
onResetForm();
};
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Form.Item label="Status" style={{ margin: 0 }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="status" valuePropName="checked" noStyle>
<Switch
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>{statusValue ? 'Running' : 'Offline'}</Text>
</div>
</Form.Item>
{!isErrorCodeFormReadOnly && (
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={handleAddErrorCode}
>
{editingErrorCodeKey ? 'Update Error Code' : 'Tambah Error Code'}
</Button>
</ConfigProvider>
)}
</div>
<Form.Item name="error_code" label="Error Code" rules={[{ required: true, message: 'Error Code wajib diisi' }]}>
<Input disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item name="error_code_name" label="Error Code Name" rules={[{ required: true, message: 'Error Code Name wajib diisi' }]}>
<Input disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Form.Item name="error_code_description" label="Error Code Description" rules={[{ required: true, message: 'Error Code Description wajib diisi' }]}>
<Input.TextArea disabled={isErrorCodeFormReadOnly} />
</Form.Item>
<Divider>Solutions</Divider>
{solutionFields.map((fieldId, index) => (
<SolutionField
key={fieldId}
fieldId={fieldId}
index={index}
solutionType={solutionTypes[fieldId]}
solutionStatus={solutionStatuses[fieldId]}
isReadOnly={isErrorCodeFormReadOnly}
fileList={fileList.filter(file => file.solutionId === fieldId)}
onRemove={() => onRemoveSolutionField(fieldId)}
onSolutionTypeChange={(type) => onSolutionTypeChange(fieldId, type)}
onSolutionStatusChange={(status) => onSolutionStatusChange(fieldId, status)}
onFileUpload={onSolutionFileUpload}
currentSolutionData={window.currentSolutionData?.[fieldId] || null}
onFileView={onFileView}
errorCodeForm={errorCodeForm}
/>
))}
{!isErrorCodeFormReadOnly && (
<Form.Item style={{ textAlign: 'center' }}>
<Button
icon={<PlusOutlined />}
onClick={onAddSolutionField}
style={{ width: '100%' }}
>
Add More Solution
</Button>
</Form.Item>
)}
{!isErrorCodeFormReadOnly && editingErrorCodeKey && (
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
<Button onClick={handleResetForm}>
Kembali
</Button>
</Form.Item>
)}
{isErrorCodeFormReadOnly && editingErrorCodeKey && (
<Form.Item style={{ textAlign: 'right', marginTop: 16 }}>
<Button onClick={handleResetForm}>
Kembali
</Button>
</Form.Item>
)}
</>
);
};
export default ErrorCodeForm;

View File

@@ -0,0 +1,121 @@
import { useState } from 'react';
import { Upload, Modal } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
const FileUploadHandler = ({
solutionFields,
fileList,
onFileUpload,
onFileRemove
}) => {
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
const getBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
const handleUploadPreview = async (file) => {
const preview = await getBase64(file);
setPreviewImage(preview);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
setPreviewOpen(true);
};
const handleFileUpload = async (file) => {
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 Upload.LIST_IGNORE;
}
try {
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;
onFileUpload(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.`
});
}
return false;
};
const uploadProps = {
multiple: true,
accept: '.pdf,.jpg,.jpeg,.png,.gif',
onRemove: onFileRemove,
beforeUpload: handleFileUpload,
fileList,
onPreview: handleUploadPreview,
};
return (
<>
<Upload.Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">Support for PDF and image files only</p>
</Upload.Dragger>
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={() => setPreviewOpen(false)}
width="80%"
style={{ top: 20 }}
>
{previewImage && (
<img
alt={previewTitle}
style={{ width: '100%' }}
src={previewImage}
/>
)}
</Modal>
</>
);
};
export default FileUploadHandler;

View File

@@ -0,0 +1,70 @@
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;

View File

@@ -1,5 +1,5 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag } from 'antd';
import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag, Spin } from 'antd';
import {
PlusOutlined,
EditOutlined,
@@ -7,46 +7,10 @@ import {
SearchOutlined,
EyeOutlined,
} from '@ant-design/icons';
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { NotifAlert, NotifConfirmDialog, NotifOk } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import TableList from '../../../../components/Global/TableList';
import { getAllBrands } from '../../../../api/master-brand';
// Dummy data
const initialBrandDeviceData = [
{
brand_id: 1,
brandName: 'Siemens S7-1200',
brandType: 'PLC',
manufacturer: 'Siemens',
model: 'S7-1200',
status: 'Active',
},
{
brand_id: 2,
brandName: 'Allen Bradley CompactLogix',
brandType: 'PLC',
manufacturer: 'Rockwell Automation',
model: 'CompactLogix 5370',
status: 'Active',
},
{
brand_id: 3,
brandName: 'Schneider Modicon M580',
brandType: 'PLC',
manufacturer: 'Schneider Electric',
model: 'M580',
status: 'Active',
},
{
brand_id: 4,
brandName: 'Mitsubishi FX5U',
brandType: 'PLC',
manufacturer: 'Mitsubishi',
model: 'FX5U',
status: 'Inactive',
},
];
import { getAllBrands, deleteBrand } from '../../../../api/master-brand';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
@@ -58,43 +22,45 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
},
{
title: 'Brand Device ',
dataIndex: 'brandName',
key: 'brandName',
dataIndex: 'brand_name',
key: 'brand_name',
width: '20%',
},
{
title: 'Type',
dataIndex: 'brandType',
key: 'brandType',
dataIndex: 'brand_type',
key: 'brand_type',
width: '15%',
render: (text) => text || '-',
},
{
title: 'Manufacturer',
dataIndex: 'manufacturer',
key: 'manufacturer',
dataIndex: 'brand_manufacture',
key: 'brand_manufacture',
width: '20%',
},
{
title: 'Model',
dataIndex: 'model',
key: 'model',
dataIndex: 'brand_model',
key: 'brand_model',
width: '15%',
render: (text) => text || '-',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
dataIndex: 'is_active',
key: 'is_active',
width: '10%',
align: 'center',
render: (_, { status }) => (
render: (_, { is_active }) => (
<>
{status === 'Active' ? (
{is_active === true ? (
<Tag color={'green'} key={'status'}>
Active
Running
</Tag>
) : (
<Tag color={'red'} key={'status'}>
Inactive
Offline
</Tag>
)}
</>
@@ -138,7 +104,6 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
const ListBrandDevice = memo(function ListBrandDevice(props) {
const [trigerFilter, setTrigerFilter] = useState(false);
const [brandDeviceData, setBrandDeviceData] = useState(initialBrandDeviceData);
const defaultFilter = { search: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
@@ -149,14 +114,14 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
if (props.actionMode == 'list') {
if (props.actionMode === 'list') {
setFormDataFilter(defaultFilter);
doFilter();
}
} else {
navigate('/signin');
}
}, [props.actionMode, brandDeviceData]);
}, [props.actionMode, navigate]);
const doFilter = () => {
setTrigerFilter((prev) => !prev);
@@ -174,41 +139,55 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
};
const showPreviewModal = (param) => {
props.setSelectedData(param);
props.setActionMode('preview');
// Direct navigation without loading, page will handle its own loading
navigate(`/master/brand-device/view/${param.brand_id}`);
};
const showEditModal = (param = null) => {
props.setSelectedData(param);
props.setActionMode('edit');
// Direct navigation without loading, page will handle its own loading
if (param) {
navigate(`/master/brand-device/edit/${param.brand_id}`);
} else {
navigate('/master/brand-device/add');
}
};
const showDeleteDialog = (param) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi',
message: 'Apakah anda yakin hapus data "' + param.brandName + '" ?',
message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
onConfirm: () => handleDelete(param.brand_id),
onCancel: () => props.setSelectedData(null),
onCancel: () => {},
});
};
const handleDelete = async (brand_id) => {
// Find brand name before deleting
const brandToDelete = brandDeviceData.find((brand) => brand.brand_id === brand_id);
try {
const response = await deleteBrand(brand_id);
// Simulate delete API call
await new Promise((resolve) => setTimeout(resolve, 300));
// Remove from state
const updatedBrands = brandDeviceData.filter((brand) => brand.brand_id !== brand_id);
setBrandDeviceData(updatedBrands);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Data Brand Device "${brandToDelete?.brandName || ''}" berhasil dihapus.`,
});
if (response && response.statusCode === 200) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Data Brand Device berhasil dihapus.',
});
doFilter(); // Refresh data
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menghapus Data Brand Device',
});
}
} catch (error) {
console.error('Delete Brand Device Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Gagal menghapus Data Brand Device',
});
}
};
return (
@@ -267,7 +246,9 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
>
<Button
icon={<PlusOutlined />}
onClick={() => navigate('/master/brand-device/add')}
onClick={() => {
navigate('/master/brand-device/add');
}}
size="large"
>
Tambah Brand Device

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { Table, Button, Space } from 'antd';
import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
const ErrorCodeTable = ({
errorCodes,
loading,
onPreview,
onEdit,
onDelete,
onFileView
}) => {
const errorCodeColumns = [
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
{ title: 'Error Code Name', dataIndex: 'error_code_name', key: 'error_code_name' },
{
title: 'Solutions',
dataIndex: 'solution',
key: 'solution',
render: (solutions) => (
<div>
{solutions && solutions.length > 0 ? (
solutions.map((sol, index) => (
<div key={index} style={{ marginBottom: 4 }}>
<span style={{ fontSize: '12px' }}>
{sol.solution_name}
</span>
</div>
))
) : (
<span style={{ color: '#999', fontSize: '12px' }}>No solutions</span>
)}
</div>
)
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => onPreview(record)}
style={{ color: '#1890ff', borderColor: '#1890ff' }}
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEdit(record)}
style={{ color: '#faad14', borderColor: '#faad14' }}
/>
<Button
danger
type="text"
icon={<DeleteOutlined />}
onClick={() => onDelete(record.key)}
style={{ borderColor: '#ff4d4f' }}
/>
</Space>
),
},
];
const dataSource = loading
? Array.from({ length: 3 }, (_, index) => ({
key: `loading-${index}`,
error_code: 'Loading...',
error_code_name: 'Loading...',
solution: []
}))
: errorCodes;
return (
<Table
columns={errorCodeColumns}
dataSource={dataSource}
rowKey="key"
pagination={false}
/>
);
};
export default ErrorCodeTable;

View File

@@ -1,33 +0,0 @@
import React, { memo } from 'react';
import { Row, Col } from 'antd';
const ListErrorMaster = memo(function ListErrorMaster(props) {
return (
<React.Fragment>
<Row>
<Col xs={24}>
<div
style={{
textAlign: 'center',
padding: '100px 20px',
backgroundColor: '#f5f5f5',
borderRadius: '8px',
}}
>
<h2
style={{
fontSize: '24px',
color: '#595959',
marginBottom: '16px',
}}
>
Cooming soon
</h2>
</div>
</Col>
</Row>
</React.Fragment>
);
});
export default ListErrorMaster;

View File

@@ -0,0 +1,237 @@
import React, { useEffect } from 'react';
import { Form, Input, Button, Switch, Radio, Upload, Typography } from 'antd';
import { DeleteOutlined, UploadOutlined } from '@ant-design/icons';
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
const { Text } = Typography;
const SolutionField = ({
fieldId,
index,
solutionStatus,
isReadOnly,
fileList,
onRemove,
onSolutionTypeChange,
onSolutionStatusChange,
onFileUpload,
currentSolutionData,
onFileView,
errorCodeForm
}) => {
useEffect(() => {
if (currentSolutionData && errorCodeForm) {
if (currentSolutionData.solution_name) {
errorCodeForm.setFieldValue(`solution_name_${fieldId}`, currentSolutionData.solution_name);
}
if (currentSolutionData.type_solution === 'text' && currentSolutionData.text_solution) {
errorCodeForm.setFieldValue(`text_solution_${fieldId}`, currentSolutionData.text_solution);
}
if (currentSolutionData.type_solution) {
const formValue = currentSolutionData.type_solution === 'image' || currentSolutionData.type_solution === 'pdf' ? 'file' : currentSolutionData.type_solution;
errorCodeForm.setFieldValue(`solution_type_${fieldId}`, formValue);
}
if (currentSolutionData.is_active !== undefined) {
errorCodeForm.setFieldValue(`solution_status_${fieldId}`, currentSolutionData.is_active);
}
}
}, [currentSolutionData, fieldId, errorCodeForm]);
const handleBeforeUpload = async (file) => {
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 Upload.LIST_IGNORE;
}
try {
// Upload file immediately to get path
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 = fieldId;
file.type_solution = fileType;
onFileUpload(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.`
});
}
return false;
};
return (
<div
data-solution-id={fieldId}
style={{
marginBottom: 24,
padding: 16,
border: '1px solid #d9d9d9',
borderRadius: 8,
transition: 'all 0.3s ease'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Text strong>Solution {index + 1}</Text>
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => onRemove(fieldId)}
disabled={isReadOnly}
style={{ borderColor: '#ff4d4f' }}
/>
</div>
<Form.Item name={`solution_name_${fieldId}`} label="Solution Name">
<Input placeholder="Enter solution name" disabled={isReadOnly} />
</Form.Item>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Switch
checked={solutionStatus}
onChange={(checked) => {
onSolutionStatusChange(fieldId, checked);
}}
disabled={isReadOnly}
style={{ backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf' }}
/>
<Text style={{ marginLeft: 8 }}>
{solutionStatus ? 'Active' : 'Non Active'}
</Text>
</div>
</Form.Item>
<Form.Item label="Solution Type">
<Form.Item name={`solution_type_${fieldId}`} noStyle>
<Radio.Group
onChange={(e) => {
onSolutionTypeChange(fieldId, e.target.value);
}}
disabled={isReadOnly}
>
<Radio value="text">Text Solution</Radio>
<Radio value="file">File Upload</Radio>
</Radio.Group>
</Form.Item>
</Form.Item>
<Form.Item shouldUpdate={(prevValues, currentValues) => prevValues[`solution_type_${fieldId}`] !== currentValues[`solution_type_${fieldId}`]} noStyle>
{({ getFieldValue }) => {
const currentType = getFieldValue(`solution_type_${fieldId}`) || 'text';
const displayType = currentType === 'file' && currentSolutionData ?
(currentSolutionData.type_solution === 'image' ? 'image' :
currentSolutionData.type_solution === 'pdf' ? 'pdf' : 'file') : currentType;
return displayType === 'text' ? (
<Form.Item name={`text_solution_${fieldId}`} label="Text Solution">
<Input.TextArea
placeholder="Enter text solution"
disabled={isReadOnly}
rows={4}
/>
</Form.Item>
) : (
<>
{/* Show existing file info for both preview and edit mode */}
{currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution && (
<Form.Item label="Current Document">
{(() => {
const solution = currentSolutionData;
const fileName = solution.file_upload_name || solution.path_solution?.split('/')[1] || 'File';
const fileType = solution.type_solution;
if (fileType !== 'text' && solution.path_solution) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<Text>
{fileType === 'image' ? '[Image]' : '[Document]'} {fileName}
</Text>
<Button
type="link"
size="small"
onClick={() => onFileView(solution.path_solution, solution.type_solution)}
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
>
View Document
</Button>
</div>
);
}
return null;
})()}
</Form.Item>
)}
<Form.Item label="Upload File">
<Upload
multiple={true}
accept=".pdf,.jpg,.jpeg,.png,.gif"
disabled={isReadOnly}
fileList={[
...fileList.filter(file => file.solutionId === fieldId),
// Add existing file to fileList if it exists
...(currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution ? [{
uid: `existing-${fieldId}`,
name: currentSolutionData.file_upload_name || currentSolutionData.path_solution?.split('/')[1] || 'File',
status: 'done',
url: null, // We'll use the path_solution for viewing
solutionId: fieldId,
type_solution: currentSolutionData.type_solution,
uploadPath: currentSolutionData.path_solution,
existingFile: true
}] : [])
]}
onRemove={(file) => {
}}
beforeUpload={handleBeforeUpload}
>
<Button icon={<UploadOutlined />} disabled={isReadOnly}>
Click to Upload (File or Image)
</Button>
</Upload>
</Form.Item>
</>
);
}}
</Form.Item>
</div>
);
};
export default SolutionField;

View File

@@ -0,0 +1,264 @@
import { useState, useEffect } from 'react';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
export const useErrorCodeLogic = (errorCodeForm, fileList) => {
const [solutionFields, setSolutionFields] = useState([0]);
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
const [firstSolutionValid, setFirstSolutionValid] = useState(false);
const [solutionsToDelete, setSolutionsToDelete] = useState(new Set());
const checkPreviousSolutionValid = (currentSolutionIndex) => {
for (let i = 0; i < currentSolutionIndex; i++) {
const fieldId = solutionFields[i];
const solutionType = solutionTypes[fieldId];
const solutionName = errorCodeForm.getFieldValue(`solution_name_${fieldId}`);
if (!solutionName || solutionName.trim() === '') {
return false;
}
if (solutionType === 'text') {
const textSolution = errorCodeForm.getFieldValue(`text_solution_${fieldId}`);
if (!textSolution || textSolution.trim() === '') {
return false;
}
} else if (solutionType === 'file') {
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
if (filesForSolution.length === 0) {
return false;
}
}
}
return true;
};
const checkFirstSolutionValid = () => {
if (solutionFields.length === 0) {
setFirstSolutionValid(false);
return false;
}
const isValid = checkPreviousSolutionValid(1);
setFirstSolutionValid(isValid);
return isValid;
};
const handleAddSolutionField = () => {
const currentSolutionCount = solutionFields.length;
const nextSolutionNumber = currentSolutionCount + 1;
if (!checkPreviousSolutionValid(currentSolutionCount)) {
let incompleteSolutionIndex = -1;
for (let i = 0; i < currentSolutionCount; i++) {
const fieldId = solutionFields[i];
const solutionType = solutionTypes[fieldId];
const solutionName = errorCodeForm.getFieldValue(`solution_name_${fieldId}`);
let hasContent = false;
if (solutionType === 'text') {
const textSolution = errorCodeForm.getFieldValue(`text_solution_${fieldId}`);
hasContent = textSolution && textSolution.trim();
} else if (solutionType === 'file') {
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
hasContent = filesForSolution.length > 0;
}
if (!solutionName?.trim() || !hasContent) {
incompleteSolutionIndex = i + 1;
break;
}
}
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: `Harap lengkapi Solution ${incompleteSolutionIndex} terlebih dahulu sebelum menambah Solution ${nextSolutionNumber}!`
});
return;
}
const newId = `new-${Date.now()}`;
setSolutionFields(prev => [...prev, newId]);
setSolutionTypes(prev => ({ ...prev, [newId]: 'text' }));
setSolutionStatuses(prev => ({ ...prev, [newId]: true }));
errorCodeForm.setFieldValue(`solution_status_${newId}`, true);
errorCodeForm.setFieldValue(`solution_type_${newId}`, 'text');
};
const handleRemoveSolutionField = (id) => {
const isNewSolution = !id.toString().startsWith('existing-');
if (isNewSolution) {
if (solutionFields.length > 1) {
setSolutionFields(solutionFields.filter(fieldId => fieldId !== id));
setSolutionTypes(prev => {
const newTypes = { ...prev };
delete newTypes[id];
return newTypes;
});
setSolutionStatuses(prev => {
const newStatuses = { ...prev };
delete newStatuses[id];
return newStatuses;
});
setSolutionsToDelete(prev => {
const newSet = new Set(prev);
newSet.delete(id);
return newSet;
});
} else {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!'
});
}
} else {
const solutionName = errorCodeForm.getFieldValue(`solution_name_${id}`);
const solutionType = solutionTypes[id];
let isEmpty = true;
const existingSolution = window.currentSolutionData?.[id];
const hasExistingData = existingSolution && (
(existingSolution.solution_name && existingSolution.solution_name.trim()) ||
(existingSolution.text_solution && existingSolution.text_solution.trim()) ||
(existingSolution.path_solution && existingSolution.path_solution.trim())
);
if (solutionType === 'text') {
const textSolution = errorCodeForm.getFieldValue(`text_solution_${id}`);
isEmpty = !solutionName?.trim() && !textSolution?.trim() && !hasExistingData;
} else if (solutionType === 'file') {
const filesForSolution = fileList.filter(file => file.solutionId === id);
isEmpty = !solutionName?.trim() && filesForSolution.length === 0 && !hasExistingData;
}
if (isEmpty) {
if (solutionFields.length > 1) {
setSolutionFields(solutionFields.filter(fieldId => fieldId !== id));
setSolutionTypes(prev => {
const newTypes = { ...prev };
delete newTypes[id];
return newTypes;
});
setSolutionStatuses(prev => {
const newStatuses = { ...prev };
delete newStatuses[id];
return newStatuses;
});
if (window.currentSolutionData) {
delete window.currentSolutionData[id];
}
setSolutionsToDelete(prev => {
const newSet = new Set(prev);
newSet.delete(id);
return newSet;
});
} else {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!'
});
}
} else {
if (solutionFields.length > 1) {
setSolutionsToDelete(prev => new Set(prev).add(id));
const solutionElement = document.querySelector(`[data-solution-id="${id}"]`);
if (solutionElement) {
solutionElement.style.opacity = '0.5';
solutionElement.style.border = '2px dashed #ff4d4f';
}
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Solution ditandai untuk dihapus. Klik "Update Error Code" untuk menyimpan perubahan.'
});
} else {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!'
});
}
}
}
};
const handleSolutionTypeChange = (fieldId, type) => {
setSolutionTypes(prev => ({ ...prev, [fieldId]: type }));
};
const handleSolutionStatusChange = (fieldId, status) => {
setSolutionStatuses(prev => ({
...prev,
[fieldId]: status
}));
};
const setSolutionsForExistingRecord = (solutions, errorCodeForm) => {
const newSolutionFields = [];
const newSolutionTypes = {};
const newSolutionStatuses = {};
const newSolutionData = {};
solutions.forEach((solution, index) => {
const fieldId = `existing-${index}`;
newSolutionFields.push(fieldId);
newSolutionTypes[fieldId] = solution.type_solution || 'text';
newSolutionStatuses[fieldId] = solution.is_active !== false;
newSolutionData[fieldId] = {
...solution,
brand_code_solution_id: solution.brand_code_solution_id
};
setTimeout(() => {
errorCodeForm.setFieldsValue({
[`solution_name_${fieldId}`]: solution.solution_name,
[`text_solution_${fieldId}`]: solution.text_solution || '',
[`solution_status_${fieldId}`]: solution.is_active !== false,
[`solution_type_${fieldId}`]: solution.type_solution === 'image' || solution.type_solution === 'pdf' ? 'file' : solution.type_solution
});
}, 100);
});
setSolutionFields(newSolutionFields);
setSolutionTypes(newSolutionTypes);
setSolutionStatuses(newSolutionStatuses);
window.currentSolutionData = newSolutionData;
};
const resetSolutionFields = () => {
setSolutionFields([0]);
setSolutionTypes({ 0: 'text' });
setSolutionStatuses({ 0: true });
setFirstSolutionValid(false);
setSolutionsToDelete(new Set());
};
useEffect(() => {
const timer = setTimeout(() => {
checkFirstSolutionValid();
}, 100);
return () => clearTimeout(timer);
}, [solutionFields, solutionTypes, fileList, errorCodeForm]);
return {
solutionFields,
solutionTypes,
solutionStatuses,
firstSolutionValid,
solutionsToDelete,
handleAddSolutionField,
handleRemoveSolutionField,
handleSolutionTypeChange,
handleSolutionStatusChange,
resetSolutionFields,
checkFirstSolutionValid,
setSolutionsForExistingRecord
};
};