Merge pull request 'lavoce' (#10) from lavoce into main
Reviewed-on: #10
This commit is contained in:
11
src/App.jsx
11
src/App.jsx
@@ -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
126
src/api/file-uploads.jsx
Normal 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
|
||||
};
|
||||
@@ -19,6 +19,7 @@ const TableList = memo(function TableList({
|
||||
cardColor,
|
||||
fieldColor,
|
||||
firstLoad = true,
|
||||
columnDynamic = false,
|
||||
}) {
|
||||
const [gridLoading, setGridLoading] = useState(false);
|
||||
|
||||
@@ -31,6 +32,8 @@ const TableList = memo(function TableList({
|
||||
total_page: 1,
|
||||
});
|
||||
|
||||
const [columnsDynamic, setColumnsDynamic] = useState(columns);
|
||||
|
||||
const [viewMode, setViewMode] = useState('table');
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
@@ -57,6 +60,49 @@ const TableList = memo(function TableList({
|
||||
const param = new URLSearchParams({ ...paging, ...queryParams });
|
||||
const resData = await getData(param);
|
||||
|
||||
if (columnDynamic && resData) {
|
||||
const columnsApi = resData[columnDynamic] ?? '';
|
||||
|
||||
// Pisahkan string menjadi array kolom
|
||||
const colArray = columnsApi.split(',').map((c) => c.trim());
|
||||
|
||||
// Kolom default datetime di awal
|
||||
const defaultColumns = [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Datetime',
|
||||
dataIndex: 'datetime',
|
||||
key: 'datetime',
|
||||
width: '15%',
|
||||
// render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
// Buat kolom numerik dengan format 4 angka di belakang koma
|
||||
const numericColumns = colArray.map((colName) => ({
|
||||
title: colName,
|
||||
dataIndex: colName,
|
||||
key: colName,
|
||||
align: 'right',
|
||||
width: 'auto',
|
||||
render: (value) => {
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(4);
|
||||
}
|
||||
return value ?? '-';
|
||||
},
|
||||
}));
|
||||
|
||||
// Gabungkan default + API columns
|
||||
setColumnsDynamic([...defaultColumns, ...numericColumns]);
|
||||
}
|
||||
|
||||
setData(resData?.data ?? []);
|
||||
|
||||
const pagingData = resData?.paging;
|
||||
@@ -71,6 +117,8 @@ const TableList = memo(function TableList({
|
||||
}));
|
||||
}
|
||||
|
||||
setGridLoading(false);
|
||||
|
||||
if (resData) {
|
||||
setTimeout(() => {
|
||||
setGridLoading(false);
|
||||
@@ -109,7 +157,7 @@ const TableList = memo(function TableList({
|
||||
cardColor={cardColor}
|
||||
fieldColor={fieldColor}
|
||||
data={data}
|
||||
column={columns}
|
||||
column={columnsDynamic}
|
||||
header={header}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
@@ -119,7 +167,7 @@ const TableList = memo(function TableList({
|
||||
<Row gutter={24} style={{ marginTop: '16px' }}>
|
||||
<Table
|
||||
rowSelection={rowSelection || null}
|
||||
columns={columns}
|
||||
columns={columnsDynamic}
|
||||
dataSource={data.map((item, index) => ({ ...item, key: index }))}
|
||||
pagination={false}
|
||||
loading={gridLoading}
|
||||
|
||||
@@ -43,7 +43,7 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
|
||||
case 1:
|
||||
return (
|
||||
<span>
|
||||
{record.lim_low + 1} : {record.lim_high - 1}
|
||||
{record.lim_low} : {record.lim_high}
|
||||
</span>
|
||||
);
|
||||
case 2:
|
||||
@@ -51,13 +51,13 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
|
||||
case 3:
|
||||
return (
|
||||
<span>
|
||||
{record.lim_low_crash + 1} : {record.lim_low - 1}
|
||||
{record.lim_low_crash} : {record.lim_low}
|
||||
</span>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<span>
|
||||
{record.lim_high + 1} : {record.lim_high_crash - 1}
|
||||
{record.lim_high} : {record.lim_high_crash}
|
||||
</span>
|
||||
);
|
||||
case 5:
|
||||
|
||||
@@ -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);
|
||||
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
|
||||
}
|
||||
await mockErrorCodeApi.createErrorCode({
|
||||
...errorCode,
|
||||
brand_device_id: newBrandDeviceId
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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.',
|
||||
});
|
||||
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');
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Gagal menambahkan 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
|
||||
};
|
||||
|
||||
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
|
||||
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 (newErrorCode) => {
|
||||
if (editingErrorCodeKey) {
|
||||
setErrorCodes(errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item));
|
||||
message.success('Error code berhasil diupdate');
|
||||
const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
|
||||
setErrorCodes(updatedCodes);
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Error code berhasil diupdate!'
|
||||
});
|
||||
} else {
|
||||
setErrorCodes([...errorCodes, newErrorCode]);
|
||||
message.success('Error code berhasil ditambahkan');
|
||||
const updatedCodes = [...errorCodes, newErrorCode];
|
||||
setErrorCodes(updatedCodes);
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Error code berhasil ditambahkan!'
|
||||
});
|
||||
}
|
||||
|
||||
resetErrorCodeForm();
|
||||
};
|
||||
|
||||
const resetErrorCodeForm = () => {
|
||||
errorCodeForm.resetFields();
|
||||
errorCodeForm.setFieldsValue({
|
||||
status: true,
|
||||
solution_status_0: true,
|
||||
solution_type_0: 'text'
|
||||
});
|
||||
setFileList([]);
|
||||
} catch (error) {
|
||||
console.log('Validate Failed:', error);
|
||||
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' });
|
||||
}
|
||||
setIsErrorCodeFormReadOnly(false); // Reset status read-only
|
||||
setEditingErrorCodeKey(null); // Reset key item yang diedit
|
||||
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);
|
||||
if (errorCodes.length <= 1) {
|
||||
NotifAlert({
|
||||
icon: 'warning',
|
||||
title: 'Perhatian',
|
||||
message: 'Setiap brand harus memiliki minimal 1 error code!'
|
||||
});
|
||||
|
||||
// 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');
|
||||
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;
|
||||
}
|
||||
|
||||
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]);
|
||||
return false; // Prevent auto-upload
|
||||
},
|
||||
fileList,
|
||||
onPreview: handleUploadPreview, // Tambahkan handler onPreview
|
||||
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' }}
|
||||
<BrandForm
|
||||
form={brandForm}
|
||||
formData={formData}
|
||||
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
|
||||
isEdit={false}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
<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.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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
445
src/pages/master/brandDevice/EditBrandDevice.jsx
Normal file
445
src/pages/master/brandDevice/EditBrandDevice.jsx
Normal 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;
|
||||
551
src/pages/master/brandDevice/ViewBrandDevice.jsx
Normal file
551
src/pages/master/brandDevice/ViewBrandDevice.jsx
Normal 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;
|
||||
490
src/pages/master/brandDevice/ViewFilePage.jsx
Normal file
490
src/pages/master/brandDevice/ViewFilePage.jsx
Normal 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;
|
||||
76
src/pages/master/brandDevice/component/BrandForm.jsx
Normal file
76
src/pages/master/brandDevice/component/BrandForm.jsx
Normal 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;
|
||||
220
src/pages/master/brandDevice/component/ErrorCodeForm.jsx
Normal file
220
src/pages/master/brandDevice/component/ErrorCodeForm.jsx
Normal 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;
|
||||
121
src/pages/master/brandDevice/component/FileUploadHandler.jsx
Normal file
121
src/pages/master/brandDevice/component/FileUploadHandler.jsx
Normal 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;
|
||||
70
src/pages/master/brandDevice/component/FormActions.jsx
Normal file
70
src/pages/master/brandDevice/component/FormActions.jsx
Normal 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;
|
||||
@@ -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({
|
||||
if (response && response.statusCode === 200) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Brand Device "${brandToDelete?.brandName || ''}" berhasil dihapus.`,
|
||||
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
|
||||
|
||||
84
src/pages/master/brandDevice/component/ListErrorCode.jsx
Normal file
84
src/pages/master/brandDevice/component/ListErrorCode.jsx
Normal 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;
|
||||
@@ -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;
|
||||
237
src/pages/master/brandDevice/component/SolutionField.jsx
Normal file
237
src/pages/master/brandDevice/component/SolutionField.jsx
Normal 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;
|
||||
264
src/pages/master/brandDevice/hooks/errorCode.js
Normal file
264
src/pages/master/brandDevice/hooks/errorCode.js
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,10 @@ import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'a
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import dayjs from 'dayjs';
|
||||
import { FileTextOutlined } from '@ant-design/icons';
|
||||
import { getAllHistoryValueReport } from '../../../../api/history-value';
|
||||
import {
|
||||
getAllHistoryValueReport,
|
||||
getAllHistoryValueReportPivot,
|
||||
} from '../../../../api/history-value';
|
||||
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -29,19 +32,19 @@ const ListReport = memo(function ListReport(props) {
|
||||
key: 'tag_name',
|
||||
width: '70%',
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'val',
|
||||
key: 'val',
|
||||
width: '10%',
|
||||
render: (_, record) => Number(record.val).toFixed(4),
|
||||
},
|
||||
{
|
||||
title: 'Stat',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '10%',
|
||||
},
|
||||
// {
|
||||
// title: 'Value',
|
||||
// dataIndex: 'val',
|
||||
// key: 'val',
|
||||
// width: '10%',
|
||||
// render: (_, record) => Number(record.val).toFixed(4),
|
||||
// },
|
||||
// {
|
||||
// title: 'Stat',
|
||||
// dataIndex: 'status',
|
||||
// key: 'status',
|
||||
// width: '10%',
|
||||
// },
|
||||
];
|
||||
|
||||
const dateNow = dayjs();
|
||||
@@ -49,21 +52,21 @@ const ListReport = memo(function ListReport(props) {
|
||||
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
|
||||
const [plantSubSection, setPlantSubSection] = useState(0);
|
||||
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
|
||||
const [startDate, setStartDate] = useState(dateNow);
|
||||
const [endDate, setEndDate] = useState(dateNow);
|
||||
const [periode, setPeriode] = useState(10);
|
||||
|
||||
const defaultFilter = {
|
||||
criteria: '',
|
||||
plant_sub_section_id: 0,
|
||||
from: dateNowFormated,
|
||||
to: dateNowFormated,
|
||||
interval: 10,
|
||||
interval: periode,
|
||||
};
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
|
||||
const [plantSubSection, setPlantSubSection] = useState(0);
|
||||
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
|
||||
const [startDate, setStartDate] = useState(dateNow);
|
||||
const [endDate, setEndDate] = useState(dateNow);
|
||||
const [periode, setPeriode] = useState(5);
|
||||
|
||||
const handleSearch = () => {
|
||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||
@@ -199,9 +202,13 @@ const ListReport = memo(function ListReport(props) {
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
firstLoad={false}
|
||||
getData={getAllHistoryValueReport}
|
||||
mobile
|
||||
cardColor={'#d38943ff'}
|
||||
header={'datetime'}
|
||||
getData={getAllHistoryValueReportPivot}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns}
|
||||
columnDynamic={'columns'}
|
||||
triger={trigerFilter}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -62,10 +62,10 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
: [],
|
||||
}));
|
||||
|
||||
// setTrendingValue(cleanedData);
|
||||
setTrendingValue(cleanedData);
|
||||
} else {
|
||||
// 🔹 Jika tidak ada data dari API
|
||||
// setTrendingValue([]);
|
||||
setTrendingValue([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user