lavoce #10
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 IndexTag from './pages/master/tag/IndexTag';
|
||||||
import IndexUnit from './pages/master/unit/IndexUnit';
|
import IndexUnit from './pages/master/unit/IndexUnit';
|
||||||
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
|
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
|
||||||
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
|
|
||||||
import IndexStatus from './pages/master/status/IndexStatus';
|
import IndexStatus from './pages/master/status/IndexStatus';
|
||||||
import IndexShift from './pages/master/shift/IndexShift';
|
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
|
// Jadwal Shift
|
||||||
import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift';
|
import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift';
|
||||||
@@ -74,6 +78,11 @@ const App = () => {
|
|||||||
<Route path="unit" element={<IndexUnit />} />
|
<Route path="unit" element={<IndexUnit />} />
|
||||||
<Route path="brand-device" element={<IndexBrandDevice />} />
|
<Route path="brand-device" element={<IndexBrandDevice />} />
|
||||||
<Route path="brand-device/add" element={<AddBrandDevice />} />
|
<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="plant-sub-section" element={<IndexPlantSubSection />} />
|
||||||
<Route path="shift" element={<IndexShift />} />
|
<Route path="shift" element={<IndexShift />} />
|
||||||
<Route path="status" element={<IndexStatus />} />
|
<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,
|
cardColor,
|
||||||
fieldColor,
|
fieldColor,
|
||||||
firstLoad = true,
|
firstLoad = true,
|
||||||
|
columnDynamic = false,
|
||||||
}) {
|
}) {
|
||||||
const [gridLoading, setGridLoading] = useState(false);
|
const [gridLoading, setGridLoading] = useState(false);
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@ const TableList = memo(function TableList({
|
|||||||
total_page: 1,
|
total_page: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [columnsDynamic, setColumnsDynamic] = useState(columns);
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState('table');
|
const [viewMode, setViewMode] = useState('table');
|
||||||
|
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
@@ -57,6 +60,49 @@ const TableList = memo(function TableList({
|
|||||||
const param = new URLSearchParams({ ...paging, ...queryParams });
|
const param = new URLSearchParams({ ...paging, ...queryParams });
|
||||||
const resData = await getData(param);
|
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 ?? []);
|
setData(resData?.data ?? []);
|
||||||
|
|
||||||
const pagingData = resData?.paging;
|
const pagingData = resData?.paging;
|
||||||
@@ -71,6 +117,8 @@ const TableList = memo(function TableList({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGridLoading(false);
|
||||||
|
|
||||||
if (resData) {
|
if (resData) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setGridLoading(false);
|
setGridLoading(false);
|
||||||
@@ -109,7 +157,7 @@ const TableList = memo(function TableList({
|
|||||||
cardColor={cardColor}
|
cardColor={cardColor}
|
||||||
fieldColor={fieldColor}
|
fieldColor={fieldColor}
|
||||||
data={data}
|
data={data}
|
||||||
column={columns}
|
column={columnsDynamic}
|
||||||
header={header}
|
header={header}
|
||||||
showPreviewModal={showPreviewModal}
|
showPreviewModal={showPreviewModal}
|
||||||
showEditModal={showEditModal}
|
showEditModal={showEditModal}
|
||||||
@@ -119,7 +167,7 @@ const TableList = memo(function TableList({
|
|||||||
<Row gutter={24} style={{ marginTop: '16px' }}>
|
<Row gutter={24} style={{ marginTop: '16px' }}>
|
||||||
<Table
|
<Table
|
||||||
rowSelection={rowSelection || null}
|
rowSelection={rowSelection || null}
|
||||||
columns={columns}
|
columns={columnsDynamic}
|
||||||
dataSource={data.map((item, index) => ({ ...item, key: index }))}
|
dataSource={data.map((item, index) => ({ ...item, key: index }))}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
loading={gridLoading}
|
loading={gridLoading}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
|
|||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{record.lim_low + 1} : {record.lim_high - 1}
|
{record.lim_low} : {record.lim_high}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
@@ -51,13 +51,13 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
|
|||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{record.lim_low_crash + 1} : {record.lim_low - 1}
|
{record.lim_low_crash} : {record.lim_low}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{record.lim_high + 1} : {record.lim_high_crash - 1}
|
{record.lim_high} : {record.lim_high_crash}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 5:
|
case 5:
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { Divider, Typography, Button, Steps, Form, Row, Col, Card } from 'antd';
|
||||||
import { PlusOutlined, UploadOutlined, EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
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;
|
const { Step } = Steps;
|
||||||
|
|
||||||
// Mock API for Error Codes (can be moved to a separate file later)
|
const defaultData = {
|
||||||
const mockErrorCodeApi = {
|
brand_name: '',
|
||||||
errorCodes: [],
|
brand_type: '',
|
||||||
createErrorCode: async (data) => {
|
brand_model: '',
|
||||||
const newId = mockErrorCodeApi.errorCodes.length > 0 ? Math.max(...mockErrorCodeApi.errorCodes.map(ec => ec.error_code_id)) + 1 : 1;
|
brand_manufacture: '',
|
||||||
const newErrorCode = { ...data, error_code_id: newId };
|
is_active: true,
|
||||||
mockErrorCodeApi.errorCodes.push(newErrorCode);
|
brand_code: '',
|
||||||
return { statusCode: 201, data: newErrorCode };
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddBrandDevice = () => {
|
const AddBrandDevice = () => {
|
||||||
@@ -27,29 +31,37 @@ const AddBrandDevice = () => {
|
|||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [fileList, setFileList] = useState([]);
|
const [fileList, setFileList] = useState([]);
|
||||||
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); // State untuk mengontrol form read-only
|
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
|
||||||
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); // State untuk melacak item yang sedang diedit
|
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
|
||||||
// State untuk preview file
|
const [loading, setLoading] = useState(false);
|
||||||
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 [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
const [errorCodes, setErrorCodes] = useState([]);
|
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 = () => {
|
const handleCancel = () => {
|
||||||
navigate('/master/brand-device');
|
navigate('/master/brand-device');
|
||||||
};
|
};
|
||||||
@@ -59,290 +71,294 @@ const AddBrandDevice = () => {
|
|||||||
await brandForm.validateFields();
|
await brandForm.validateFields();
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Validate Failed:', error);
|
|
||||||
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' });
|
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async () => {
|
const handleFinish = async () => {
|
||||||
if (errorCodes.length === 0) {
|
|
||||||
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Silakan tambahkan minimal satu error code.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
try {
|
try {
|
||||||
const finalFormData = { ...formData, status: formData.status ? 'Active' : 'Inactive' };
|
const transformedErrorCodes = errorCodes.map(ec => ({
|
||||||
console.log("Saving brand device:", finalFormData);
|
error_code: ec.error_code,
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
error_code_name: ec.error_code_name || '',
|
||||||
const newBrandDeviceId = Date.now();
|
error_code_description: ec.error_code_description || '',
|
||||||
console.log("Brand device saved with ID:", newBrandDeviceId);
|
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);
|
const finalFormData = {
|
||||||
for (const errorCode of errorCodes) {
|
brand_name: formData.brand_name,
|
||||||
if (errorCode.another_solution === 'image' && errorCode.image) {
|
brand_type: formData.brand_type || '',
|
||||||
console.log(`Uploading image for error code ${errorCode.error_code}:`, errorCode.image.name);
|
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');
|
navigate('/master/brand-device');
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.message || 'Gagal menambahkan Brand Device',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setConfirmLoading(false);
|
|
||||||
console.error("Failed to save data:", error);
|
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: "error",
|
icon: "error",
|
||||||
title: "Gagal",
|
title: "Gagal",
|
||||||
message: "Gagal menyimpan data. Silakan coba lagi.",
|
message: error.message || "Gagal menyimpan data. Silakan coba lagi.",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreviewErrorCode = (record) => {
|
const handlePreviewErrorCode = (record) => {
|
||||||
errorCodeForm.setFieldsValue(record); // Isi form dengan data record
|
errorCodeForm.setFieldsValue({
|
||||||
setFileList(record.fileList || []); // Muat file jika ada
|
error_code: record.error_code,
|
||||||
setIsErrorCodeFormReadOnly(true); // Jadikan form read-only
|
error_code_name: record.error_code_name,
|
||||||
setEditingErrorCodeKey(null); // Bukan dalam mode edit
|
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) => {
|
const handleEditErrorCode = (record) => {
|
||||||
errorCodeForm.setFieldsValue(record); // Isi form dengan data record
|
errorCodeForm.setFieldsValue({
|
||||||
setFileList(record.fileList || []); // Muat file jika ada
|
error_code: record.error_code,
|
||||||
setIsErrorCodeFormReadOnly(false); // Aktifkan form untuk diedit
|
error_code_name: record.error_code_name,
|
||||||
setEditingErrorCodeKey(record.key); // Tandai item ini sebagai yang sedang diedit
|
error_code_description: record.error_code_description,
|
||||||
};
|
status: record.status,
|
||||||
|
});
|
||||||
const handleAddErrorCode = async () => {
|
setFileList(record.fileList || []);
|
||||||
try {
|
setIsErrorCodeFormReadOnly(false);
|
||||||
const values = await errorCodeForm.validateFields();
|
setEditingErrorCodeKey(record.key);
|
||||||
const newErrorCode = {
|
|
||||||
...values,
|
if (record.solution && record.solution.length > 0) {
|
||||||
status: values.status === undefined ? true : values.status,
|
setSolutionsForExistingRecord(record.solution, errorCodeForm);
|
||||||
fileList: fileList,
|
}
|
||||||
key: editingErrorCodeKey || `temp-${Date.now()}` // Gunakan key yang ada jika edit, jika tidak buat baru
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddErrorCode = async (newErrorCode) => {
|
||||||
if (editingErrorCodeKey) {
|
if (editingErrorCodeKey) {
|
||||||
setErrorCodes(errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item));
|
const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item);
|
||||||
message.success('Error code berhasil diupdate');
|
setErrorCodes(updatedCodes);
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'Error code berhasil diupdate!'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setErrorCodes([...errorCodes, newErrorCode]);
|
const updatedCodes = [...errorCodes, newErrorCode];
|
||||||
message.success('Error code berhasil ditambahkan');
|
setErrorCodes(updatedCodes);
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'Error code berhasil ditambahkan!'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetErrorCodeForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetErrorCodeForm = () => {
|
||||||
errorCodeForm.resetFields();
|
errorCodeForm.resetFields();
|
||||||
|
errorCodeForm.setFieldsValue({
|
||||||
|
status: true,
|
||||||
|
solution_status_0: true,
|
||||||
|
solution_type_0: 'text'
|
||||||
|
});
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
} catch (error) {
|
resetSolutionFields();
|
||||||
console.log('Validate Failed:', error);
|
setIsErrorCodeFormReadOnly(false);
|
||||||
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' });
|
setEditingErrorCodeKey(null);
|
||||||
}
|
};
|
||||||
setIsErrorCodeFormReadOnly(false); // Reset status read-only
|
|
||||||
setEditingErrorCodeKey(null); // Reset key item yang diedit
|
const handleCreateNewErrorCode = () => {
|
||||||
|
resetErrorCodeForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteErrorCode = (key) => {
|
const handleDeleteErrorCode = (key) => {
|
||||||
setErrorCodes(errorCodes.filter(item => item.key !== key));
|
if (errorCodes.length <= 1) {
|
||||||
message.success('Error code berhasil dihapus');
|
NotifAlert({
|
||||||
};
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
// Fungsi untuk mengubah file menjadi base64 untuk preview gambar
|
message: 'Setiap brand harus memiliki minimal 1 error code!'
|
||||||
const getBase64 = (file) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => resolve(reader.result);
|
|
||||||
reader.onerror = (error) => reject(error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fungsi untuk menangani preview file dari komponen Upload
|
|
||||||
const handleUploadPreview = async (file) => {
|
|
||||||
// Jika file bukan gambar, buka di tab baru
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
const url = URL.createObjectURL(file.originFileObj || file);
|
|
||||||
window.open(url, '_blank');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Jika file adalah gambar, tampilkan di modal
|
|
||||||
if (!file.url && !file.preview) {
|
setErrorCodes(errorCodes.filter(item => item.key !== key));
|
||||||
file.preview = await getBase64(file.originFileObj || file);
|
NotifOk({
|
||||||
}
|
icon: 'success',
|
||||||
setPreviewImage(file.url || file.preview);
|
title: 'Berhasil',
|
||||||
setPreviewOpen(true);
|
message: 'Error code berhasil dihapus!'
|
||||||
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadProps = {
|
const handleFileView = (pathSolution, fileType) => {
|
||||||
multiple: true,
|
const filePath = pathSolution || '';
|
||||||
accept: '.pdf,.jpg,.jpeg,.png,.gif',
|
if (!filePath) return;
|
||||||
onRemove: (file) => {
|
|
||||||
const newFileList = fileList.filter(item => item.uid !== file.uid);
|
const parts = filePath.split('/');
|
||||||
setFileList(newFileList);
|
if (parts.length < 2) return;
|
||||||
},
|
|
||||||
beforeUpload: (file) => {
|
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);
|
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type);
|
||||||
if (!isAllowedType) {
|
if (!isAllowedType) {
|
||||||
message.error(`${file.name} bukan file PDF atau gambar yang diizinkan.`);
|
NotifAlert({
|
||||||
return Upload.LIST_IGNORE;
|
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]);
|
setFileList(prevList => [...prevList, file]);
|
||||||
return false; // Prevent auto-upload
|
NotifOk({
|
||||||
},
|
icon: 'success',
|
||||||
fileList,
|
title: 'Berhasil',
|
||||||
onPreview: handleUploadPreview, // Tambahkan handler onPreview
|
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 = [
|
const handleFileRemove = (file) => {
|
||||||
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
|
const newFileList = fileList.filter(item => item.uid !== file.uid);
|
||||||
{ title: 'Trouble Description', dataIndex: 'description', key: 'description' },
|
setFileList(newFileList);
|
||||||
{
|
};
|
||||||
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 renderStepContent = () => {
|
const renderStepContent = () => {
|
||||||
if (currentStep === 0) {
|
if (currentStep === 0) {
|
||||||
return (
|
return (
|
||||||
<Form layout="vertical" form={brandForm} onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))} initialValues={formData}>
|
<BrandForm
|
||||||
<Form.Item label="Status">
|
form={brandForm}
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
formData={formData}
|
||||||
<Form.Item name="status" valuePropName="checked" noStyle>
|
onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))}
|
||||||
<Switch
|
isEdit={false}
|
||||||
checked={formData.status}
|
|
||||||
style={{ backgroundColor: formData.status ? '#23A55A' : '#bfbfbf' }}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
<Text style={{ marginLeft: 8 }}>{formData.status ? 'Active' : 'Inactive'}</Text>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Brand Code" name="brand_code">
|
|
||||||
<Input placeholder={'Brand Code Auto Fill'} disabled style={{ backgroundColor: '#f5f5f5', cursor: 'not-allowed' }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item label="Brand Name" name="brandName" rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item label="Manufacturer" name="manufacturer" rules={[{ required: true, message: 'Manufacturer wajib diisi!' }]}>
|
|
||||||
<Input placeholder="Enter Manufacturer" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item label="Brand Type" name="brandType">
|
|
||||||
<Input placeholder="Enter Brand Type (Optional)" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item label="Model" name="model">
|
|
||||||
<Input placeholder="Enter Model (Optional)" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Form.Item label="Description" name="description">
|
|
||||||
<Input.TextArea rows={4} placeholder="Enter Description (Optional)" />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>Tambah Error Code</Title>
|
<Title level={5} style={{ marginBottom: 16 }}>
|
||||||
<Form form={errorCodeForm} layout="vertical" initialValues={{ status: true }}>
|
{isErrorCodeFormReadOnly
|
||||||
<Form.Item label="Status">
|
? 'View Error Code'
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
: (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code')
|
||||||
<Form.Item name="status" valuePropName="checked" noStyle>
|
}
|
||||||
<Switch
|
</Title>
|
||||||
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
|
<Form
|
||||||
disabled={isErrorCodeFormReadOnly}
|
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>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Title level={5}>Daftar Error Code ({errorCodes.length})</Title>
|
<ErrorCodeTable
|
||||||
<Table columns={errorCodeColumns} dataSource={errorCodes} rowKey="key" pagination={false} />
|
errorCodes={errorCodes}
|
||||||
|
loading={loading}
|
||||||
|
onPreview={handlePreviewErrorCode}
|
||||||
|
onEdit={handleEditErrorCode}
|
||||||
|
onDelete={handleDeleteErrorCode}
|
||||||
|
onFileView={handleFileView}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
@@ -352,8 +368,7 @@ const AddBrandDevice = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Title level={4}>Tambah Brand Device</Title>
|
<Title level={4} style={{ margin: '0 0 24px 0' }}>Tambah Brand Device</Title>
|
||||||
<Divider />
|
|
||||||
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
||||||
<Step title="Brand Device Details" />
|
<Step title="Brand Device Details" />
|
||||||
<Step title="Error Codes" />
|
<Step title="Error Codes" />
|
||||||
@@ -362,57 +377,15 @@ const AddBrandDevice = () => {
|
|||||||
{renderStepContent()}
|
{renderStepContent()}
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
<FormActions
|
||||||
<ConfigProvider
|
currentStep={currentStep}
|
||||||
theme={{
|
onPreviousStep={() => setCurrentStep(currentStep - 1)}
|
||||||
token: { colorBgContainer: '#E9F6EF' },
|
onNextStep={handleNextStep}
|
||||||
components: {
|
onSave={handleFinish}
|
||||||
Button: {
|
onCancel={handleCancel}
|
||||||
defaultBg: 'white',
|
confirmLoading={confirmLoading}
|
||||||
defaultColor: '#23A55A',
|
isEditMode={false}
|
||||||
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>
|
|
||||||
</Card>
|
</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 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 {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -7,46 +7,10 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
} from '@ant-design/icons';
|
} 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 { useNavigate } from 'react-router-dom';
|
||||||
import TableList from '../../../../components/Global/TableList';
|
import TableList from '../../../../components/Global/TableList';
|
||||||
import { getAllBrands } from '../../../../api/master-brand';
|
import { getAllBrands, deleteBrand } 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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||||
{
|
{
|
||||||
@@ -58,43 +22,45 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Brand Device ',
|
title: 'Brand Device ',
|
||||||
dataIndex: 'brandName',
|
dataIndex: 'brand_name',
|
||||||
key: 'brandName',
|
key: 'brand_name',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Type',
|
title: 'Type',
|
||||||
dataIndex: 'brandType',
|
dataIndex: 'brand_type',
|
||||||
key: 'brandType',
|
key: 'brand_type',
|
||||||
width: '15%',
|
width: '15%',
|
||||||
|
render: (text) => text || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Manufacturer',
|
title: 'Manufacturer',
|
||||||
dataIndex: 'manufacturer',
|
dataIndex: 'brand_manufacture',
|
||||||
key: 'manufacturer',
|
key: 'brand_manufacture',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Model',
|
title: 'Model',
|
||||||
dataIndex: 'model',
|
dataIndex: 'brand_model',
|
||||||
key: 'model',
|
key: 'brand_model',
|
||||||
width: '15%',
|
width: '15%',
|
||||||
|
render: (text) => text || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'is_active',
|
||||||
key: 'status',
|
key: 'is_active',
|
||||||
width: '10%',
|
width: '10%',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, { status }) => (
|
render: (_, { is_active }) => (
|
||||||
<>
|
<>
|
||||||
{status === 'Active' ? (
|
{is_active === true ? (
|
||||||
<Tag color={'green'} key={'status'}>
|
<Tag color={'green'} key={'status'}>
|
||||||
Active
|
Running
|
||||||
</Tag>
|
</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag color={'red'} key={'status'}>
|
<Tag color={'red'} key={'status'}>
|
||||||
Inactive
|
Offline
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -138,7 +104,6 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
|
|
||||||
const ListBrandDevice = memo(function ListBrandDevice(props) {
|
const ListBrandDevice = memo(function ListBrandDevice(props) {
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
const [brandDeviceData, setBrandDeviceData] = useState(initialBrandDeviceData);
|
|
||||||
|
|
||||||
const defaultFilter = { search: '' };
|
const defaultFilter = { search: '' };
|
||||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
@@ -149,14 +114,14 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
if (props.actionMode == 'list') {
|
if (props.actionMode === 'list') {
|
||||||
setFormDataFilter(defaultFilter);
|
setFormDataFilter(defaultFilter);
|
||||||
doFilter();
|
doFilter();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate('/signin');
|
navigate('/signin');
|
||||||
}
|
}
|
||||||
}, [props.actionMode, brandDeviceData]);
|
}, [props.actionMode, navigate]);
|
||||||
|
|
||||||
const doFilter = () => {
|
const doFilter = () => {
|
||||||
setTrigerFilter((prev) => !prev);
|
setTrigerFilter((prev) => !prev);
|
||||||
@@ -174,41 +139,55 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showPreviewModal = (param) => {
|
const showPreviewModal = (param) => {
|
||||||
props.setSelectedData(param);
|
// Direct navigation without loading, page will handle its own loading
|
||||||
props.setActionMode('preview');
|
navigate(`/master/brand-device/view/${param.brand_id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditModal = (param = null) => {
|
const showEditModal = (param = null) => {
|
||||||
props.setSelectedData(param);
|
// Direct navigation without loading, page will handle its own loading
|
||||||
props.setActionMode('edit');
|
if (param) {
|
||||||
|
navigate(`/master/brand-device/edit/${param.brand_id}`);
|
||||||
|
} else {
|
||||||
|
navigate('/master/brand-device/add');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDeleteDialog = (param) => {
|
const showDeleteDialog = (param) => {
|
||||||
NotifConfirmDialog({
|
NotifConfirmDialog({
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
title: 'Konfirmasi',
|
title: 'Konfirmasi',
|
||||||
message: 'Apakah anda yakin hapus data "' + param.brandName + '" ?',
|
message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
|
||||||
onConfirm: () => handleDelete(param.brand_id),
|
onConfirm: () => handleDelete(param.brand_id),
|
||||||
onCancel: () => props.setSelectedData(null),
|
onCancel: () => {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (brand_id) => {
|
const handleDelete = async (brand_id) => {
|
||||||
// Find brand name before deleting
|
try {
|
||||||
const brandToDelete = brandDeviceData.find((brand) => brand.brand_id === brand_id);
|
const response = await deleteBrand(brand_id);
|
||||||
|
|
||||||
// Simulate delete API call
|
if (response && response.statusCode === 200) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
NotifOk({
|
||||||
|
|
||||||
// Remove from state
|
|
||||||
const updatedBrands = brandDeviceData.filter((brand) => brand.brand_id !== brand_id);
|
|
||||||
setBrandDeviceData(updatedBrands);
|
|
||||||
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
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 (
|
return (
|
||||||
@@ -267,7 +246,9 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => navigate('/master/brand-device/add')}
|
onClick={() => {
|
||||||
|
navigate('/master/brand-device/add');
|
||||||
|
}}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
Tambah Brand Device
|
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 TableList from '../../../../components/Global/TableList';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
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';
|
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -29,19 +32,19 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
key: 'tag_name',
|
key: 'tag_name',
|
||||||
width: '70%',
|
width: '70%',
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'Value',
|
// title: 'Value',
|
||||||
dataIndex: 'val',
|
// dataIndex: 'val',
|
||||||
key: 'val',
|
// key: 'val',
|
||||||
width: '10%',
|
// width: '10%',
|
||||||
render: (_, record) => Number(record.val).toFixed(4),
|
// render: (_, record) => Number(record.val).toFixed(4),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: 'Stat',
|
// title: 'Stat',
|
||||||
dataIndex: 'status',
|
// dataIndex: 'status',
|
||||||
key: 'status',
|
// key: 'status',
|
||||||
width: '10%',
|
// width: '10%',
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
const dateNow = dayjs();
|
const dateNow = dayjs();
|
||||||
@@ -49,21 +52,21 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
|
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
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 = {
|
const defaultFilter = {
|
||||||
criteria: '',
|
criteria: '',
|
||||||
plant_sub_section_id: 0,
|
plant_sub_section_id: 0,
|
||||||
from: dateNowFormated,
|
from: dateNowFormated,
|
||||||
to: dateNowFormated,
|
to: dateNowFormated,
|
||||||
interval: 10,
|
interval: periode,
|
||||||
};
|
};
|
||||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
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 handleSearch = () => {
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
const formattedDateEnd = endDate.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' }}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||||
<TableList
|
<TableList
|
||||||
firstLoad={false}
|
firstLoad={false}
|
||||||
getData={getAllHistoryValueReport}
|
mobile
|
||||||
|
cardColor={'#d38943ff'}
|
||||||
|
header={'datetime'}
|
||||||
|
getData={getAllHistoryValueReportPivot}
|
||||||
queryParams={formDataFilter}
|
queryParams={formDataFilter}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
columnDynamic={'columns'}
|
||||||
triger={trigerFilter}
|
triger={trigerFilter}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
: [],
|
: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// setTrendingValue(cleanedData);
|
setTrendingValue(cleanedData);
|
||||||
} else {
|
} else {
|
||||||
// 🔹 Jika tidak ada data dari API
|
// 🔹 Jika tidak ada data dari API
|
||||||
// setTrendingValue([]);
|
setTrendingValue([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user