From 47f7c7b682809159bd61253849a5f61873b8d579 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Tue, 28 Oct 2025 11:07:54 +0700 Subject: [PATCH 1/2] integration api brandDevice, file upload in brand device --- src/App.jsx | 11 +- src/api/file-uploads.jsx | 126 ++++ .../master/brandDevice/AddBrandDevice.jsx | 611 +++++++++--------- .../master/brandDevice/EditBrandDevice.jsx | 445 +++++++++++++ .../master/brandDevice/ViewBrandDevice.jsx | 551 ++++++++++++++++ src/pages/master/brandDevice/ViewFilePage.jsx | 490 ++++++++++++++ .../brandDevice/component/BrandForm.jsx | 76 +++ .../brandDevice/component/ErrorCodeForm.jsx | 220 +++++++ .../component/FileUploadHandler.jsx | 121 ++++ .../brandDevice/component/FormActions.jsx | 70 ++ .../brandDevice/component/ListBrandDevice.jsx | 135 ++-- .../brandDevice/component/ListErrorCode.jsx | 84 +++ .../brandDevice/component/ListErrorMaster.jsx | 33 - .../brandDevice/component/SolutionField.jsx | 237 +++++++ .../master/brandDevice/hooks/errorCode.js | 264 ++++++++ 15 files changed, 3044 insertions(+), 430 deletions(-) create mode 100644 src/api/file-uploads.jsx create mode 100644 src/pages/master/brandDevice/EditBrandDevice.jsx create mode 100644 src/pages/master/brandDevice/ViewBrandDevice.jsx create mode 100644 src/pages/master/brandDevice/ViewFilePage.jsx create mode 100644 src/pages/master/brandDevice/component/BrandForm.jsx create mode 100644 src/pages/master/brandDevice/component/ErrorCodeForm.jsx create mode 100644 src/pages/master/brandDevice/component/FileUploadHandler.jsx create mode 100644 src/pages/master/brandDevice/component/FormActions.jsx create mode 100644 src/pages/master/brandDevice/component/ListErrorCode.jsx delete mode 100644 src/pages/master/brandDevice/component/ListErrorMaster.jsx create mode 100644 src/pages/master/brandDevice/component/SolutionField.jsx create mode 100644 src/pages/master/brandDevice/hooks/errorCode.js diff --git a/src/App.jsx b/src/App.jsx index 7eeb525..1ad3150 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,9 +14,13 @@ import IndexDevice from './pages/master/device/IndexDevice'; import IndexTag from './pages/master/tag/IndexTag'; import IndexUnit from './pages/master/unit/IndexUnit'; import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; -import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; import IndexStatus from './pages/master/status/IndexStatus'; import IndexShift from './pages/master/shift/IndexShift'; +// Brand device +import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; +import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice'; +import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice'; +import ViewFilePage from './pages/master/brandDevice/ViewFilePage'; // Jadwal Shift import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift'; @@ -74,6 +78,11 @@ const App = () => { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/api/file-uploads.jsx b/src/api/file-uploads.jsx new file mode 100644 index 0000000..d629846 --- /dev/null +++ b/src/api/file-uploads.jsx @@ -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 +}; \ No newline at end of file diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx index 488039d..0cf4a91 100644 --- a/src/pages/master/brandDevice/AddBrandDevice.jsx +++ b/src/pages/master/brandDevice/AddBrandDevice.jsx @@ -1,22 +1,26 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Input, Divider, Typography, Switch, Button, Steps, Form, message, Table, Row, Col, Modal, Card, Tag, Upload, ConfigProvider, Space } from 'antd'; -import { PlusOutlined, UploadOutlined, EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { Divider, Typography, Button, Steps, Form, Row, Col, Card } from 'antd'; import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; +import { createBrand } from '../../../api/master-brand'; +import BrandForm from './component/BrandForm'; +import ErrorCodeForm from './component/ErrorCodeForm'; +import ErrorCodeTable from './component/ListErrorCode'; +import FormActions from './component/FormActions'; +import { useErrorCodeLogic } from './hooks/errorCode'; +import { uploadFile, getFolderFromFileType } from '../../../api/file-uploads'; -const { Text, Title } = Typography; +const { Title } = Typography; const { Step } = Steps; -// Mock API for Error Codes (can be moved to a separate file later) -const mockErrorCodeApi = { - errorCodes: [], - createErrorCode: async (data) => { - const newId = mockErrorCodeApi.errorCodes.length > 0 ? Math.max(...mockErrorCodeApi.errorCodes.map(ec => ec.error_code_id)) + 1 : 1; - const newErrorCode = { ...data, error_code_id: newId }; - mockErrorCodeApi.errorCodes.push(newErrorCode); - return { statusCode: 201, data: newErrorCode }; - }, +const defaultData = { + brand_name: '', + brand_type: '', + brand_model: '', + brand_manufacture: '', + is_active: true, + brand_code: '', }; const AddBrandDevice = () => { @@ -27,29 +31,37 @@ const AddBrandDevice = () => { const [confirmLoading, setConfirmLoading] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [fileList, setFileList] = useState([]); - const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); // State untuk mengontrol form read-only - const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); // State untuk melacak item yang sedang diedit - // State untuk preview file - const [previewOpen, setPreviewOpen] = useState(false); - const [previewImage, setPreviewImage] = useState(''); - const [previewTitle, setPreviewTitle] = useState(''); - - // Watch for form values changes to update the switch color - const statusValue = Form.useWatch('status', errorCodeForm); - - const defaultData = { - brandName: '', - brandType: '', - model: '', - manufacturer: '', - status: true, - brand_code: '', - description: '', - }; - + const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); + const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); + const [loading, setLoading] = useState(false); const [formData, setFormData] = useState(defaultData); const [errorCodes, setErrorCodes] = useState([]); + const { + solutionFields, + solutionTypes, + solutionStatuses, + firstSolutionValid, + solutionsToDelete, + handleAddSolutionField, + handleRemoveSolutionField, + handleSolutionTypeChange, + handleSolutionStatusChange, + resetSolutionFields, + checkFirstSolutionValid, + setSolutionsForExistingRecord + } = useErrorCodeLogic(errorCodeForm, fileList); + + useEffect(() => { + setBreadcrumbItems([ + { title: • Master }, + { + title: navigate('/master/brand-device')}>Brand Device + }, + { title: Tambah Brand Device } + ]); + }, [setBreadcrumbItems, navigate]); + const handleCancel = () => { navigate('/master/brand-device'); }; @@ -59,290 +71,294 @@ const AddBrandDevice = () => { await brandForm.validateFields(); setCurrentStep(1); } catch (error) { - console.log('Validate Failed:', error); NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' }); } }; const handleFinish = async () => { - if (errorCodes.length === 0) { - NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Silakan tambahkan minimal satu error code.' }); - return; - } - setConfirmLoading(true); try { - const finalFormData = { ...formData, status: formData.status ? 'Active' : 'Inactive' }; - console.log("Saving brand device:", finalFormData); - await new Promise((resolve) => setTimeout(resolve, 500)); - const newBrandDeviceId = Date.now(); - console.log("Brand device saved with ID:", newBrandDeviceId); + const transformedErrorCodes = errorCodes.map(ec => ({ + error_code: ec.error_code, + error_code_name: ec.error_code_name || '', + error_code_description: ec.error_code_description || '', + is_active: ec.status !== undefined ? ec.status : true, + solution: (ec.solution || []).map(sol => ({ + solution_name: sol.solution_name, + type_solution: sol.type_solution, + text_solution: sol.text_solution || '', + path_solution: sol.path_solution || '', + is_active: sol.is_active !== false + })) + })); - console.log("Saving error codes:", errorCodes); - for (const errorCode of errorCodes) { - if (errorCode.another_solution === 'image' && errorCode.image) { - console.log(`Uploading image for error code ${errorCode.error_code}:`, errorCode.image.name); - } - await mockErrorCodeApi.createErrorCode({ - ...errorCode, - brand_device_id: newBrandDeviceId + const finalFormData = { + brand_name: formData.brand_name, + brand_type: formData.brand_type || '', + brand_model: formData.brand_model || '', + brand_manufacture: formData.brand_manufacture, + is_active: formData.is_active, + error_code: transformedErrorCodes.length > 0 ? transformedErrorCodes : [ + { + error_code: "DEFAULT", + error_code_name: "Default Error Code", + error_code_description: "Default error description", + is_active: true, + solution: [ + { + solution_name: "Default Solution", + type_solution: "text", + text_solution: "Default solution text", + path_solution: "", + is_active: true + } + ] + } + ] + }; + + const response = await createBrand(finalFormData); + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: response.message || 'Brand Device berhasil ditambahkan.', + }); + navigate('/master/brand-device'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Gagal menambahkan Brand Device', }); - console.log("Saved error code:", errorCode.error_code); } - - setConfirmLoading(false); - NotifOk({ icon: 'success', title: 'Berhasil', message: 'Brand Device dan Error Code berhasil disimpan.' }); - navigate('/master/brand-device'); } catch (error) { - setConfirmLoading(false); - console.error("Failed to save data:", error); NotifAlert({ icon: "error", title: "Gagal", - message: "Gagal menyimpan data. Silakan coba lagi.", + message: error.message || "Gagal menyimpan data. Silakan coba lagi.", }); + } finally { + setConfirmLoading(false); } }; const handlePreviewErrorCode = (record) => { - errorCodeForm.setFieldsValue(record); // Isi form dengan data record - setFileList(record.fileList || []); // Muat file jika ada - setIsErrorCodeFormReadOnly(true); // Jadikan form read-only - setEditingErrorCodeKey(null); // Bukan dalam mode edit + errorCodeForm.setFieldsValue({ + error_code: record.error_code, + error_code_name: record.error_code_name, + error_code_description: record.error_code_description, + status: record.status, + }); + setFileList(record.fileList || []); + setIsErrorCodeFormReadOnly(true); + setEditingErrorCodeKey(null); + + if (record.solution && record.solution.length > 0) { + setSolutionsForExistingRecord(record.solution, errorCodeForm); + } }; const handleEditErrorCode = (record) => { - errorCodeForm.setFieldsValue(record); // Isi form dengan data record - setFileList(record.fileList || []); // Muat file jika ada - setIsErrorCodeFormReadOnly(false); // Aktifkan form untuk diedit - setEditingErrorCodeKey(record.key); // Tandai item ini sebagai yang sedang diedit + errorCodeForm.setFieldsValue({ + error_code: record.error_code, + error_code_name: record.error_code_name, + error_code_description: record.error_code_description, + status: record.status, + }); + setFileList(record.fileList || []); + setIsErrorCodeFormReadOnly(false); + setEditingErrorCodeKey(record.key); + + if (record.solution && record.solution.length > 0) { + setSolutionsForExistingRecord(record.solution, errorCodeForm); + } }; - const handleAddErrorCode = async () => { - try { - const values = await errorCodeForm.validateFields(); - const newErrorCode = { - ...values, - status: values.status === undefined ? true : values.status, - fileList: fileList, - key: editingErrorCodeKey || `temp-${Date.now()}` // Gunakan key yang ada jika edit, jika tidak buat baru - }; - - if (editingErrorCodeKey) { - setErrorCodes(errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item)); - message.success('Error code berhasil diupdate'); - } else { - setErrorCodes([...errorCodes, newErrorCode]); - message.success('Error code berhasil ditambahkan'); - } - errorCodeForm.resetFields(); - setFileList([]); - } catch (error) { - console.log('Validate Failed:', error); - NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' }); + const handleAddErrorCode = async (newErrorCode) => { + if (editingErrorCodeKey) { + const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item); + setErrorCodes(updatedCodes); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Error code berhasil diupdate!' + }); + } else { + const updatedCodes = [...errorCodes, newErrorCode]; + setErrorCodes(updatedCodes); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Error code berhasil ditambahkan!' + }); } - setIsErrorCodeFormReadOnly(false); // Reset status read-only - setEditingErrorCodeKey(null); // Reset key item yang diedit + + resetErrorCodeForm(); + }; + + const resetErrorCodeForm = () => { + errorCodeForm.resetFields(); + errorCodeForm.setFieldsValue({ + status: true, + solution_status_0: true, + solution_type_0: 'text' + }); + setFileList([]); + resetSolutionFields(); + setIsErrorCodeFormReadOnly(false); + setEditingErrorCodeKey(null); + }; + + const handleCreateNewErrorCode = () => { + resetErrorCodeForm(); }; const handleDeleteErrorCode = (key) => { - setErrorCodes(errorCodes.filter(item => item.key !== key)); - message.success('Error code berhasil dihapus'); - }; - - // Fungsi untuk mengubah file menjadi base64 untuk preview gambar - const getBase64 = (file) => - new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result); - reader.onerror = (error) => reject(error); - }); - - // Fungsi untuk menangani preview file dari komponen Upload - const handleUploadPreview = async (file) => { - // Jika file bukan gambar, buka di tab baru - if (!file.type.startsWith('image/')) { - const url = URL.createObjectURL(file.originFileObj || file); - window.open(url, '_blank'); + if (errorCodes.length <= 1) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Setiap brand harus memiliki minimal 1 error code!' + }); return; } - // Jika file adalah gambar, tampilkan di modal - if (!file.url && !file.preview) { - file.preview = await getBase64(file.originFileObj || file); - } - setPreviewImage(file.url || file.preview); - setPreviewOpen(true); - setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1)); + + setErrorCodes(errorCodes.filter(item => item.key !== key)); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Error code berhasil dihapus!' + }); }; - const uploadProps = { - multiple: true, - accept: '.pdf,.jpg,.jpeg,.png,.gif', - onRemove: (file) => { - const newFileList = fileList.filter(item => item.uid !== file.uid); - setFileList(newFileList); - }, - beforeUpload: (file) => { + const handleFileView = (pathSolution, fileType) => { + const filePath = pathSolution || ''; + if (!filePath) return; + + const parts = filePath.split('/'); + if (parts.length < 2) return; + + const [folder, filename] = parts; + const encodedFileName = encodeURIComponent(filename); + const navigationPath = `/master/brand-device/view/temp/files/${folder}/${encodedFileName}`; + navigate(navigationPath); + }; + + const handleSolutionFileUpload = async (file) => { + try { const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type); if (!isAllowedType) { - message.error(`${file.name} bukan file PDF atau gambar yang diizinkan.`); - return Upload.LIST_IGNORE; + NotifAlert({ + icon: 'error', + title: 'Error', + message: `${file.name} bukan file PDF atau gambar yang diizinkan.` + }); + return; } - setFileList(prevList => [...prevList, file]); - return false; // Prevent auto-upload - }, - fileList, - onPreview: handleUploadPreview, // Tambahkan handler onPreview + + const fileExtension = file.name.split('.').pop().toLowerCase(); + const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension); + const fileType = isImage ? 'image' : 'pdf'; + const folder = getFolderFromFileType(fileType); + + const uploadResponse = await uploadFile(file, folder); + const actualPath = uploadResponse.data?.path_solution || ''; + + if (actualPath) { + file.uploadPath = actualPath; + file.solution_name = file.name; + file.solutionId = solutionFields[0]; + file.type_solution = fileType; + setFileList(prevList => [...prevList, file]); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `${file.name} berhasil diupload!` + }); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: `Gagal mengupload ${file.name}` + }); + } + } catch (error) { + console.error('Error uploading file:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: `Gagal mengupload ${file.name}. Silakan coba lagi.` + }); + } }; - const errorCodeColumns = [ - { title: 'Error Code', dataIndex: 'error_code', key: 'error_code' }, - { title: 'Trouble Description', dataIndex: 'description', key: 'description' }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - render: (status) => ( - - {status ? 'Active' : 'Inactive'} - - ), - }, - { - title: 'Action', - key: 'action', - render: (_, record) => ( - - - - - - - - - + + {isErrorCodeFormReadOnly + ? 'View Error Code' + : (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code') + } + + + - Daftar Error Code ({errorCodes.length}) - + ); @@ -352,8 +368,7 @@ const AddBrandDevice = () => { return ( - Tambah Brand Device - + Tambah Brand Device @@ -362,57 +377,15 @@ const AddBrandDevice = () => { {renderStepContent()} -
- - - {currentStep > 0 && ( - - )} - - - {currentStep < 1 && ( - - )} - {currentStep === 1 && ( - - )} - -
- {/* Modal untuk preview gambar */} - setPreviewOpen(false)} - > - example - + setCurrentStep(currentStep - 1)} + onNextStep={handleNextStep} + onSave={handleFinish} + onCancel={handleCancel} + confirmLoading={confirmLoading} + isEditMode={false} + />
); }; diff --git a/src/pages/master/brandDevice/EditBrandDevice.jsx b/src/pages/master/brandDevice/EditBrandDevice.jsx new file mode 100644 index 0000000..105193e --- /dev/null +++ b/src/pages/master/brandDevice/EditBrandDevice.jsx @@ -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: • Master }, + { + title: navigate('/master/brand-device')}>Brand Device + }, + { title: Edit Brand Device } + ]); + + 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 ( + setFormData(prev => ({...prev, ...allValues}))} + isEdit={true} + /> + ); + } + + if (currentStep === 1) { + return ( + +
+ + {isErrorCodeFormReadOnly + ? (editingErrorCodeKey ? 'View Error Code' : 'Error Code Form') + : (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code') + } + + + + + + + ({ + key: `loading-${index}`, + error_code: 'Loading...', + error_code_name: 'Loading...', + solution: [] + })) : + errorCodes + } + loading={loading} + onPreview={handlePreviewErrorCode} + onEdit={handleEditErrorCode} + onDelete={handleDeleteErrorCode} + onFileView={handleFileView} + /> + + + ); + } + return null; + }; + + return ( + + Edit Brand Device + + + + +
+ {loading && ( +
+ +
+ )} +
+ {renderStepContent()} +
+
+ + setCurrentStep(currentStep - 1)} + onNextStep={handleNextStep} + onSave={handleFinish} + onCancel={handleCancel} + confirmLoading={confirmLoading} + isEditMode={true} + /> +
+ ); +}; + +export default EditBrandDevice; \ No newline at end of file diff --git a/src/pages/master/brandDevice/ViewBrandDevice.jsx b/src/pages/master/brandDevice/ViewBrandDevice.jsx new file mode 100644 index 0000000..e8b369d --- /dev/null +++ b/src/pages/master/brandDevice/ViewBrandDevice.jsx @@ -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: • Master }, + { + title: navigate('/master/brand-device')}>Brand Device + }, + { title: View Brand Device } + ]); + + 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 ( + //
+ // + //
+ // ); + // } + + if (!brandData && !loading) { + return
Brand Device not found
; + } + + // 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) => ( +
+ {solutions && solutions.length > 0 ? ( +
+ {solutions.length} solution(s) +
+ {solutions.slice(0, 2).map((sol, index) => ( +
+ {sol.type_solution === 'text' ? ( + • {sol.solution_name} + ) : ( + • {sol.solution_name} ({sol.type_solution}) + )} +
+ ))} + {solutions.length > 2 && ( +
+ ...and {solutions.length - 2} more +
+ )} +
+
+ ) : ( + No solutions + )} +
+ ) + }, + { + title: 'Status', + dataIndex: 'is_active', + key: 'is_active', + width: '10%', + align: 'center', + render: (_, { is_active }) => ( + + {is_active ? 'Active' : 'Inactive'} + + ), + }, + { + title: 'Action', + key: 'action', + align: 'center', + width: '5%', + render: (_, record) => ( +
+ + + + + {solution.type_solution === 'pdf' ? ( + + ) : ( + + )} + {solution.solution_name} + + + + + {solution.type_solution.toUpperCase()} + + + +
+ {solution.type_solution === 'text' ? ( + {solution.text_solution} + ) : ( + + File: {solution.path_document || solution.path_solution} + {solution.path_document && ( + + )} + + )} +
+ + + ))} + + ) : ( + No solutions available + )} + + ), + }); + }} + 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 ( +
+
+ Status +
+
+
+
+
+ {(brandData || {}).is_active ? 'Running' : 'Offline'} +
+ +
+ Brand Code +
+
+
+ {brandData?.brand_code || (loading ? 'Loading...' : '-')} +
+
+ + +
+
+ Brand Name +
+
+
+ {brandData?.brand_name || (loading ? 'Loading...' : '-')} +
+
+ + +
+ Manufacture +
+
+
+ {brandData?.brand_manufacture || (loading ? 'Loading...' : '-')} +
+
+ + + + + +
+ Brand Type +
+
+
+ {brandData?.brand_type || (loading ? 'Loading...' : '-')} +
+
+ + +
+ Model +
+
+
+ {brandData?.brand_model || (loading ? 'Loading...' : '-')} +
+
+ + + + ); + } + + if (currentStep === 1) { + const errorCodesCount = loading ? 3 : (brandData?.error_code?.length || 0); + + return ( +
+ + Error Codes ({errorCodesCount}) + + {errorCodesCount > 0 ? ( + + ) : ( + !loading && No error codes available + )} +
+ ); + } + + return null; + }; + + return ( + + + + + View Brand Device + + + + + + + + + + + + + + {/* Content area with blur overlay during loading */} +
+ {/* Overlay with blur effect during loading - only on content area */} + {loading && ( +
+ +
+ )} + +
+ {renderStepContent()} +
+
+ + +
+ {currentStep > 0 && ( + + )} + {currentStep < 1 && ( + + )} +
+ + + ); +}; + +export default ViewBrandDevice; \ No newline at end of file diff --git a/src/pages/master/brandDevice/ViewFilePage.jsx b/src/pages/master/brandDevice/ViewFilePage.jsx new file mode 100644 index 0000000..f6c5b03 --- /dev/null +++ b/src/pages/master/brandDevice/ViewFilePage.jsx @@ -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: • Master }, + { + title: navigate('/master/brand-device')}>Brand Device + } + ]; + + if (isFromEdit) { + breadcrumbItems.push({ + title: navigate(`/master/brand-device/edit/${fallbackId || id}`)}>Edit Brand Device + }); + } else { + breadcrumbItems.push({ + title: navigate(`/master/brand-device/view/${fallbackId || id}`)}>View Brand Device + }); + } + + breadcrumbItems.push({ title: View Document }); + + 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 ( + + ); + } + + 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 ( +
+ {isImage ? ( +
+
+ +
Loading image...
+
+
+ ) : isPdf ? ( +
+
+ +
Loading PDF...
+
+
+ ) : ( +
+
+ +
Loading file...
+
+
+ )} +
+ ); + } + + if (isImage) { + return ( +
+ {actualFileName} setError('Failed to load image')} + /> +
+ ); + } + + if (isPdf) { + const displayUrl = pdfBlobUrl || fileUrl; + + return ( +
+ {pdfBlobUrl ? ( +