diff --git a/package.json b/package.json index 21bd8b0..e942371 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "exceljs": "^4.4.0", "file-saver": "^2.0.5", "html2canvas": "^1.4.1", - "jspdf": "^3.0.1", + "jspdf": "^3.0.4", + "jspdf-autotable": "^5.0.2", "mqtt": "^5.14.0", "qrcode": "^1.5.4", "react": "^18.2.0", @@ -30,6 +31,7 @@ "react-icons": "^4.11.0", "react-router-dom": "^6.22.3", "react-svg": "^16.3.0", + "recharts": "^3.6.0", "sweetalert2": "^11.17.2" }, "devDependencies": { diff --git a/public/assets/pupuk-indonesia-1.png b/public/assets/pupuk-indonesia-1.png new file mode 100644 index 0000000..689669a Binary files /dev/null and b/public/assets/pupuk-indonesia-1.png differ diff --git a/public/assets/pupuk-indonesia-2.jpg b/public/assets/pupuk-indonesia-2.jpg new file mode 100644 index 0000000..d1a5d2b Binary files /dev/null and b/public/assets/pupuk-indonesia-2.jpg differ diff --git a/src/App.jsx b/src/App.jsx index 8493d06..ea24775 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -36,7 +36,7 @@ import IndexNotification from './pages/notification/IndexNotification'; import IndexRole from './pages/role/IndexRole'; import IndexUser from './pages/user/IndexUser'; import IndexContact from './pages/contact/IndexContact'; -import DetailNotificationTab from './pages/detailNotification/IndexDetailNotification'; +import DetailNotificationTab from './pages/notificationDetail/IndexNotificationDetail'; import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart'; import SvgTest from './pages/home/SvgTest'; @@ -51,6 +51,9 @@ import SvgAirDryerC from './pages/home/SvgAirDryerC'; import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm'; import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent'; +// Image Viewer +import ImageViewer from './Utils/ImageViewer'; + const App = () => { return ( @@ -61,7 +64,7 @@ const App = () => { } /> } /> } /> { } /> + } /> + }> } /> } /> @@ -91,6 +96,11 @@ const App = () => { } /> } /> } /> + } /> + } /> + } /> + + {/* Brand Device Routes */} } /> } /> } /> @@ -107,9 +117,6 @@ const App = () => { path="brand-device/view/temp/files/:fileName" element={} /> - } /> - } /> - } /> }> @@ -142,7 +149,6 @@ const App = () => { } /> - {/* Catch-all */} } /> diff --git a/src/Utils/ImageViewer.jsx b/src/Utils/ImageViewer.jsx new file mode 100644 index 0000000..a463f32 --- /dev/null +++ b/src/Utils/ImageViewer.jsx @@ -0,0 +1,248 @@ +import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { getFileUrl, getFolderFromFileType } from '../api/file-uploads'; + +const ImageViewer = () => { + const { fileName } = useParams(); + const [fileUrl, setFileUrl] = useState(''); + const [error, setError] = useState(''); + const [zoom, setZoom] = useState(1); + const [isImage, setIsImage] = useState(false); + + useEffect(() => { + if (!fileName) { + setError('No file specified'); + return; + } + + try { + const decodedFileName = decodeURIComponent(fileName); + const fileExtension = decodedFileName.split('.').pop()?.toLowerCase(); + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + + setIsImage(imageExtensions.includes(fileExtension)); + + const folder = getFolderFromFileType(fileExtension); + + const url = getFileUrl(folder, decodedFileName); + setFileUrl(url); + + document.title = `File Viewer - ${decodedFileName}`; + } catch (error) { + + setError('Failed to load file'); + } + }, [fileName]); + + useEffect(() => { + const handleKeyDown = (e) => { + if (!isImage) return; + + if (e.key === '+' || e.key === '=') { + setZoom(prev => Math.min(prev + 0.1, 3)); + } else if (e.key === '-' || e.key === '_') { + setZoom(prev => Math.max(prev - 0.1, 0.1)); + } else if (e.key === '0') { + setZoom(1); + } else if (e.key === 'Escape') { + window.close(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isImage]); + + + const handleWheel = (e) => { + if (!isImage || !e.ctrlKey) return; + + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + setZoom(prev => Math.min(Math.max(prev + delta, 0.1), 3)); + }; + + const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.1, 3)); + const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.1, 0.1)); + const handleResetZoom = () => setZoom(1); + + if (error) { + return ( +
+
+

Error

+

{error}

+
+
+ ); + } + + + if (!isImage) { + return ( +
+
+

File Type Not Supported

+

Image viewer only supports image files.

+

Please use direct file preview for PDFs and other documents.

+
+
+ ); + } + + return ( +
+ + {isImage && ( +
+ + + {Math.round(zoom * 100)}% + + + +
+ )} + + + {isImage && fileUrl ? ( +
+ {decodeURIComponent(fileName)} 1 ? 'move' : 'default' + }} + onError={() => setError('Failed to load image')} + draggable={false} + /> +
+ ) : isImage ? ( +
+

Loading image...

+
+ ) : null} + + + {isImage && ( +
+
Mouse wheel + Ctrl: Zoom
+
Keyboard: +/− Zoom, 0: Reset, ESC: Close
+
+ )} +
+ ); +}; + +export default ImageViewer; \ No newline at end of file diff --git a/src/api/master-brand.jsx b/src/api/master-brand.jsx index 942e4e3..908d8ef 100644 --- a/src/api/master-brand.jsx +++ b/src/api/master-brand.jsx @@ -47,4 +47,63 @@ const deleteBrand = async (id) => { return response.data; }; -export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand }; +const getErrorCodesByBrandId = async (brandId, queryParams) => { + const query = queryParams ? `?${queryParams.toString()}` : ''; + const response = await SendRequest({ + method: 'get', + prefix: `error-code/brand/${brandId}${query}`, + }); + + return response.data; +}; + +const getErrorCodeById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `error-code/${id}`, + }); + + return response.data; +}; + +const createErrorCode = async (brandId, queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `error-code/brand/${brandId}`, + params: queryParams, + }); + + return response.data; +}; + +const updateErrorCode = async (brandId, errorCodeId, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `error-code/brand/${brandId}/${errorCodeId}`, + params: queryParams, + }); + + return response.data; +}; + +const deleteErrorCode = async (brandId, errorCode) => { + const response = await SendRequest({ + method: 'delete', + prefix: `error-code/brand/${brandId}/${errorCode}`, + }); + + return response.data; +}; + +export { + getAllBrands, + getBrandById, + createBrand, + updateBrand, + deleteBrand, + getErrorCodesByBrandId, + getErrorCodeById, + createErrorCode, + updateErrorCode, + deleteErrorCode +}; diff --git a/src/api/notification.jsx b/src/api/notification.jsx index fe38523..2e04cf8 100644 --- a/src/api/notification.jsx +++ b/src/api/notification.jsx @@ -18,4 +18,38 @@ const getNotificationById = async (id) => { return response.data; }; -export { getAllNotification, getNotificationById }; +const getNotificationDetail = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `notification/${id}`, + }); + + return response.data; +}; + +// Create new notification log +const createNotificationLog = async (data) => { + const response = await SendRequest({ + method: 'post', + prefix: 'notification-log', + params: data, + }); + return response.data; +}; + +// Get notification logs by notification_error_id +const getNotificationLogByNotificationId = async (notificationId) => { + const response = await SendRequest({ + method: 'get', + prefix: `notification-log/notification_error/${notificationId}`, + }); + return response.data; +}; + +export { + getAllNotification, + getNotificationById, + getNotificationDetail, + createNotificationLog, + getNotificationLogByNotificationId +}; diff --git a/src/pages/contact/component/DetailContact.jsx b/src/pages/contact/component/DetailContact.jsx index 3b6b873..ed4ff19 100644 --- a/src/pages/contact/component/DetailContact.jsx +++ b/src/pages/contact/component/DetailContact.jsx @@ -14,7 +14,6 @@ const DetailContact = memo(function DetailContact(props) { name: '', phone: '', is_active: true, - contact_type: '', }; const [formData, setFormData] = useState(defaultData); @@ -37,13 +36,7 @@ const DetailContact = memo(function DetailContact(props) { } }; - const handleContactTypeChange = (value) => { - setFormData((prev) => ({ - ...prev, - contact_type: value, - })); - }; - + const handleStatusToggle = (checked) => { setFormData({ ...formData, @@ -58,7 +51,6 @@ const DetailContact = memo(function DetailContact(props) { const validationRules = [ { field: 'name', label: 'Contact Name', required: true }, { field: 'phone', label: 'Phone', required: true }, - { field: 'contact_type', label: 'Contact Type', required: true }, ]; if ( @@ -97,7 +89,6 @@ const DetailContact = memo(function DetailContact(props) { contact_name: formData.name, contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number is_active: formData.is_active, - contact_type: formData.contact_type, }; let response; @@ -145,18 +136,16 @@ const DetailContact = memo(function DetailContact(props) { phone: props.selectedData.contact_phone || props.selectedData.phone, is_active: props.selectedData.is_active || props.selectedData.status === 'active', - contact_type: props.selectedData.contact_type || props.contactType || '', }); } else if (props.actionMode === 'add') { setFormData({ name: '', phone: '', is_active: true, - contact_type: props.contactType === 'all' ? '' : props.contactType || '', }); } } - }, [props.showModal, props.actionMode, props.selectedData, props.contactType]); + }, [props.showModal, props.actionMode, props.selectedData]); return (
-
-
- Status -
-
-
- -
+ {/* Status field only show in add mode*/} + {props.actionMode === 'add' && ( + <>
- {formData.is_active ? 'Active' : 'Inactive'} +
+ Status +
+
+
+ +
+
+ {formData.is_active ? 'Active' : 'Inactive'} +
+
-
-
- + + + )}
Name @@ -251,7 +249,8 @@ const DetailContact = memo(function DetailContact(props) { style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }} />
-
+ {/* Contact Type */} + {/*
Contact Type * -
+
*/}
); diff --git a/src/pages/contact/component/ListContact.jsx b/src/pages/contact/component/ListContact.jsx index 936361d..84ab232 100644 --- a/src/pages/contact/component/ListContact.jsx +++ b/src/pages/contact/component/ListContact.jsx @@ -1,5 +1,5 @@ import React, { memo, useState, useEffect } from 'react'; -import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag } from 'antd'; +import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag, Switch } from 'antd'; import { PlusOutlined, EditOutlined, @@ -10,9 +10,43 @@ import { } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; -import { getAllContact, deleteContact } from '../../../api/contact'; +import { getAllContact, deleteContact, updateContact } from '../../../api/contact'; + +const ContactCard = memo(function ContactCard({ + contact, + showEditModal, + showDeleteModal, + onStatusToggle, +}) { + const handleStatusToggle = async (checked) => { + try { + const updatedContact = { + contact_name: contact.contact_name || contact.name, + contact_phone: contact.contact_phone || contact.phone, + is_active: checked, + contact_type: contact.contact_type, + }; + + await updateContact(contact.contact_id || contact.id, updatedContact); + + NotifAlert({ + icon: 'success', + title: 'Berhasil', + message: `Status "${contact.contact_name || contact.name}" berhasil diperbarui.`, + }); + + // Refresh contacts list + onStatusToggle && onStatusToggle(); + } catch (error) { + console.error('Error updating contact status:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Gagal memperbarui status kontak', + }); + } + }; -const ContactCard = memo(function ContactCard({ contact, showEditModal, showDeleteModal }) { return (
{/* Type Badge - Top Left */} -
+ {/*
{contact.contact_type === 'operator' ? 'Operator' : contact.contact_type === 'gudang' ? 'Gudang' : 'Unknown'} -
+
*/} - {/* Status Badge - Top Right */} -
- {contact.status === 'active' ? ( - - Active - - ) : ( - - InActive - - )} + {/* Status Slider - Top Right */} +
+
+ + + {contact.status === 'active' ? 'Active' : 'Inactive'} + +
{/* Main Content */} @@ -316,7 +368,7 @@ const ListContact = memo(function ListContact(props) { { const value = e.target.value; @@ -382,7 +434,8 @@ const ListContact = memo(function ListContact(props) { marginBottom: '16px', }} > - + /> */}
{getFilteredContacts().length === 0 ? ( @@ -423,6 +476,7 @@ const ListContact = memo(function ListContact(props) { }} showEditModal={showEditModal} showDeleteModal={showDeleteModal} + onStatusToggle={fetchContacts} /> ))} diff --git a/src/pages/detailNotification/IndexDetailNotification.jsx b/src/pages/detailNotification/IndexDetailNotification.jsx deleted file mode 100644 index 66f177b..0000000 --- a/src/pages/detailNotification/IndexDetailNotification.jsx +++ /dev/null @@ -1,357 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { - Layout, - Card, - Row, - Col, - Typography, - Space, - Button, - Spin, - Result, - Input, -} from 'antd'; -import { - ArrowLeftOutlined, - CloseCircleFilled, - WarningFilled, - CheckCircleFilled, - InfoCircleFilled, - CloseOutlined, - BookOutlined, - ToolOutlined, - HistoryOutlined, - FilePdfOutlined, - PlusOutlined, - UserOutlined, -} from '@ant-design/icons'; -// Path disesuaikan karena lokasi file berubah -// import { getNotificationById } from '../../api/notification'; // Dihapus karena belum ada di file API -import UserHistoryModal from '../notification/component/UserHistoryModal'; -import LogHistoryModal from '../notification/component/LogHistoryModal'; - -const { Content } = Layout; -const { Text, Paragraph, Link } = Typography; - -// Menggunakan kembali fungsi transform dari ListNotification untuk konsistensi data -const transformNotificationData = (item) => ({ - id: `notification-${item.notification_error_id || 'dummy'}-0`, - type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical', - title: item.device_name || 'Unknown Device', - issue: item.error_code_name || 'Unknown Error', - description: `${item.error_code} - ${item.error_code_name}`, - timestamp: new Date(item.created_at || Date.now()).toLocaleString('id-ID'), - location: item.device_location || 'Location not specified', - details: item.message_error_issue || 'No details available', - link: '#', - subsection: item.solution_name || 'N/A', - isRead: item.is_read || false, - status: item.is_read ? 'Resolved' : item.is_delivered ? 'Delivered' : 'Pending', - tag: item.error_code, - plc: item.plc || 'N/A', -}); - -const getDummyNotificationById = (id) => { - console.log("Fetching dummy data for ID:", id); - // Data mentah dummy, seolah-olah dari API - const rawDummyData = { device_name: 'Compressor C-101', error_code_name: 'High Temperature', error_code: 'TEMP-H-303', device_location: 'Gudang Produksi A', message_error_issue: 'Suhu kompresor terdeteksi melebihi ambang batas aman.', is_delivered: true, plc: 'PLC-UTL-01' }; - // Mengolah data mentah dummy menggunakan transform function - return transformNotificationData(rawDummyData); -}; - -const getIconAndColor = (type) => { - switch (type) { - case 'critical': - return { IconComponent: CloseCircleFilled, color: '#ff4d4f', bgColor: '#fff1f0' }; - case 'warning': - return { IconComponent: WarningFilled, color: '#faad14', bgColor: '#fffbe6' }; - case 'resolved': - return { IconComponent: CheckCircleFilled, color: '#52c41a', bgColor: '#f6ffed' }; - default: - return { IconComponent: InfoCircleFilled, color: '#1890ff', bgColor: '#e6f7ff' }; - } -}; - -const DetailNotificationTab = () => { - const { notificationId } = useParams(); // Mungkin perlu disesuaikan jika route berbeda - const navigate = useNavigate(); - const [notification, setNotification] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [modalContent, setModalContent] = useState(null); // 'user', 'log', atau null - const [isAddingLog, setIsAddingLog] = useState(false); - - const logHistoryData = [ - { - id: 1, - timestamp: '04-11-2025 11:55 WIB', - addedBy: { - name: 'Budi Santoso', - phone: '081122334455', - }, - description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', - }, - { - id: 2, - timestamp: '04-11-2025 11:45 WIB', - addedBy: { - name: 'John Doe', - phone: '081234567890', - }, - description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', - }, - { - id: 3, - timestamp: '04-11-2025 11:40 WIB', - addedBy: { - name: 'Jane Smith', - phone: '087654321098', - }, - description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', - }, - ]; - - useEffect(() => { - const fetchDetail = async () => { - try { - setLoading(true); - // Ganti dengan fungsi API asli Anda - // const response = await getNotificationById(notificationId); - // setNotification(response.data); - - // Menggunakan data dummy untuk sekarang - const dummyData = getDummyNotificationById(notificationId); - if (dummyData) { - setNotification(dummyData); - } else { - throw new Error('Notification not found'); - } - - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - }; - - fetchDetail(); - }, [notificationId]); - - if (loading) { - return ( - - - - ); - } - - if (error || !notification) { - return ( - - navigate('/notification')}>Back to List} - /> - - ); - } - - const { color } = getIconAndColor(notification.type); - - return ( - - - -
- - - - - - - - -
- - Error Notification Detail - -
-
- - - - {/* Kolom Kiri: Data Kompresor */} - - - - - -
- - - {notification.title} -
{notification.issue}
- -
-
- Plant Subsection -
{notification.subsection}
- Time -
{notification.timestamp}
-
-
- - Value
N/A
- Treshold
N/A
-
-
-
-
- - - {/* Kolom Kanan: Informasi Teknis */} - - - -
PLC
{notification.plc || 'N/A'}
-
Status
{notification.status}
-
Tag
{notification.tag}
-
-
- -
- - - Handling Guideline - Spare Part - setModalContent('log')} style={{ cursor: 'pointer' }}>Log Activity - - - - - - - - Error 303.pdf - lihat disini - - - SOP Kompresor.pdf - lihat disini - - - - - - - - - - -
- -
- Available - - - Air Filter - Filters incoming air to remove dust. - -
-
-
-
- - - - - - - {isAddingLog && ( - <> - - Add New Log / Update Progress - - - - )} - - - - {logHistoryData.map((log) => ( - - - {log.addedBy.name}:{' '} - {log.description} - - - {log.timestamp} - - - ))} - - - -
-
-
-
- - setModalContent(null)} - notificationData={notification} - /> - setModalContent(null)} - notificationData={notification} - /> -
- ); -}; - -export default DetailNotificationTab; \ No newline at end of file diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx index 0f91e57..dfa88e5 100644 --- a/src/pages/master/brandDevice/AddBrandDevice.jsx +++ b/src/pages/master/brandDevice/AddBrandDevice.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom'; import { Divider, Typography, @@ -9,102 +9,870 @@ import { Row, Col, Card, - ConfigProvider, - Table, - Tag, + Spin, Space, + ConfigProvider, } from 'antd'; -import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { + EditOutlined, + DeleteOutlined +} from '@ant-design/icons'; +import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; -import { createBrand } from '../../../api/master-brand'; +import { getBrandById, createBrand, createErrorCode, getErrorCodesByBrandId, updateErrorCode, deleteErrorCode, deleteBrand } from '../../../api/master-brand'; import BrandForm from './component/BrandForm'; -import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm'; -import ErrorCodeListModal from './component/ErrorCodeListModal'; -import FormActions from './component/FormActions'; +import ErrorCodeForm from './component/ErrorCodeForm'; import SolutionForm from './component/SolutionForm'; -import SparepartForm from './component/SparepartForm'; -import { useSolutionLogic } from './hooks/solution'; -import { useSparepartLogic } from './hooks/sparepart'; -import { uploadFile, getFolderFromFileType } from '../../../api/file-uploads'; -import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; +import SparepartSelect from './component/SparepartSelect'; +import ListErrorCode from './component/ListErrorCode'; const { Title } = Typography; const { Step } = Steps; -const defaultData = { - brand_name: '', - brand_type: '', - brand_model: '', - brand_manufacture: '', - is_active: true, - brand_code: '', -}; - const AddBrandDevice = () => { const navigate = useNavigate(); + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const location = useLocation(); const { setBreadcrumbItems } = useBreadcrumb(); const [brandForm] = Form.useForm(); const [errorCodeForm] = Form.useForm(); const [solutionForm] = Form.useForm(); - const [sparepartForm] = 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(false); - const [formData, setFormData] = useState(defaultData); - const [errorCodes, setErrorCodes] = useState([]); const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [selectedSparepartIds, setSelectedSparepartIds] = useState([]); + const [loading, setLoading] = useState(false); + const tab = searchParams.get('tab'); + const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : (location.state?.phase || 0)); + const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); + const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); + const [searchText, setSearchText] = useState(''); + const [apiErrorCodes, setApiErrorCodes] = useState([]); + const [trigerFilter, setTrigerFilter] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const [brandInfo, setBrandInfo] = useState({}); + const [tempErrorCodes, setTempErrorCodes] = useState([]); + const [existingErrorCodes, setExistingErrorCodes] = useState([]); + const [selectedErrorCode, setSelectedErrorCode] = useState(null); + const [solutionFields, setSolutionFields] = useState([0]); + const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' }); + const [solutionStatuses, setSolutionStatuses] = useState({ 0: true }); + const [currentSolutionData, setCurrentSolutionData] = useState([]); + const [confirmLoading, setConfirmLoading] = useState(false); + const [temporaryBrandId, setTemporaryBrandId] = useState(null); + const [isTemporaryBrand, setIsTemporaryBrand] = useState(false); + const [isAddingNewErrorCode, setIsAddingNewErrorCode] = useState(false); - const { - solutionFields, - solutionTypes, - solutionStatuses, - solutionsToDelete, - firstSolutionValid, - handleAddSolutionField, - handleRemoveSolutionField, - handleSolutionTypeChange, - handleSolutionStatusChange, - resetSolutionFields, - checkFirstSolutionValid, - getSolutionData, - setSolutionsForExistingRecord, - } = useSolutionLogic(solutionForm); + const getSolutionData = () => { + if (!solutionForm) return []; + try { + const values = solutionForm.getFieldsValue(true); + const solutions = []; - // For spareparts, we'll use the local state directly since it's just an array of IDs - const handleSparepartChange = (values) => { - setSelectedSparepartIds(values || []); + solutionFields.forEach(fieldKey => { + let solution = null; + + if (values.solution_items && values.solution_items[fieldKey]) { + solution = values.solution_items[fieldKey]; + } + + if (!solution || !solution.name || solution.name.trim() === '') { + return; + } + + const solutionType = solutionTypes[fieldKey] || solution.type || 'text'; + let isValid = true; + + if (solutionType === 'text') { + isValid = solution.text && solution.text.trim() !== ''; + } else if (solutionType === 'file') { + const hasPathSolution = solution.path_solution && solution.path_solution.trim() !== ''; + const hasFileUpload = (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0); + const hasFile = (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0); + isValid = hasPathSolution || hasFileUpload || hasFile; + } + + if (isValid) { + solutions.push(solution); + } + }); + + return solutions; + } catch (error) { + return []; + } }; - const resetSparepartFields = () => { - setSelectedSparepartIds([]); + const resetSolutionFields = () => { + setSolutionFields([0]); + setSolutionTypes({ 0: 'text' }); + setSolutionStatuses({ 0: true }); + + if (solutionForm && solutionForm.resetFields) { + solutionForm.resetFields(); + setTimeout(() => { + solutionForm.setFieldsValue({ + solution_items: { + 0: { + name: '', + type: 'text', + text: '', + status: true, + file: null, + fileUpload: null + } + } + }); + }, 100); + } + setCurrentSolutionData([]); }; - const getSparepartData = () => { - return selectedSparepartIds; - }; + const setSolutionsForExistingRecord = (solutions, targetForm) => { - const setSparepartsForExistingRecord = (sparepartData) => { - if (!sparepartData) { - setSelectedSparepartIds([]); + if (!targetForm || !solutions || solutions.length === 0) { return; } - if (Array.isArray(sparepartData)) { - setSelectedSparepartIds(sparepartData); - } else if (typeof sparepartData === 'object' && sparepartData.spareparts) { - setSelectedSparepartIds(sparepartData.spareparts || []); - } else { - setSelectedSparepartIds(sparepartData.map(sp => sp.sparepart_id || sp.brand_sparepart_id || sp.id).filter(id => id)); + targetForm.resetFields(); + + const solutionItems = {}; + const newSolutionFields = []; + const newSolutionTypes = {}; + const newSolutionStatuses = {}; + + solutions.forEach((solution, index) => { + const fieldKey = index; + newSolutionFields.push(fieldKey); + + const isFileType = solution.type_solution && solution.type_solution !== 'text'; + newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text'; + newSolutionStatuses[fieldKey] = solution.is_active; + + let fileObject = null; + if (isFileType && (solution.path_solution || solution.path_document)) { + fileObject = { + uploadPath: solution.path_solution || solution.path_document, + path_solution: solution.path_solution || solution.path_document, + name: solution.file_upload_name || (solution.path_solution || solution.path_document).split('/').pop() || 'File', + type_solution: solution.type_solution, + isExisting: true, + size: 0, + url: solution.path_solution || solution.path_document + }; + } + + solutionItems[fieldKey] = { + brand_code_solution_id: solution.brand_code_solution_id, + name: solution.solution_name || '', + type: isFileType ? 'file' : 'text', + text: solution.text_solution || '', + status: solution.is_active, + file: fileObject, + fileUpload: fileObject, + path_solution: solution.path_solution || solution.path_document || null, + fileName: solution.file_upload_name || null + }; + }); + + setSolutionFields(newSolutionFields); + + setSolutionTypes(newSolutionTypes); + + setSolutionStatuses(newSolutionStatuses); + + + targetForm.resetFields(); + + setTimeout(() => { + targetForm.setFieldsValue({ + solution_items: solutionItems + }); + + setTimeout(() => { + Object.keys(solutionItems).forEach(key => { + const solution = solutionItems[key]; + targetForm.setFieldValue(['solution_items', key, 'name'], solution.name); + targetForm.setFieldValue(['solution_items', key, 'type'], solution.type); + targetForm.setFieldValue(['solution_items', key, 'text'], solution.text); + targetForm.setFieldValue(['solution_items', key, 'file'], solution.file); + targetForm.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload); + targetForm.setFieldValue(['solution_items', key, 'status'], solution.status); + targetForm.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution); + targetForm.setFieldValue(['solution_items', key, 'fileName'], solution.fileName); + }); + + + const finalValues = targetForm.getFieldsValue(); + }, 100); + }, 100); + }; + + const handleAddSolutionField = () => { + const newKey = Math.max(...solutionFields, 0) + 1; + setSolutionFields(prev => [...prev, newKey]); + setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' })); + setSolutionStatuses(prev => ({ ...prev, [newKey]: true })); + }; + + const handleRemoveSolutionField = (fieldKey) => { + if (solutionFields.length > 1) { + setSolutionFields(prev => prev.filter(key => key !== fieldKey)); + const newTypes = { ...solutionTypes }; + const newStatuses = { ...solutionStatuses }; + delete newTypes[fieldKey]; + delete newStatuses[fieldKey]; + setSolutionTypes(newTypes); + setSolutionStatuses(newStatuses); + + const currentValues = solutionForm.getFieldsValue(); + if (currentValues.solution_items && currentValues.solution_items[fieldKey]) { + delete currentValues.solution_items[fieldKey]; + solutionForm.setFieldsValue(currentValues); + } } }; + const handleSolutionTypeChange = (fieldKey, type) => { + setSolutionTypes(prev => ({ ...prev, [fieldKey]: type })); + + if (type === 'file') { + solutionForm.setFieldValue(['solution_items', fieldKey, 'text'], ''); + } + + if (type === 'text') { + solutionForm.setFieldValue(['solution_items', fieldKey, 'file'], null); + solutionForm.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null); + } + }; + + const handleSolutionStatusChange = (fieldKey, status) => { + setSolutionStatuses(prev => ({ ...prev, [fieldKey]: status })); + }; + + const handleNextStep = async () => { + try { + setConfirmLoading(true); + const brandValues = await brandForm.validateFields(); + + const brandApiData = { + brand_name: brandValues.brand_name, + brand_type: brandValues.brand_type || '', + brand_manufacture: brandValues.brand_manufacture || '', + brand_model: brandValues.brand_model || '', + is_active: brandValues.is_active !== undefined ? brandValues.is_active : true + }; + + const response = await createBrand(brandApiData); + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + const newBrandInfo = { + ...brandValues, + brand_id: response.data.brand_id, + brand_code: response.data.brand_code + }; + setBrandInfo(newBrandInfo); + setTemporaryBrandId(response.data.brand_id); + setIsTemporaryBrand(true); + setCurrentStep(1); + localStorage.setItem(`brand_device_add_last_phase`, '1'); + + NotifOk({ + icon: 'success', + title: 'Brand Created', + message: 'Brand berhasil dibuat. Silakan tambahkan minimal 1 error code.', + }); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Gagal membuat brand', + }); + } + } catch (error) { + NotifAlert({ + icon: 'error', + title: 'Validasi Error', + message: error.message || 'Silakan lengkapi semua field yang wajib diisi', + }); + } finally { + setConfirmLoading(false); + } + }; + + const handlePrevStep = () => { + setCurrentStep(0); + }; + + const handleCancel = async () => { + if (isTemporaryBrand && temporaryBrandId) { + try { + await deleteBrand(temporaryBrandId); + } catch (error) { + } + } + navigate('/master/brand-device'); + }; + + const handleAddErrorCode = () => { + resetErrorCodeForm(); + setIsErrorCodeFormReadOnly(false); + setEditingErrorCodeKey(null); + }; + + const loadErrorCodeData = (record) => { + setIsErrorCodeFormReadOnly(false); + const editingKey = record.tempId || `existing_${record.error_code_id}`; + setEditingErrorCodeKey(editingKey); + + errorCodeForm.setFieldsValue({ + error_code: record.error_code, + error_code_name: record.error_code_name || '', + error_code_description: record.error_code_description || '', + error_code_color: record.error_code_color || '#000000', + status: record.is_active, + }); + + if (record.path_icon) { + setErrorCodeIcon({ + name: record.path_icon.split('/').pop(), + uploadPath: record.path_icon, + url: record.path_icon, + }); + } + + if (record.solution && record.solution.length > 0) { + setSolutionsForExistingRecord(record.solution, solutionForm); + } + + if (record.spareparts && record.spareparts.length > 0) { + setSelectedSparepartIds(record.spareparts); + } + }; + + const handleSearch = () => { + setSearchText(searchValue); + setTrigerFilter((prev) => !prev); + }; + + const handleSearchClear = () => { + setSearchValue(''); + setSearchText(''); + setTrigerFilter((prev) => !prev); + }; + + + + const resetErrorCodeForm = () => { + errorCodeForm.resetFields(); + errorCodeForm.setFieldsValue({ + status: true, + }); + setErrorCodeIcon(null); + resetSolutionFields(); + setIsErrorCodeFormReadOnly(false); + setEditingErrorCodeKey(null); + setSelectedSparepartIds([]); + setSelectedErrorCode(null); + setIsAddingNewErrorCode(true); + }; + + const handleSaveErrorCode = async () => { + try { + setConfirmLoading(true); + const errorCodeValues = await errorCodeForm.validateFields(); + const solutionData = getSolutionData(); + + if (!errorCodeValues.error_code || !errorCodeValues.error_code_name) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Error code dan error name wajib diisi!', + }); + return; + } + + if (!solutionData || solutionData.length === 0) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Setiap error code harus memiliki minimal 1 solution!', + }); + return; + } + + const formattedSolutions = solutionData.map(solution => { + const solutionType = solution.type || 'text'; + + let typeSolution = solutionType === 'text' ? 'text' : 'image'; + if (solution.file && solution.file.type_solution) { + typeSolution = solution.file.type_solution; + } else if (solution.fileUpload && solution.fileUpload.type_solution) { + typeSolution = solution.fileUpload.type_solution; + } + + const formattedSolution = { + solution_name: solution.name, + type_solution: typeSolution, + is_active: solution.status !== false, + }; + + if (typeSolution === 'text') { + formattedSolution.text_solution = solution.text || ''; + formattedSolution.path_solution = ''; + } else { + formattedSolution.text_solution = ''; + + formattedSolution.path_solution = solution.path_solution || solution.file?.uploadPath || solution.fileUpload?.uploadPath || ''; + } + + if (formattedSolution.brand_code_solution_id) { + delete formattedSolution.brand_code_solution_id; + } + + return formattedSolution; + }); + + const payload = { + error_code: errorCodeValues.error_code, + error_code_name: errorCodeValues.error_code_name, + error_code_description: errorCodeValues.error_code_description || '', + error_code_color: errorCodeValues.error_code_color || '#000000', + path_icon: errorCodeIcon?.uploadPath || '', + is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status, + solution: formattedSolutions, + spareparts: selectedSparepartIds || [] + }; + + let response; + + if (editingErrorCodeKey && editingErrorCodeKey.startsWith('existing_')) { + const errorCodeId = editingErrorCodeKey.replace('existing_', ''); + response = await updateErrorCode(brandInfo.brand_id || id, errorCodeId, payload); + } else { + response = await createErrorCode(brandInfo.brand_id || id, payload); + } + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: editingErrorCodeKey ? 'Error code berhasil diupdate!' : 'Error code berhasil ditambahkan!', + }); + + resetErrorCodeForm(); + setTrigerFilter(prev => !prev); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Gagal menyimpan error code', + }); + } + } catch (error) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: error.message || 'Harap isi semua kolom wajib!', + }); + } finally { + setConfirmLoading(false); + } + }; + + + const handleErrorCodeIconRemove = () => { + setErrorCodeIcon(null); + }; + + + const handleFinish = async () => { + setConfirmLoading(true); + try { + if (!brandInfo.brand_id) { + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Brand tidak ditemukan. Silakan refresh halaman.', + }); + return; + } + + if (brandInfo.brand_id) { + try { + const queryParams = new URLSearchParams(); + queryParams.set('page', '1'); + queryParams.set('limit', '15'); + + const response = await getErrorCodesByBrandId(brandInfo.brand_id, queryParams); + + if (response && response.statusCode === 200 && response.data) { + const freshErrorCodes = response.data.map(ec => ({ + ...ec, + tempId: `existing_${ec.error_code_id}`, + status: 'existing' + })); + setApiErrorCodes(freshErrorCodes); + + if (freshErrorCodes.length === 0 && tempErrorCodes.length === 0) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap tambahkan minimal 1 error code sebelum menyelesaikan.', + }); + return; + } + } else { + if (tempErrorCodes.length === 0) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap tambahkan minimal 1 error code sebelum menyelesaikan.', + }); + return; + } + } + } catch (error) { + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Gagal memeriksa error codes. Silakan coba lagi.', + }); + return; + } + } else if (!tempErrorCodes.length) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap tambahkan minimal 1 error code sebelum menyelesaikan.', + }); + return; + } + + setIsTemporaryBrand(false); + setTemporaryBrandId(null); + + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Brand device berhasil dibuat dengan error codes.', + }); + navigate('/master/brand-device'); + } catch (error) { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: error.message || 'Gagal menyelesaikan brand device', + }); + } finally { + setConfirmLoading(false); + } + }; + + const renderStepContent = () => { + if (currentStep === 0) { + return ( +
+ {loading && ( +
+ +
+ )} + +
+ ); + } + + if (currentStep === 1) { + const handleErrorCodeSelect = (errorCode) => { + setSelectedErrorCode(errorCode); + loadErrorCodeData(errorCode); + }; + + const handleAddNew = () => { + setSelectedErrorCode(null); + resetErrorCodeForm(); + }; + + return ( + + + { + setSearchText(value); + setSearchValue(value); + if (value === '') { + setTrigerFilter((prev) => !prev); + } + }} + onSearch={handleSearch} + onSearchClear={handleSearchClear} + /> + + + +
+ + + + Error Code Form + + +
+ } + style={{ + width: '100%', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)', + borderRadius: '12px' + }} + styles={{ + body: { padding: '16px 24px 12px 24px' }, + header: { + padding: '16px 24px', + borderBottom: '1px solid #f0f0f0', + backgroundColor: '#fafafa' + } + }} + > +
+
+ +
+ + + +
+
+
+

+ Solution +

+
+ { + }} + onFileView={(fileData) => { + if (fileData && (fileData.url || fileData.uploadPath)) { + window.open(fileData.url || fileData.uploadPath, '_blank'); + } + }} + isReadOnly={false} + solutionData={currentSolutionData} + /> +
+ + +
+
+
+

+ Sparepart Selection +

+
+
+ +
+
+ +
+ +
+ + {editingErrorCodeKey && ( + + )} +
+
+ +
+ + + ); + } + return null; + }; + useEffect(() => { + errorCodeForm.setFieldsValue({ + status: true, + }); + + const tab = searchParams.get('tab') || 'brand'; + setBreadcrumbItems([ - { title: • Master }, + { + title: • Master + }, { title: ( { ), }, ]); - }, [setBreadcrumbItems, navigate]); - const handleCancel = () => { - 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 { - // Validation: Ensure at least one error code - if (errorCodes.length === 0) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setidaknya tambahkan 1 error code!', - }); - setConfirmLoading(false); - return; - } - - const transformedErrorCodes = errorCodes.map((ec) => ({ - error_code: ec.error_code, - error_code_name: ec.error_code_name || '', - error_code_description: ec.error_code_description || '', - error_code_color: ec.error_code_color || '#000000', - path_icon: ec.path_icon || '', - is_active: ec.status !== undefined ? ec.status : true, - solution: (ec.solution || []).map((sol) => ({ - solution_name: sol.solution_name, - type_solution: sol.type_solution, - text_solution: sol.text_solution || '', - path_solution: sol.path_solution || '', - is_active: sol.is_active !== false, - })), - })); - - const sparepartData = getSparepartData(); - 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, - spareparts: sparepartData, - error_code: transformedErrorCodes, - }; - - console.log('Final form data:', finalFormData); // Debug log - - 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', - }); - } - } catch (error) { - console.error('Finish Error:', error); - NotifAlert({ - icon: 'error', - title: 'Gagal', - message: error.message || 'Gagal menyimpan data. Silakan coba lagi.', - }); - } finally { - setConfirmLoading(false); - } - }; - - const handlePreviewErrorCode = (record) => { - errorCodeForm.setFieldsValue({ - error_code: record.error_code, - error_code_name: record.error_code_name, - error_code_description: record.error_code_description, - error_code_color: record.error_code_color, - status: record.status, - }); - setFileList(record.fileList || []); - setErrorCodeIcon(record.errorCodeIcon || null); - setIsErrorCodeFormReadOnly(true); - setEditingErrorCodeKey(null); - - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); + if (location.state?.fromFileViewer && location.state.phase !== undefined) { + setCurrentStep(location.state.phase); } - if (record.sparepart && record.sparepart.length > 0) { - setSparepartsForExistingRecord(record.sparepart); + }, [setBreadcrumbItems, navigate, searchParams, location.state]); + + useEffect(() => { + if (brandInfo.brand_id && currentStep === 1) { + setTrigerFilter(prev => !prev); } - }; + }, [brandInfo.brand_id, currentStep]); - const handleEditErrorCode = (record) => { - // Prevent infinite loop - if (editingErrorCodeKey === record.key) { - return; - } - - errorCodeForm.setFieldsValue({ - error_code: record.error_code, - error_code_name: record.error_code_name, - error_code_description: record.error_code_description, - error_code_color: record.error_code_color || '#000000', - status: record.status !== false, - }); - setFileList(record.fileList || []); - setErrorCodeIcon(record.errorCodeIcon || null); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(record.key); - - if (record.solution && record.solution.length > 0) { - // Reset solution fields first - resetSolutionFields(); - // Then load new solutions - setTimeout(() => { - setSolutionsForExistingRecord(record.solution, solutionForm); - }, 0); - } else { - resetSolutionFields(); - } - - if (record.sparepart && record.sparepart.length > 0) { - setSparepartsForExistingRecord(record.sparepart); - } - }; - - const handleAddErrorCode = async () => { - try { - const formValues = errorCodeForm.getFieldsValue(); - - // Validation - if (!formValues.error_code || !formValues.error_code_name) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Error code dan error name wajib diisi!', - }); - return; - } - - // Validate at least 1 solution - const solutions = getSolutionData(); - - if (solutions.length === 0) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap error code harus memiliki minimal 1 solution!', - }); - return; - } - - const newErrorCode = { - key: Date.now(), - error_code: formValues.error_code, - error_code_name: formValues.error_code_name || '', - error_code_description: formValues.error_code_description || '', - error_code_color: formValues.error_code_color || '#000000', - path_icon: errorCodeIcon?.uploadPath || '', - status: formValues.status !== false, - errorCodeIcon: errorCodeIcon, - solution: solutions, - }; - - 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!', - }); - } - - // Reset all forms - resetErrorCodeForm(); - resetSolutionFields(); - } catch (error) { - console.error('Error adding error code:', error); - NotifAlert({ - icon: 'error', - title: 'Error', - message: 'Gagal menambahkan error code', - }); - } - }; - - const resetErrorCodeForm = () => { - errorCodeForm.resetFields(); - errorCodeForm.setFieldsValue({ - status: true, - solution_status_0: true, - solution_type_0: 'text', - }); - setFileList([]); - setErrorCodeIcon(null); - resetSolutionFields(); - resetSparepartFields(); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(null); - }; - - const handleCreateNewErrorCode = () => { - resetErrorCodeForm(); - }; - - const handleDeleteErrorCode = (key) => { - if (errorCodes.length <= 1) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap brand harus memiliki minimal 1 error code!', - }); - return; - } - - setErrorCodes(errorCodes.filter((item) => item.key !== key)); - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: 'Error code berhasil dihapus!', - }); - }; - - 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) { - NotifAlert({ - icon: 'error', - title: 'Error', - message: `${file.name} bukan file PDF atau gambar yang diizinkan.`, - }); - return; - } - - const fileExtension = file.name.split('.').pop().toLowerCase(); - const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension); - const fileType = isImage ? 'image' : 'pdf'; - const folder = getFolderFromFileType(fileType); - - const uploadResponse = await uploadFile(file, folder); - const actualPath = uploadResponse.data?.path_solution || ''; - - if (actualPath) { - file.uploadPath = actualPath; - file.solution_name = file.name; - file.solutionId = solutionFields[0]; - file.type_solution = fileType; - setFileList((prevList) => [...prevList, file]); - 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 handleFileRemove = (file) => { - const newFileList = fileList.filter((item) => item.uid !== file.uid); - setFileList(newFileList); - }; - - const handleErrorCodeIconUpload = (iconData) => { - setErrorCodeIcon(iconData); - }; - - const handleErrorCodeIconRemove = () => { - setErrorCodeIcon(null); - }; - - const renderStepContent = () => { - if (currentStep === 0) { - return ( - - setFormData((prev) => ({ ...prev, ...allValues })) + useEffect(() => { + const fetchErrorCodes = async () => { + if (brandInfo.brand_id) { + try { + const response = await getErrorCodesByBrandId(brandInfo.brand_id); + if (response && response.statusCode === 200) { + const errorCodes = response.data || []; + setApiErrorCodes(errorCodes); } - isEdit={false} - /> - ); - } + } catch (error) { + } + } + }; + fetchErrorCodes(); + }, [brandInfo.brand_id, trigerFilter]); - if (currentStep === 1) { - return ( - <> - - {/* Error Code Form Column */} - - -
- - -
- + useEffect(() => { + const handleBeforeUnload = async (event) => { + if (isTemporaryBrand && temporaryBrandId && currentStep === 0) { + try { + await deleteBrand(temporaryBrandId); + } catch (error) { + } + } + }; - {/* Solution Form Column */} - - -
- - -
- - - -
- - -
- + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [isTemporaryBrand, temporaryBrandId, currentStep]); - {/* Error Codes Table Column */} - - - { - const solutionCount = record.solution - ? record.solution.length - : 0; - return ( - 0 ? 'green' : 'red'} - > - {solutionCount} Sol - - ); - }, - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - width: '20%', - align: 'center', - render: (_, { status }) => ( - - {status ? 'Active' : 'Inactive'} - - ), - }, - { - title: 'Action', - key: 'action', - align: 'center', - width: '15%', - render: (_, record) => ( - - + {currentStep === 1 && ( + + )} + +
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )} +
+ + + ); }; -export default AddBrandDevice; +export default AddBrandDevice; \ No newline at end of file diff --git a/src/pages/master/brandDevice/EditBrandDevice.jsx b/src/pages/master/brandDevice/EditBrandDevice.jsx index b54d818..5aa8213 100644 --- a/src/pages/master/brandDevice/EditBrandDevice.jsx +++ b/src/pages/master/brandDevice/EditBrandDevice.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useNavigate, useParams, useLocation } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom'; import { Divider, Typography, @@ -10,106 +10,246 @@ import { Col, Card, Spin, - Modal, - ConfigProvider, - Table, Tag, Space, + Input, + ConfigProvider } from 'antd'; -import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; -import { getBrandById, updateBrand } from '../../../api/master-brand'; +import { getBrandById, updateBrand, getErrorCodesByBrandId, deleteErrorCode, updateErrorCode as updateErrorCodeAPI, createErrorCode as createErrorCodeAPI } from '../../../api/master-brand'; import { getFileUrl } from '../../../api/file-uploads'; +import { SendRequest } from '../../../components/Global/ApiRequest'; import BrandForm from './component/BrandForm'; -import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm'; +import ErrorCodeForm from './component/ErrorCodeForm'; import SolutionForm from './component/SolutionForm'; -import SparepartForm from './component/SparepartForm'; -import ErrorCodeListModal from './component/ErrorCodeListModal'; -import FormActions from './component/FormActions'; -import { useErrorCodeLogic } from './hooks/errorCode'; -import { useSolutionLogic } from './hooks/solution'; -import { useSparepartLogic } from './hooks/sparepart'; -import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; +import SparepartSelect from './component/SparepartSelect'; +import ListErrorCode from './component/ListErrorCode'; 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 [searchParams] = useSearchParams(); 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 [errorCodeIcon, setErrorCodeIcon] = useState(null); const [solutionForm] = Form.useForm(); - const [sparepartForm] = Form.useForm(); + const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [selectedSparepartIds, setSelectedSparepartIds] = useState([]); + const [loading, setLoading] = useState(false); + const tab = searchParams.get('tab'); + const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0); + const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); + const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); + const [searchText, setSearchText] = useState(''); + const [apiErrorCodes, setApiErrorCodes] = useState([]); + const [trigerFilter, setTrigerFilter] = useState(false); + const [brandInfo, setBrandInfo] = useState({}); + const [tempErrorCodes, setTempErrorCodes] = useState([]); + const [existingErrorCodes, setExistingErrorCodes] = useState([]); + const [selectedErrorCode, setSelectedErrorCode] = useState(null); + const [solutionFields, setSolutionFields] = useState([0]); + const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' }); + const [solutionStatuses, setSolutionStatuses] = useState({ 0: true }); + const [currentSolutionData, setCurrentSolutionData] = useState([]); + const [confirmLoading, setConfirmLoading] = useState(false); - const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic( - errorCodeForm, - fileList - ); + const getSolutionData = () => { + if (!solutionForm) return []; + try { + const values = solutionForm.getFieldsValue(true); + const solutions = []; - const { - solutionFields, - solutionTypes, - solutionStatuses, - handleAddSolutionField, - handleRemoveSolutionField, - handleSolutionTypeChange, - handleSolutionStatusChange, - resetSolutionFields, - getSolutionData, - setSolutionsForExistingRecord, - } = useSolutionLogic(solutionForm); + solutionFields.forEach(fieldKey => { + let solution = null; - // For spareparts, we'll use the local state directly since it's just an array of IDs - const handleSparepartChange = (values) => { - setSelectedSparepartIds(values || []); + if (values.solution_items && values.solution_items[fieldKey]) { + solution = values.solution_items[fieldKey]; + } + + if (!solution || !solution.name || solution.name.trim() === '') { + return; + } + + const solutionType = solutionTypes[fieldKey] || solution.type || 'text'; + let isValid = true; + + if (solutionType === 'text') { + isValid = solution.text && solution.text.trim() !== ''; + } else if (solutionType === 'file') { + const hasPathSolution = solution.path_solution && solution.path_solution.trim() !== ''; + const hasFileUpload = (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0); + const hasFile = (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0); + isValid = hasPathSolution || hasFileUpload || hasFile; + } + + if (isValid) { + solutions.push(solution); + } + }); + + return solutions; + } catch (error) { + return []; + } }; - const resetSparepartFields = () => { - setSelectedSparepartIds([]); + const resetSolutionFields = () => { + setSolutionFields([0]); + setSolutionTypes({ 0: 'text' }); + setSolutionStatuses({ 0: true }); + + if (solutionForm && solutionForm.resetFields) { + solutionForm.resetFields(); + setTimeout(() => { + solutionForm.setFieldsValue({ + solution_items: { + 0: { + name: '', + type: 'text', + text: '', + status: true, + file: null, + fileUpload: null + } + } + }); + }, 100); + } + setCurrentSolutionData([]); }; - const getSparepartData = () => { - return selectedSparepartIds; - }; + const setSolutionsForExistingRecord = (solutions, targetForm) => { - const setSparepartsForExistingRecord = (sparepartData) => { - if (!sparepartData) { - setSelectedSparepartIds([]); + if (!targetForm || !solutions || solutions.length === 0) { return; } - if (Array.isArray(sparepartData)) { - setSelectedSparepartIds(sparepartData); - } else if (typeof sparepartData === 'object' && sparepartData.spareparts) { - setSelectedSparepartIds(sparepartData.spareparts || []); - } else { - setSelectedSparepartIds(sparepartData.map(sp => sp.sparepart_id || sp.brand_sparepart_id || sp.id).filter(id => id)); + targetForm.resetFields(); + + const solutionItems = {}; + const newSolutionFields = []; + const newSolutionTypes = {}; + const newSolutionStatuses = {}; + + solutions.forEach((solution, index) => { + const fieldKey = index; + newSolutionFields.push(fieldKey); + + const isFileType = solution.type_solution && solution.type_solution !== 'text'; + newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text'; + newSolutionStatuses[fieldKey] = solution.is_active; + + let fileObject = null; + if (isFileType && (solution.path_solution || solution.path_document)) { + fileObject = { + uploadPath: solution.path_solution || solution.path_document, + path_solution: solution.path_solution || solution.path_document, + name: solution.file_upload_name || (solution.path_solution || solution.path_document).split('/').pop() || 'File', + type_solution: solution.type_solution, + isExisting: true, + size: 0, + url: solution.path_solution || solution.path_document + }; + } + + solutionItems[fieldKey] = { + brand_code_solution_id: solution.brand_code_solution_id, + name: solution.solution_name || '', + type: isFileType ? 'file' : 'text', + text: solution.text_solution || '', + status: solution.is_active, + file: fileObject, + fileUpload: fileObject, + path_solution: solution.path_solution || solution.path_document || null, + fileName: solution.file_upload_name || null + }; + }); + + setSolutionFields(newSolutionFields); + + setSolutionTypes(newSolutionTypes); + + setSolutionStatuses(newSolutionStatuses); + + + targetForm.resetFields(); + + setTimeout(() => { + targetForm.setFieldsValue({ + solution_items: solutionItems + }); + + setTimeout(() => { + Object.keys(solutionItems).forEach(key => { + const solution = solutionItems[key]; + targetForm.setFieldValue(['solution_items', key, 'name'], solution.name); + targetForm.setFieldValue(['solution_items', key, 'type'], solution.type); + targetForm.setFieldValue(['solution_items', key, 'text'], solution.text); + targetForm.setFieldValue(['solution_items', key, 'file'], solution.file); + targetForm.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload); + targetForm.setFieldValue(['solution_items', key, 'status'], solution.status); + targetForm.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution); + targetForm.setFieldValue(['solution_items', key, 'fileName'], solution.fileName); + }); + + + const finalValues = targetForm.getFieldsValue(); + }, 100); + }, 100); + }; + + const handleAddSolutionField = () => { + const newKey = Math.max(...solutionFields, 0) + 1; + setSolutionFields(prev => [...prev, newKey]); + setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' })); + setSolutionStatuses(prev => ({ ...prev, [newKey]: true })); + }; + + const handleRemoveSolutionField = (fieldKey) => { + if (solutionFields.length > 1) { + setSolutionFields(prev => prev.filter(key => key !== fieldKey)); + const newTypes = { ...solutionTypes }; + const newStatuses = { ...solutionStatuses }; + delete newTypes[fieldKey]; + delete newStatuses[fieldKey]; + setSolutionTypes(newTypes); + setSolutionStatuses(newStatuses); + + const currentValues = solutionForm.getFieldsValue(); + if (currentValues.solution_items && currentValues.solution_items[fieldKey]) { + delete currentValues.solution_items[fieldKey]; + solutionForm.setFieldsValue(currentValues); + } } }; + const handleSolutionTypeChange = (fieldKey, type) => { + setSolutionTypes(prev => ({ ...prev, [fieldKey]: type })); + + if (type === 'file') { + solutionForm.setFieldValue(['solution_items', fieldKey, 'text'], ''); + } + + if (type === 'text') { + solutionForm.setFieldValue(['solution_items', fieldKey, 'file'], null); + solutionForm.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null); + } + }; + + const handleSolutionStatusChange = (fieldKey, status) => { + setSolutionStatuses(prev => ({ ...prev, [fieldKey]: status })); + }; + useEffect(() => { + errorCodeForm.setFieldsValue({ + status: true, + }); + const fetchBrandData = async () => { const token = localStorage.getItem('token'); if (!token) { @@ -117,15 +257,12 @@ const EditBrandDevice = () => { 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`); - } + const tab = searchParams.get('tab') || 'brand'; setBreadcrumbItems([ - { title: • Master }, + { + title: • Master + }, { title: ( { 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, + + const brandInfoData = { brand_code: brandData.brand_code, + brand_name: brandData.brand_name, + brand_type: brandData.brand_type || '', + brand_manufacture: brandData.brand_manufacture || '', + brand_model: brandData.brand_model || '', + is_active: brandData.is_active }; - 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 || '', - error_code_color: ec.error_code_color || '#000000', - path_icon: ec.path_icon || '', - status: ec.is_active, - solution: ec.solution || [], - sparepart: ec.sparepart || [], - errorCodeIcon: ec.path_icon - ? { - name: ec.path_icon.split('/').pop(), // Ambil nama file dari path - uploadPath: ec.path_icon, - url: (() => { - const pathParts = ec.path_icon.split('/'); - const folder = pathParts[0]; - const filename = pathParts.slice(1).join('/'); - return getFileUrl(folder, filename); - })(), - type_solution: 'image', - } - : null, - })) - : []; + setBrandInfo(brandInfoData); + brandForm.setFieldsValue(brandInfoData); - setFormData(newFormData); - brandForm.setFieldsValue(newFormData); - setErrorCodes(existingErrorCodes); - - // Set the selected sparepart IDs if available in the response - if (response.data.spareparts) { - // Extract the IDs from the spareparts objects - const sparepartIds = response.data.spareparts.map(sp => sp.sparepart_id); - setSelectedSparepartIds(sparepartIds); - setSparepartsForExistingRecord(sparepartIds); + if (brandData.brand_id) { + try { + const errorCodesResponse = await getErrorCodesByBrandId(id || brandData.brand_id); + if (errorCodesResponse && errorCodesResponse.statusCode === 200) { + const apiErrorData = errorCodesResponse.data || []; + const existingCodes = apiErrorData.map(ec => ({ + ...ec, + tempId: `existing_${ec.error_code_id}`, + status: 'existing', + solution: ec.solution || [], + spareparts: ec.spareparts || [] + })); + setExistingErrorCodes(existingCodes); + setApiErrorCodes(existingCodes); + } + } catch (error) { + } } + + setCurrentStep(tab === 'brand' ? 0 : 1); } else { NotifAlert({ icon: 'error', @@ -218,285 +340,108 @@ const EditBrandDevice = () => { }; fetchBrandData(); - }, [id, setBreadcrumbItems, navigate, brandForm, location]); + }, [id, navigate]); + + useEffect(() => { + const tab = searchParams.get('tab') || 'brand'; + setCurrentStep(tab === 'brand' ? 0 : 1); + }, [searchParams]); + + + useEffect(() => { + if (currentStep === 1 && id) { + setTrigerFilter(prev => !prev); + } + }, [currentStep, id]); + + + useEffect(() => { + if (location.state?.refreshErrorCodes) { + + setTrigerFilter(prev => !prev); + + + const state = { ...location.state }; + delete state.refreshErrorCodes; + navigate(location.pathname + location.search, { + replace: true, + state + }); + } + }, [location, navigate]); + + + - 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!', - }); - } - }; + setConfirmLoading(true); - const handleFinish = async () => { - setConfirmLoading(true); - try { - // Get current solution data from forms - const currentSolutionData = getSolutionData(); + const brandValues = brandForm.getFieldsValue(); - 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) => { - // If editing current error code, get latest data from forms - if (ec.key === editingErrorCodeKey) { - return { - error_code: ec.error_code, - error_code_name: ec.error_code_name || '', - error_code_description: ec.error_code_description || '', - error_code_color: ec.error_code_color || '#000000', - path_icon: ec.errorCodeIcon?.uploadPath || ec.path_icon || '', - is_active: ec.status !== undefined ? ec.status : true, - solution: currentSolutionData.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, - })), - }; - } + if (!brandValues.brand_name || brandValues.brand_name.trim() === '') { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Brand Name wajib diisi!', + }); + return; + } - // Return existing data for other error codes - return { - error_code: ec.error_code, - error_code_name: ec.error_code_name || '', - error_code_description: ec.error_code_description || '', - error_code_color: ec.error_code_color || '#000000', - path_icon: ec.errorCodeIcon?.uploadPath || ec.path_icon || '', - is_active: ec.status !== undefined ? ec.status : true, - solution: (ec.solution || []).map((sol) => ({ - solution_name: sol.solution_name, - type_solution: sol.type_solution, - text_solution: sol.text_solution || '', - path_solution: sol.path_solution || '', - is_active: sol.is_active !== false, - })), - }; - }), + if (!brandValues.brand_manufacture || brandValues.brand_manufacture.trim() === '') { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Manufacturer wajib diisi!', + }); + return; + } + + const brandApiData = { + brand_name: brandValues.brand_name.trim(), + brand_type: brandValues.brand_type || '', + brand_manufacture: brandValues.brand_manufacture.trim(), + brand_model: brandValues.brand_model || '', + is_active: brandValues.is_active !== undefined ? brandValues.is_active : true }; - const sparepartData = getSparepartData(); - const updatedFinalFormData = { - ...finalFormData, - spareparts: sparepartData, - }; - - const response = await updateBrand(id, updatedFinalFormData); + const response = await updateBrand(id, brandApiData); 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.', + message: 'Brand device berhasil diupdate.', }); - navigate('/master/brand-device'); + + const currentBrandId = id; + if (currentBrandId) { + navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`); + } } else { NotifAlert({ icon: 'error', title: 'Gagal', - message: response?.message || 'Gagal mengupdate Brand Device', + message: response?.message || 'Gagal mengupdate brand device', }); } } catch (error) { NotifAlert({ icon: 'error', title: 'Gagal', - message: error.message || 'Gagal mengupdate data. Silakan coba lagi.', + message: error.message || 'Gagal mengupdate brand device', }); } finally { setConfirmLoading(false); } }; - const handlePreviewErrorCode = (record) => { - errorCodeForm.setFieldsValue({ - error_code: record.error_code, - error_code_name: record.error_code_name, - error_code_description: record.error_code_description, - error_code_color: record.error_code_color, - status: record.status, - }); - setErrorCodeIcon(record.errorCodeIcon || null); - setIsErrorCodeFormReadOnly(true); - setEditingErrorCodeKey(record.key); - - // Load solutions to solution form - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); - } else { - resetSolutionFields(); - } - - // Load spareparts to sparepart form - if (record.sparepart && record.sparepart.length > 0) { - setSparepartsForExistingRecord(record.sparepart); - } else { - resetSparepartFields(); - } + const handleCancel = () => { + navigate('/master/brand-device'); }; - const handleEditErrorCode = (record) => { - errorCodeForm.setFieldsValue({ - error_code: record.error_code, - error_code_name: record.error_code_name, - error_code_description: record.error_code_description, - error_code_color: record.error_code_color, - status: record.status, - }); - setErrorCodeIcon(record.errorCodeIcon || null); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(record.key); - // Load solutions to solution form - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); - } - - // Load spareparts to sparepart form - if (record.sparepart && record.sparepart.length > 0) { - setSparepartsForExistingRecord(record.sparepart); - } - - const formElement = document.querySelector('.ant-form'); - if (formElement) { - formElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }; - - const handleAddErrorCode = async () => { - try { - // Validate error code form - const errorCodeValues = await errorCodeForm.validateFields(); - - // Get solution data from solution form - const solutionData = getSolutionData(); - - if (solutionData.length === 0) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap error code harus memiliki minimal 1 solution!', - }); - return; - } - - // Get sparepart data from sparepart form - const sparepartData = getSparepartData(); - - // Create complete error code object - const newErrorCode = { - error_code: errorCodeValues.error_code, - error_code_name: errorCodeValues.error_code_name, - error_code_description: errorCodeValues.error_code_description, - error_code_color: errorCodeValues.error_code_color || '#000000', - path_icon: errorCodeIcon?.uploadPath || '', - status: errorCodeValues.status === undefined ? true : errorCodeValues.status, - solution: solutionData, - ...(sparepartData && sparepartData.length > 0 && { sparepart: sparepartData }), - errorCodeIcon: errorCodeIcon, - key: editingErrorCodeKey || `temp-${Date.now()}`, - }; - - let updatedErrorCodes; - if (editingErrorCodeKey) { - // Update existing error code - updatedErrorCodes = errorCodes.map((item) => { - if (item.key === editingErrorCodeKey) { - return { - ...item, - ...newErrorCode, - error_code_id: item.error_code_id || newErrorCode.error_code_id, - }; - } - return item; - }); - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: 'Error code berhasil diupdate!', - }); - } else { - // Add new error code - updatedErrorCodes = [...errorCodes, newErrorCode]; - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: 'Error code berhasil ditambahkan!', - }); - } - - setErrorCodes(updatedErrorCodes); - - // Delay form reset to prevent data loss - setTimeout(() => { - resetErrorCodeForm(); - }, 100); - } catch (error) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!', - }); - } - }; - - const resetErrorCodeForm = () => { - errorCodeForm.resetFields(); - errorCodeForm.setFieldsValue({ - status: true, - solution_status_0: true, - solution_type_0: 'text', - }); - setFileList([]); - setErrorCodeIcon(null); - resetSolutionFields(); - resetSparepartFields(); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(null); - }; - - const handleDeleteErrorCode = async (key) => { - if (errorCodes.length <= 1) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap brand harus memiliki minimal 1 error code!', - }); - return; - } - - const updatedErrorCodes = errorCodes.filter((item) => item.key !== key); - setErrorCodes(updatedErrorCodes); - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: 'Error code berhasil dihapus!', - }); - }; - - const handleCreateNewErrorCode = () => { - resetErrorCodeForm(); - resetSolutionFields(); - resetSparepartFields(); - setErrorCodeIcon(null); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(null); - }; const handleErrorCodeIconUpload = (iconData) => { setErrorCodeIcon(iconData); @@ -506,305 +451,564 @@ const EditBrandDevice = () => { setErrorCodeIcon(null); }; - 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 resetErrorCodeForm = () => { + errorCodeForm.resetFields(); + errorCodeForm.setFieldsValue({ + status: true, + }); + setErrorCodeIcon(null); + resetSolutionFields(); + setIsErrorCodeFormReadOnly(false); + setEditingErrorCodeKey(null); + setSelectedSparepartIds([]); + setSelectedErrorCode(null); }; - const handleSolutionFileUpload = (file) => { - setFileList((prevList) => [...prevList, file]); + const handleSaveErrorCode = async () => { + try { + setConfirmLoading(true); + const errorCodeValues = await errorCodeForm.validateFields(); + const solutionData = getSolutionData(); + + if (!errorCodeValues.error_code || !errorCodeValues.error_code_name) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Error code dan error name wajib diisi!', + }); + return; + } + + if (!solutionData || solutionData.length === 0) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Setiap error code harus memiliki minimal 1 solution!', + }); + return; + } + + const formattedSolutions = solutionData.map(solution => { + const solutionType = solution.type || 'text'; + + let typeSolution = solutionType === 'text' ? 'text' : 'image'; + if (solution.file && solution.file.type_solution) { + typeSolution = solution.file.type_solution; + } else if (solution.fileUpload && solution.fileUpload.type_solution) { + typeSolution = solution.fileUpload.type_solution; + } + + const formattedSolution = { + solution_name: solution.name, + type_solution: typeSolution, + is_active: solution.status !== false, + }; + + if (typeSolution === 'text') { + formattedSolution.text_solution = solution.text || ''; + formattedSolution.path_solution = ''; + } else { + formattedSolution.text_solution = ''; + + formattedSolution.path_solution = solution.path_solution || solution.file?.uploadPath || solution.fileUpload?.uploadPath || ''; + } + + if (formattedSolution.brand_code_solution_id) { + delete formattedSolution.brand_code_solution_id; + } + + return formattedSolution; + }); + + const payload = { + error_code: errorCodeValues.error_code, + error_code_name: errorCodeValues.error_code_name, + error_code_description: errorCodeValues.error_code_description || '', + error_code_color: errorCodeValues.error_code_color || '#000000', + path_icon: errorCodeIcon?.uploadPath || '', + is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status, + solution: formattedSolutions, + spareparts: selectedSparepartIds || [] + }; + + let response; + + if (editingErrorCodeKey && editingErrorCodeKey.startsWith('existing_')) { + const errorCodeId = editingErrorCodeKey.replace('existing_', ''); + response = await updateErrorCodeAPI(id, errorCodeId, payload); + } else { + response = await createErrorCodeAPI(id, payload); + } + + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: editingErrorCodeKey ? 'Error code berhasil diupdate!' : 'Error code berhasil ditambahkan!', + }); + + setTrigerFilter(prev => !prev); + resetErrorCodeForm(); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Gagal menyimpan error code', + }); + } + } catch (error) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: error.message || 'Harap isi semua kolom wajib!', + }); + } finally { + setConfirmLoading(false); + } }; - const handleFileRemove = (file) => { - const newFileList = fileList.filter((item) => item.uid !== file.uid); - setFileList(newFileList); + const loadErrorCodeData = (errorCode, isPreview = false) => { + if (errorCode) { + + const formValues = { + error_code: errorCode.error_code, + error_code_name: errorCode.error_code_name, + error_code_description: errorCode.error_code_description || '', + error_code_color: errorCode.error_code_color && errorCode.error_code_color !== '' ? errorCode.error_code_color : '#000000', + status: errorCode.is_active, + }; + + errorCodeForm.setFieldsValue(formValues); + + if (errorCode.path_icon && errorCode.path_icon !== '') { + const iconData = { + name: errorCode.path_icon.split('/').pop(), + uploadPath: errorCode.path_icon, + }; + setErrorCodeIcon(iconData); + } else { + setErrorCodeIcon(null); + } + + setIsErrorCodeFormReadOnly(isPreview); + + const editingKey = errorCode.tempId || `existing_${errorCode.error_code_id}`; + setEditingErrorCodeKey(editingKey); + + } + }; + + const handleSearch = () => { + setTrigerFilter((prev) => !prev); + }; + + const handleSearchClear = () => { + setSearchText(''); + setTrigerFilter((prev) => !prev); }; const renderStepContent = () => { if (currentStep === 0) { return ( - - setFormData((prev) => ({ ...prev, ...allValues })) - } - isEdit={true} - /> +
+ {loading && ( +
+ +
+ )} + +
); } if (currentStep === 1) { + const handleErrorCodeSelect = async (errorCode) => { + + setSelectedErrorCode(errorCode); + + try { + + + + const directResponse = await SendRequest({ + method: 'get', + prefix: `error-code/${errorCode.error_code_id}`, + }); + + + const apiResponse = directResponse.data; + + + if (apiResponse && apiResponse.statusCode === 200 && apiResponse.data) { + const fullErrorCodeData = { + ...apiResponse.data, + tempId: `existing_${apiResponse.data.error_code_id}` + }; + loadErrorCodeData(fullErrorCodeData, false); + + if (apiResponse.data.solution && apiResponse.data.solution.length > 0) { + setCurrentSolutionData(apiResponse.data.solution); + setSolutionsForExistingRecord(apiResponse.data.solution, solutionForm); + } + + if (apiResponse.data.spareparts && apiResponse.data.spareparts.length > 0) { + setSelectedSparepartIds(apiResponse.data.spareparts.map(sp => sp.sparepart_id)); + } else { + setSelectedSparepartIds([]); + } + } else { + const basicErrorCodeData = { + ...errorCode, + tempId: `existing_${errorCode.error_code_id}` + }; + loadErrorCodeData(basicErrorCodeData, false); + resetSolutionFields(); + setSelectedSparepartIds([]); + } + } catch (error) { + const basicErrorCodeData = { + ...errorCode, + tempId: `existing_${errorCode.error_code_id}` + }; + loadErrorCodeData(basicErrorCodeData, false); + } + }; + + const handleAddNew = () => { + setSelectedErrorCode(null); + resetErrorCodeForm(); + }; + return ( - <> - -
+ + + { + setSearchText(value); + if (value === '') { + setTrigerFilter((prev) => !prev); + } + }} + onSearch={handleSearch} + onSearchClear={handleSearchClear} + /> + + + +
- {isErrorCodeFormReadOnly - ? editingErrorCodeKey - ? 'View Error Code' - : 'Error Code Form' - : editingErrorCodeKey - ? 'Edit Error Code' - : 'Error Code'} - +
+ + + Error Code Form + + +
} - size="small" + style={{ + width: '100%', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)', + borderRadius: '12px' + }} + styles={{ + body: { padding: '16px 24px 12px 24px' }, + header: { + padding: '16px 24px', + borderBottom: '1px solid #f0f0f0', + backgroundColor: '#fafafa' + } + }} > -
- - -
- -
- - Solutions - - } - size="small" - > -
- - -
- - - - Spareparts - - } - size="small" - > -
- - -
- - - -
{ - const solutionCount = record.solution - ? record.solution.length - : 0; - return ( - 0 ? 'green' : 'red'} - > - {solutionCount} Sol - - ); - }, - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - width: '20%', - align: 'center', - render: (_, { status }) => ( - - {status ? 'Active' : 'Inactive'} - - ), - }, - { - title: 'Action', - key: 'action', - align: 'center', - width: '15%', - render: (_, record) => ( - - +
+
+
+

+ Solution +

+
+ { + }} + onFileView={(fileData) => { + if (fileData && (fileData.url || fileData.uploadPath)) { + window.open(fileData.url || fileData.uploadPath, '_blank'); } + }} + isReadOnly={false} + solutionData={currentSolutionData} + /> +
+ + +
+
+
+

+ Sparepart Selection +

+
+
+ -
+
+ + + +
+ + {editingErrorCodeKey && ( + + )} +
+ - - - + + + ); } return null; }; return ( - - - Edit Brand Device - - - - - -
- {loading && ( -
- + + + + + + + {renderStepContent()} + +
+
+ {currentStep === 1 && ( + + )} +
+
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )}
- )} -
- {renderStepContent()}
-
- - setCurrentStep(currentStep - 1)} - onNextStep={handleNextStep} - onSave={handleFinish} - onCancel={handleCancel} - confirmLoading={confirmLoading} - isEditMode={true} - /> -
+ +
); }; -export default EditBrandDevice; +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 index 9e5096d..997af93 100644 --- a/src/pages/master/brandDevice/ViewBrandDevice.jsx +++ b/src/pages/master/brandDevice/ViewBrandDevice.jsx @@ -1,415 +1,765 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; -import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Steps, Collapse, Switch, Spin, Modal, Empty } from 'antd'; -import { ArrowLeftOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons'; +import { + Divider, + Typography, + Button, + Steps, + Form, + Row, + Col, + Card, + Spin, + Tag, + Space, + ConfigProvider, + Empty +} from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; -import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif'; -import { getBrandById } from '../../../api/master-brand'; +import { NotifAlert } from '../../../components/Global/ToastNotif'; +import { getBrandById, getErrorCodesByBrandId } from '../../../api/master-brand'; +import { getFileUrl, getFolderFromFileType } from '../../../api/file-uploads'; +import { SendRequest } from '../../../components/Global/ApiRequest'; +import ListErrorCode from './component/ListErrorCode'; +import BrandForm from './component/BrandForm'; +import ErrorCodeForm from './component/ErrorCodeForm'; +import SolutionForm from './component/SolutionForm'; +import SparepartSelect from './component/SparepartSelect'; 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 [brandForm] = Form.useForm(); + const [errorCodeForm] = Form.useForm(); + const [solutionForm] = Form.useForm(); + const [brandData, setBrandData] = useState(null); + const [errorCodes, setErrorCodes] = useState([]); const [loading, setLoading] = useState(true); const [currentStep, setCurrentStep] = useState(0); - const [activeErrorKeys, setActiveErrorKeys] = useState([]); + const [selectedErrorCode, setSelectedErrorCode] = useState(null); + const [selectedSparepartIds, setSelectedSparepartIds] = useState([]); + const [errorCodeIcon, setErrorCodeIcon] = useState(null); + const [trigerFilter, setTrigerFilter] = useState(false); + const [searchText, setSearchText] = useState(''); + + + const [solutionFields, setSolutionFields] = useState([0]); + const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' }); + const [solutionStatuses, setSolutionStatuses] = useState({ 0: true }); + const [currentSolutionData, setCurrentSolutionData] = useState([]); + + + const [brandInfo, setBrandInfo] = useState({}); + + const resetSolutionFields = () => { + if (solutionForm && solutionForm.resetFields) { + solutionForm.resetFields(); + solutionForm.setFieldsValue({ + solution_items: { + 0: { + name: '', + type: 'text', + text: '', + status: true + } + } + }); + } + setCurrentSolutionData([]); + }; + + const getSolutionData = () => { + if (!solutionForm) return []; + try { + const values = solutionForm.getFieldsValue(true); + + let solutions = []; + + if (values.solution_items) { + if (Array.isArray(values.solution_items)) { + solutions = values.solution_items.filter(Boolean); + } else if (typeof values.solution_items === 'object') { + solutions = Object.values(values.solution_items).filter(Boolean); + } + } + + return solutions; + } catch (error) { + return []; + } + }; + + + useEffect(() => { + const savedPhase = location.state?.phase || localStorage.getItem(`brand_device_${id}_last_phase`); + if (savedPhase) { + setCurrentStep(parseInt(savedPhase)); + localStorage.removeItem(`brand_device_${id}_last_phase`); + } + }, [location.state, id]); + + + useEffect(() => { + setBreadcrumbItems([ + { + title: • Master + }, + { + title: ( + navigate('/master/brand-device')} + > + Brand Device + + ), + }, + { + title: ( + + View Brand Device + + ), + }, + ]); + }, [setBreadcrumbItems, navigate]); 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`); - } + if (!token) { + navigate('/signin'); + return; + } - setBreadcrumbItems([ - { title: • Master }, - { - title: navigate('/master/brand-device')}>Brand Device - }, - { title: View Brand Device } - ]); + try { + setLoading(true); + const response = await getBrandById(id); - try { - setLoading(true); - const response = await getBrandById(id); + if (response && response.statusCode === 200) { + const brandData = response.data; - if (response && response.statusCode === 200) { - setBrandData(response.data); - } else { - NotifAlert({ - icon: 'error', - title: 'Error', - message: response?.message || 'Failed to fetch brand device data', - }); + const brandInfoData = { + brand_code: brandData.brand_code, + brand_name: brandData.brand_name, + brand_type: brandData.brand_type || '', + brand_manufacture: brandData.brand_manufacture || '', + brand_model: brandData.brand_model || '', + is_active: brandData.is_active + }; + + setBrandInfo(brandInfoData); + setBrandData(brandData); + brandForm.setFieldsValue(brandInfoData); + + if (brandData.brand_id) { + try { + const errorCodesResponse = await getErrorCodesByBrandId(id || brandData.brand_id); + if (errorCodesResponse && errorCodesResponse.statusCode === 200) { + const apiErrorData = errorCodesResponse.data || []; + const existingCodes = apiErrorData.map(ec => ({ + ...ec, + tempId: `existing_${ec.error_code_id}`, + status: 'existing', + solution: ec.solution || [], + spareparts: ec.spareparts || [] + })); + setErrorCodes(existingCodes); + } + } catch (error) { + } } - } catch (error) { - console.error('Fetch Brand Device Error:', error); + } else { NotifAlert({ icon: 'error', title: 'Error', - message: error.message || 'Failed to fetch brand device data', + message: response?.message || 'Failed to fetch brand device data', }); - } finally { - setLoading(false); } - } else { - navigate('/signin'); + } catch (error) { + NotifAlert({ + icon: 'error', + title: 'Error', + message: error.message || 'Failed to fetch brand device data', + }); + } finally { + setLoading(false); } }; fetchBrandData(); - }, [id, setBreadcrumbItems, navigate, location.state]); + }, [id, navigate, brandForm]); - const handleFileView = (fileName, fileType) => { - localStorage.setItem(`brand_device_${id}_last_phase`, currentStep.toString()); + useEffect(() => { + if (currentStep === 1 && id) { + setTrigerFilter(prev => !prev); + } + }, [currentStep, id]); - let actualFileName = fileName; - if (fileName && fileName.includes('/')) { - const parts = fileName.split('/'); - actualFileName = parts[parts.length - 1]; + + useEffect(() => { + if (currentStep === 1 && errorCodes.length > 0 && !selectedErrorCode) { + handleErrorCodeSelect(errorCodes[0]); + } + }, [currentStep, errorCodes]); + + const setSolutionsForExistingRecord = (solutions, targetForm) => { + + if (!targetForm || !solutions || solutions.length === 0) { + return; } - const encodedFileName = encodeURIComponent(actualFileName); - const fileTypeParam = fileType === 'image' ? 'image' : 'pdf'; - const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`; - navigate(navigationPath); + targetForm.resetFields(); + + const solutionItems = {}; + const newSolutionFields = []; + const newSolutionTypes = {}; + const newSolutionStatuses = {}; + + solutions.forEach((solution, index) => { + const fieldKey = index; + newSolutionFields.push(fieldKey); + + const isFileType = solution.type_solution && solution.type_solution !== 'text'; + newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text'; + newSolutionStatuses[fieldKey] = solution.is_active; + + let fileObject = null; + if (isFileType && (solution.path_solution || solution.path_document)) { + fileObject = { + uploadPath: solution.path_solution || solution.path_document, + path_solution: solution.path_solution || solution.path_document, + name: solution.file_upload_name || (solution.path_solution || solution.path_document).split('/').pop() || 'File', + type_solution: solution.type_solution, + isExisting: true, + size: 0, + url: solution.path_solution || solution.path_document + }; + } + + solutionItems[fieldKey] = { + brand_code_solution_id: solution.brand_code_solution_id, + name: solution.solution_name || '', + type: isFileType ? 'file' : 'text', + text: solution.text_solution || '', + status: solution.is_active, + file: fileObject, + fileUpload: fileObject, + path_solution: solution.path_solution || solution.path_document || null, + fileName: solution.file_upload_name || null + }; + }); + + setSolutionFields(newSolutionFields); + setSolutionTypes(newSolutionTypes); + setSolutionStatuses(newSolutionStatuses); + + targetForm.resetFields(); + + setTimeout(() => { + targetForm.setFieldsValue({ + solution_items: solutionItems + }); + + setTimeout(() => { + Object.keys(solutionItems).forEach(key => { + const solution = solutionItems[key]; + targetForm.setFieldValue(['solution_items', key, 'name'], solution.name); + targetForm.setFieldValue(['solution_items', key, 'type'], solution.type); + targetForm.setFieldValue(['solution_items', key, 'text'], solution.text); + targetForm.setFieldValue(['solution_items', key, 'file'], solution.file); + targetForm.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload); + targetForm.setFieldValue(['solution_items', key, 'status'], solution.status); + targetForm.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution); + targetForm.setFieldValue(['solution_items', key, 'fileName'], solution.fileName); + }); + + const finalValues = targetForm.getFieldsValue(); + }, 100); + }, 100); + }; + + const handleErrorCodeSelect = async (errorCode) => { + + setSelectedErrorCode(errorCode); + + try { + + const directResponse = await SendRequest({ + method: 'get', + prefix: `error-code/${errorCode.error_code_id}`, + }); + + const apiResponse = directResponse.data; + + if (apiResponse && apiResponse.statusCode === 200 && apiResponse.data) { + const fullErrorCodeData = { + ...apiResponse.data, + tempId: `existing_${apiResponse.data.error_code_id}` + }; + + const formValues = { + error_code: fullErrorCodeData.error_code, + error_code_name: fullErrorCodeData.error_code_name, + error_code_description: fullErrorCodeData.error_code_description || '', + error_code_color: fullErrorCodeData.error_code_color && fullErrorCodeData.error_code_color !== '' ? fullErrorCodeData.error_code_color : '#000000', + status: fullErrorCodeData.is_active, + }; + + errorCodeForm.setFieldsValue(formValues); + + if (fullErrorCodeData.path_icon && fullErrorCodeData.path_icon !== '') { + const iconData = { + name: fullErrorCodeData.path_icon.split('/').pop(), + uploadPath: fullErrorCodeData.path_icon, + }; + setErrorCodeIcon(iconData); + } else { + setErrorCodeIcon(null); + } + + if (apiResponse.data.solution && apiResponse.data.solution.length > 0) { + setCurrentSolutionData(apiResponse.data.solution); + setSolutionsForExistingRecord(apiResponse.data.solution, solutionForm); + } + + if (apiResponse.data.spareparts && apiResponse.data.spareparts.length > 0) { + setSelectedSparepartIds(apiResponse.data.spareparts.map(sp => sp.sparepart_id)); + } else { + setSelectedSparepartIds([]); + } + } else { + const basicErrorCodeData = { + ...errorCode, + tempId: `existing_${errorCode.error_code_id}` + }; + + const formValues = { + error_code: basicErrorCodeData.error_code, + error_code_name: basicErrorCodeData.error_code_name, + error_code_description: basicErrorCodeData.error_code_description || '', + error_code_color: basicErrorCodeData.error_code_color && basicErrorCodeData.error_code_color !== '' ? basicErrorCodeData.error_code_color : '#000000', + status: basicErrorCodeData.is_active, + }; + + errorCodeForm.setFieldsValue(formValues); + + if (basicErrorCodeData.path_icon && basicErrorCodeData.path_icon !== '') { + const iconData = { + name: basicErrorCodeData.path_icon.split('/').pop(), + uploadPath: basicErrorCodeData.path_icon, + }; + setErrorCodeIcon(iconData); + } else { + setErrorCodeIcon(null); + } + + resetSolutionFields(); + setSelectedSparepartIds([]); + } + } catch (error) { + const basicErrorCodeData = { + ...errorCode, + tempId: `existing_${errorCode.error_code_id}` + }; + + const formValues = { + error_code: basicErrorCodeData.error_code, + error_code_name: basicErrorCodeData.error_code_name, + error_code_description: basicErrorCodeData.error_code_description || '', + error_code_color: basicErrorCodeData.error_code_color && basicErrorCodeData.error_code_color !== '' ? basicErrorCodeData.error_code_color : '#000000', + status: basicErrorCodeData.is_active, + }; + + errorCodeForm.setFieldsValue(formValues); + + if (basicErrorCodeData.path_icon && basicErrorCodeData.path_icon !== '') { + const iconData = { + name: basicErrorCodeData.path_icon.split('/').pop(), + uploadPath: basicErrorCodeData.path_icon, + }; + setErrorCodeIcon(iconData); + } else { + setErrorCodeIcon(null); + } + + resetSolutionFields(); + setSelectedSparepartIds([]); + } + }; + + const handleBrandFormValuesChange = useCallback((changedValues, allValues) => { + setBrandInfo(allValues); + }, [setBrandInfo]); + + const handleSearch = () => { + setTrigerFilter((prev) => !prev); + }; + + const handleSearchClear = () => { + setSearchText(''); + setTrigerFilter((prev) => !prev); + }; + + const handleFileView = (fileName) => { + try { + let fileUrl = ''; + let actualFileName = ''; + + const filePath = fileName || ''; + if (filePath) { + actualFileName = filePath.split('/').pop(); + } + + if (actualFileName) { + const fileExtension = actualFileName.split('.').pop()?.toLowerCase(); + const folder = getFolderFromFileType(fileExtension); + fileUrl = getFileUrl(folder, actualFileName); + } + + if (!fileUrl && filePath) { + fileUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`; + } + + if (fileUrl && actualFileName) { + const fileExtension = actualFileName.split('.').pop()?.toLowerCase(); + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + const pdfExtensions = ['pdf']; + + if (imageExtensions.includes(fileExtension) || pdfExtensions.includes(fileExtension)) { + const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`; + window.open(viewerUrl, '_blank', 'noopener,noreferrer'); + } else { + window.open(fileUrl, '_blank', 'noopener,noreferrer'); + } + } else { + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'File URL not found' + }); + } + } catch (error) { + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Failed to open file preview' + }); + } + }; + + const handleNextStep = () => { + setCurrentStep(1); }; const renderStepContent = () => { if (currentStep === 0) { return ( -
-
- Status -
-
-
-
+ {loading && ( +
+ top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(255, 255, 255, 0.7)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + borderRadius: '8px', + }} + > +
- {(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 errorCodes = brandData?.error_code || []; - return ( -
- - Error Codes ({errorCodes.length}) - - - {errorCodes.length > 0 ? ( - - {errorCodes.map((errorCode, index) => ( - -
- {errorCode.error_code} - - - {errorCode.error_code_name} - -
-
- - {errorCode.is_active ? 'Active' : 'Inactive'} - - - {errorCode.solution?.length || 0} solution(s) - -
-
- } - > -
-
- Description: -
- {errorCode.error_code_description || 'No description'} -
-
- -
- Solutions: - {errorCode.solution && errorCode.solution.length > 0 ? ( -
- {errorCode.solution.map((solution) => ( - - -
- - {solution.type_solution === 'pdf' ? ( - - ) : solution.type_solution === 'image' ? ( - - ) : ( - - )} - {solution.solution_name} - - - - - {solution.type_solution ? solution.type_solution.toUpperCase() : 'TEXT'} - - - - -
- {solution.type_solution === 'text' ? ( - - {solution.text_solution} - - ) : ( -
- - File: {solution.path_document || solution.path_solution || 'Document'} - - {(solution.path_document || solution.path_solution) && ( - - )} -
- )} -
- - ))} - - ) : ( -
- No solutions available -
- )} - - - - ))} - - ) : ( - !loading && ( - No error codes available + + + { + setSearchText(value); + if (value === '') { + setTrigerFilter((prev) => !prev); } - /> - ) - )} - + }} + onSearch={handleSearch} + onSearchClear={handleSearchClear} + isReadOnly={true} + /> + + + +
+ {selectedErrorCode ? ( + + + Error Code Form + + } + style={{ + width: '100%', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)', + borderRadius: '12px' + }} + styles={{ + body: { padding: '16px 24px 12px 24px' }, + header: { + padding: '16px 24px', + borderBottom: '1px solid #f0f0f0', + backgroundColor: '#fafafa' + } + }} + > +
+
+
+
+

+ Error Code Details +

+
+ { }} + onErrorCodeIconRemove={() => { }} + isEdit={true} + /> +
+ + +
+
+
+
+

+ Solution +

+
+ { }} + onRemoveSolutionField={() => { }} + onSolutionTypeChange={() => { }} + onSolutionStatusChange={() => { }} + onSolutionFileUpload={() => { }} + onFileView={(fileData) => { + if (fileData && (fileData.url || fileData.uploadPath)) { + window.open(fileData.url || fileData.uploadPath, '_blank'); + } + }} + isReadOnly={true} + solutionData={currentSolutionData} + /> +
+ + +
+
+
+

+ Sparepart Selection +

+
+
+ { }} + isReadOnly={true} + /> +
+
+ + + + + ) : ( +
+ +
+ )} + + + ); } - return null; }; return ( - + - - - View Brand Device - - - - - - - - - -
- {loading && ( -
- -
- )} - -
- {renderStepContent()} + {renderStepContent()} + +
+
+ {currentStep === 1 && ( + + )} +
+
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )}
- - -
- {currentStep > 0 && ( - - )} - {currentStep < 1 && ( - - )} -
- + ); }; diff --git a/src/pages/master/brandDevice/ViewFilePage.jsx b/src/pages/master/brandDevice/ViewFilePage.jsx index 4476450..b46b56b 100644 --- a/src/pages/master/brandDevice/ViewFilePage.jsx +++ b/src/pages/master/brandDevice/ViewFilePage.jsx @@ -6,10 +6,10 @@ import { ArrowLeftOutlined, FilePdfOutlined, FileImageOutlined, DownloadOutlined import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { getBrandById } from '../../../api/master-brand'; import { - downloadFile, - getFile, - getFileUrl, - getFolderFromFileType, + downloadFile, + getFile, + getFileUrl, + getFolderFromFileType, } from '../../../api/file-uploads'; const { Title } = Typography; @@ -26,17 +26,7 @@ const ViewFilePage = () => { 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; @@ -45,7 +35,6 @@ const ViewFilePage = () => { 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'); @@ -55,13 +44,6 @@ const ViewFilePage = () => { 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' - }); } } @@ -95,12 +77,9 @@ const ViewFilePage = () => { const folder = getFolderFromFileType('pdf'); try { const blobData = await getFile(folder, decodedFileName); - console.log('PDF blob data received:', blobData); const blobUrl = window.URL.createObjectURL(blobData); setPdfBlobUrl(blobUrl); - console.log('PDF blob URL created successfully:', blobUrl); } catch (pdfError) { - console.error('Error loading PDF:', pdfError); setError('Failed to load PDF file: ' + (pdfError.message || pdfError)); setPdfBlobUrl(null); } finally { @@ -110,7 +89,6 @@ const ViewFilePage = () => { setLoading(false); } catch (error) { - console.error('Error fetching data:', error); setError('Failed to load data'); setLoading(false); } @@ -160,12 +138,6 @@ const ViewFilePage = () => { 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 @@ -196,9 +168,7 @@ const ViewFilePage = () => { 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 (
@@ -340,17 +310,14 @@ const ViewFilePage = () => {
- {/* File type indicator */} +
{
- {/* Overlay with blur effect during loading */} + {loading && (
{ - const isActive = Form.useWatch('is_active', form) ?? formData.is_active ?? true; +const BrandForm = ({ + form, + onValuesChange, + isEdit = false, + brandInfo = null, + readOnly = false, +}) => { + const isActive = Form.useWatch('is_active', form) ?? true; + + React.useEffect(() => { + if (brandInfo && brandInfo.brand_code) { + form.setFieldsValue({ + brand_code: brandInfo.brand_code + }); + } + }, [brandInfo, form]); return ( -
- -
- - - - - {isActive ? 'Running' : 'Offline'} - -
-
+
+ + +
+ + + + + {isActive ? 'Running' : 'Offline'} + +
+
- - - + + + - -
- - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/src/pages/master/brandDevice/component/CustomSparepartCard.jsx b/src/pages/master/brandDevice/component/CustomSparepartCard.jsx new file mode 100644 index 0000000..60fd55e --- /dev/null +++ b/src/pages/master/brandDevice/component/CustomSparepartCard.jsx @@ -0,0 +1,397 @@ +import React, { useState } from 'react'; +import { Card, Typography, Tag, Button, Modal, Row, Col, Space } from 'antd'; +import { EyeOutlined, DeleteOutlined, CheckOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; + +const { Text, Title } = Typography; + +const CustomSparepartCard = ({ + sparepart, + isSelected = false, + isReadOnly = false, + showPreview = true, + showDelete = false, + onPreview, + onDelete, + onCardClick, + loading = false, + size = 'small', + style = {}, +}) => { + const [previewModalVisible, setPreviewModalVisible] = useState(false); + + const getImageSrc = () => { + if (sparepart.sparepart_foto) { + if (sparepart.sparepart_foto.startsWith('http')) { + return sparepart.sparepart_foto; + } else { + const fileName = sparepart.sparepart_foto.split('/').pop(); + if (fileName === 'defaultSparepartImg.jpg') { + return `/assets/defaultSparepartImg.jpg`; + } else { + const token = localStorage.getItem('token'); + const baseURL = import.meta.env.VITE_API_SERVER || ''; + return `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`; + } + } + } + return 'https://via.placeholder.com/150'; + }; + + const handlePreview = () => { + if (onPreview) { + onPreview(sparepart); + } else { + setPreviewModalVisible(true); + } + }; + + const truncateText = (text, maxLength = 15) => { + if (!text) return 'Unnamed'; + return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; + }; + + const handleCardClick = () => { + if (!isReadOnly && onCardClick) { + onCardClick(sparepart); + } + }; + + const getCardActions = () => { + const actions = []; + + if (showPreview) { + actions.push( + + ]} + width={800} + centered + styles={{ body: { padding: '24px' } }} + > + + +
+
+ {sparepart.sparepart_name { + e.target.src = 'https://via.placeholder.com/220x220/d9d9d9/666666?text=No+Image'; + }} + /> +
+ + + {sparepart.sparepart_item_type && ( +
+ + {sparepart.sparepart_item_type} + +
+ )} + + +
+
+ Stock Status: + + {sparepart.sparepart_stok || 'Not Available'} + +
+
+ Quantity: + + {sparepart.sparepart_qty || 0} {sparepart.sparepart_unit || ''} + +
+
+
+ + + +
+ + + {sparepart.sparepart_name || 'Unnamed'} + + + +
+ +
+
+ +
+
+ Code +
+ {sparepart.sparepart_code || 'N/A'} +
+
+ + +
+ Brand +
+ {sparepart.sparepart_merk || '-'} +
+
+ + +
+ Unit +
+ {sparepart.sparepart_unit || '-'} +
+
+ + + + + + {sparepart.sparepart_model && ( + +
+ Model +
+ {sparepart.sparepart_model} +
+
+ + )} + + {sparepart.sparepart_description && ( + +
+ Description +
+ {sparepart.sparepart_description} +
+
+ + )} + + + + + {sparepart.created_at && ( +
+ +
+
+ Created +
+ {dayjs(sparepart.created_at).format('DD MMM YYYY, HH:mm')} +
+
+ + +
+ Last Updated +
+ {dayjs(sparepart.updated_at).format('DD MMM YYYY, HH:mm')} +
+
+ + + + )} + + + + + + ); +}; + +export default CustomSparepartCard; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/ErrorCodeForm.jsx b/src/pages/master/brandDevice/component/ErrorCodeForm.jsx new file mode 100644 index 0000000..3ce9d6e --- /dev/null +++ b/src/pages/master/brandDevice/component/ErrorCodeForm.jsx @@ -0,0 +1,288 @@ +import React, { useState, useEffect } from 'react'; +import { Form, Input, Switch, Typography, ConfigProvider, Card, Button } from 'antd'; +import { FileOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons'; +import FileUploadHandler from './FileUploadHandler'; +import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; +import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads'; + +const { Text } = Typography; + +const ErrorCodeForm = ({ + errorCodeForm, + isErrorCodeFormReadOnly = false, + errorCodeIcon, + onErrorCodeIconUpload, + onErrorCodeIconRemove, + isEdit = false, +}) => { + const [currentIcon, setCurrentIcon] = useState(null); + const statusWatch = Form.useWatch('status', errorCodeForm) ?? true; + + useEffect(() => { + if (errorCodeIcon && typeof errorCodeIcon === 'object' && Object.keys(errorCodeIcon).length > 0) { + setCurrentIcon(errorCodeIcon); + } else { + setCurrentIcon(null); + } + }, [errorCodeIcon]); + + const handleIconRemove = () => { + setCurrentIcon(null); + onErrorCodeIconRemove(); + }; + + const renderIconUpload = () => { + if (currentIcon) { + const displayFileName = currentIcon.name || currentIcon.uploadPath?.split('/').pop() || currentIcon.url?.split('/').pop() || 'Icon File'; + + return ( + +
+
+ +
+ +
+
+ {displayFileName} +
+
+ {currentIcon.size ? `${(currentIcon.size / 1024).toFixed(1)} KB` : 'Icon uploaded'} +
+
+ +
+
+
+
+ ); + } else { + return ( + { + setCurrentIcon(fileData); + onErrorCodeIconUpload(fileData); + }} + onFileRemove={handleIconRemove} + buttonText="Upload Icon" + buttonStyle={{ + width: '100%', + borderColor: '#23A55A', + color: '#23A55A', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '8px' + }} + uploadText="Upload error code icon" + disabled={isErrorCodeFormReadOnly} + /> + ); + } + }; + + return ( + +
+ {/* Header bar with color picker, icon upload, and status toggle */} +
+ {/* Color picker on left */} +
+ e.target.value} + getValueProps={(value) => ({ value: value || '#000000' })} + > + + + + {/* Icon upload beside color picker */} +
+ {renderIconUpload()} +
+
+ + {/* Status toggle on right */} +
+ + + + + {statusWatch ? 'Active' : 'Inactive'} + +
+
+ + {/* Error Code and Error Name in one row with 1/3 and 2/3 ratio */} +
+ + + + + + + +
+ + + + + +
+ ); +}; + +export default ErrorCodeForm; diff --git a/src/pages/master/brandDevice/component/ErrorCodeListModal.jsx b/src/pages/master/brandDevice/component/ErrorCodeListModal.jsx deleted file mode 100644 index 89a74b4..0000000 --- a/src/pages/master/brandDevice/component/ErrorCodeListModal.jsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useState } from 'react'; -import { Modal, Table, Button, Space, message, Tag, ConfigProvider } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; -import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif'; - -const ErrorCodeListModal = ({ - visible, - onClose, - errorCodes, - loading, - onPreview, - onEdit, - onDelete, - onAddNew, -}) => { - const [confirmLoading, setConfirmLoading] = useState(false); - - const columns = [ - { - title: 'No', - key: 'no', - width: '5%', - align: 'center', - render: (_, __, index) => index + 1, - }, - { - title: 'Error Code', - dataIndex: 'error_code', - key: 'error_code', - width: '15%', - }, - { - title: 'Error Name', - dataIndex: 'error_code_name', - key: 'error_code_name', - width: '30%', - render: (text) => text || '-', - }, - { - title: 'Description', - dataIndex: 'error_code_description', - key: 'error_code_description', - width: '25%', - render: (text) => text || '-', - ellipsis: true, - }, - { - title: 'Solutions', - key: 'solutions', - width: '10%', - align: 'center', - render: (_, record) => { - const solutionCount = record.solution ? record.solution.length : 0; - return 0 ? 'green' : 'red'}>{solutionCount} Sol; - }, - }, - { - title: 'Status', - dataIndex: 'status', - key: 'status', - width: '10%', - align: 'center', - render: (_, { status }) => ( - {status ? 'Active' : 'Inactive'} - ), - }, - { - title: 'Action', - key: 'action', - align: 'center', - width: '15%', - render: (_, record) => ( - - - - - } - open={visible} - onCancel={onClose} - closable={false} - maskClosable={false} - width={1200} - footer={[ - , - ]} - > -
`${range[0]}-${range[1]} of ${total} items`, - }} - scroll={{ x: 1000 }} - size="small" - /> - - ); -}; - -export default ErrorCodeListModal; diff --git a/src/pages/master/brandDevice/component/ErrorCodeSimpleForm.jsx b/src/pages/master/brandDevice/component/ErrorCodeSimpleForm.jsx deleted file mode 100644 index 79a0661..0000000 --- a/src/pages/master/brandDevice/component/ErrorCodeSimpleForm.jsx +++ /dev/null @@ -1,217 +0,0 @@ -import { Form, Input, Switch, Upload, Button, Typography, message, ConfigProvider } from 'antd'; -import { UploadOutlined } from '@ant-design/icons'; -import { uploadFile } from '../../../../api/file-uploads'; - -const { Text } = Typography; - -const ErrorCodeSimpleForm = ({ - errorCodeForm, - isErrorCodeFormReadOnly = false, - errorCodeIcon, - onErrorCodeIconUpload, - onErrorCodeIconRemove, - onAddErrorCode, -}) => { - const statusValue = Form.useWatch('status', errorCodeForm); - - const handleIconUpload = async (file) => { - // Check if file is an image - const isImage = file.type.startsWith('image/'); - if (!isImage) { - message.error('You can only upload image files!'); - return Upload.LIST_IGNORE; - } - - // Check file size (max 2MB) - const isLt2M = file.size / 1024 / 1024 < 2; - if (!isLt2M) { - message.error('Image must be smaller than 2MB!'); - return Upload.LIST_IGNORE; - } - - try { - const fileExtension = file.name.split('.').pop().toLowerCase(); - const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes( - fileExtension - ); - const fileType = isImageFile ? 'image' : 'pdf'; - const folder = 'images'; - - const uploadResponse = await uploadFile(file, folder); - const iconPath = - uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || ''; - - if (iconPath) { - onErrorCodeIconUpload({ - name: file.name, - uploadPath: iconPath, - fileExtension, - isImage: isImageFile, - size: file.size, - }); - message.success(`${file.name} uploaded successfully!`); - } else { - message.error(`Failed to upload ${file.name}`); - } - } catch (error) { - console.error('Error uploading icon:', error); - message.error(`Failed to upload ${file.name}`); - } - }; - - const handleIconRemove = () => { - onErrorCodeIconRemove(); - }; - - return ( - <> - {/* Status Switch */} - -
- - - - {statusValue ? 'Active' : 'Inactive'} -
-
- - {/* Error Code */} - - - - - {/* Error Name */} - - - - - {/* Error Description */} - - - - - {/* Color and Icon in same row */} - - - - - - - - {!isErrorCodeFormReadOnly ? ( - - - - ) : ( -
- No upload allowed -
- )} -
-
- - {errorCodeIcon && ( -
-
- Error Code Icon -
- {errorCodeIcon.name} -
- - Size: {(errorCodeIcon.size / 1024).toFixed(1)} KB - -
- {!isErrorCodeFormReadOnly && ( - - )} -
-
- )} -
- - {/* Add Error Code Button */} - {!isErrorCodeFormReadOnly && ( - - - - - - )} - - ); -}; - -export default ErrorCodeSimpleForm; diff --git a/src/pages/master/brandDevice/component/FileUploadHandler.jsx b/src/pages/master/brandDevice/component/FileUploadHandler.jsx index 8dd1adf..6e5b0e0 100644 --- a/src/pages/master/brandDevice/component/FileUploadHandler.jsx +++ b/src/pages/master/brandDevice/component/FileUploadHandler.jsx @@ -1,18 +1,45 @@ -import { useState } from 'react'; -import { Upload, Modal } from 'antd'; -import { UploadOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import { Upload, Modal, Button, Typography, Space, Image } from 'antd'; +import { UploadOutlined, EyeOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons'; import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif'; -import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads'; +import { uploadFile, getFolderFromFileType, getFileUrl, getFileType } from '../../../../api/file-uploads'; + +const { Text } = Typography; const FileUploadHandler = ({ - solutionFields, - fileList, + type = 'solution', + maxCount = 1, + accept = '.pdf,.jpg,.jpeg,.png,.gif', + disabled = false, + + fileList = [], onFileUpload, - onFileRemove + onFileRemove, + + existingFile = null, + clearSignal = null, + debugProps = {}, + + uploadText = 'Click or drag file to this area to upload', + uploadHint = 'Support for PDF and image files only', + buttonText = 'Upload File', + buttonType = 'default', + + containerStyle = {}, + buttonStyle = {}, + showPreview = true }) => { const [previewOpen, setPreviewOpen] = useState(false); const [previewImage, setPreviewImage] = useState(''); const [previewTitle, setPreviewTitle] = useState(''); + const [isUploading, setIsUploading] = useState(false); + const [uploadedFile, setUploadedFile] = useState(null); + + React.useEffect(() => { + if (clearSignal !== null && clearSignal > 0) { + setUploadedFile(null); + } + }, [clearSignal, debugProps]); const getBase64 = (file) => new Promise((resolve, reject) => { @@ -22,99 +49,372 @@ const FileUploadHandler = ({ 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)); + const handlePreview = async (file) => { + if (!file.url && !file.preview) { + file.preview = await getBase64(file.originFileObj); + } + setPreviewImage(file.url || file.preview); setPreviewOpen(true); + setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1)); }; - const handleFileUpload = async (file) => { - const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type); + const validateFile = (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.` + message: `${file.name} bukan file PDF atau gambar yang diizinkan.`, }); - return Upload.LIST_IGNORE; + return false; + } + + return true; + }; + + const handleFileUpload = async (file) => { + if (isUploading) { + return false; + } + + if (!validateFile(file)) { + return false; } try { + setIsUploading(true); + 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 || ''; + + const isSuccess = uploadResponse && ( + uploadResponse.statusCode === 200 || + uploadResponse.statusCode === 201 + ); + + if (!isSuccess) { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: uploadResponse?.message || `Gagal mengupload ${file.name}`, + }); + setIsUploading(false); + return false; + } + + let actualPath = ''; + if (uploadResponse && typeof uploadResponse === 'object') { + if (uploadResponse.data && uploadResponse.data.path_document) { + actualPath = uploadResponse.data.path_document; + } + else if (uploadResponse.path_document) { + actualPath = uploadResponse.path_document; + } + else if (uploadResponse.data && uploadResponse.data.path_solution) { + actualPath = uploadResponse.data.path_solution; + } + else if (uploadResponse.data && typeof uploadResponse.data === 'object') { + if (uploadResponse.data.file_url) { + actualPath = uploadResponse.data.file_url; + } else if (uploadResponse.data.url) { + actualPath = uploadResponse.data.url; + } else if (uploadResponse.data.path) { + actualPath = uploadResponse.data.path; + } else if (uploadResponse.data.location) { + actualPath = uploadResponse.data.location; + } else if (uploadResponse.data.filePath) { + actualPath = uploadResponse.data.filePath; + } else if (uploadResponse.data.file_path) { + actualPath = uploadResponse.data.file_path; + } else if (uploadResponse.data.publicUrl) { + actualPath = uploadResponse.data.publicUrl; + } else if (uploadResponse.data.public_url) { + actualPath = uploadResponse.data.public_url; + } + } + else if (uploadResponse && typeof uploadResponse === 'string') { + actualPath = uploadResponse; + } + } + if (actualPath) { - file.uploadPath = actualPath; - file.solution_name = file.name; - file.solutionId = solutionFields[0]; - file.type_solution = fileType; - onFileUpload(file); + let fileObject; + + if (type === 'error_code') { + fileObject = { + name: file.name, + path_icon: actualPath, + uploadPath: actualPath, + url: actualPath, + size: file.size, + type: file.type, + fileExtension + }; + } else { + fileObject = { + name: file.name, + path_solution: actualPath, + uploadPath: actualPath, + type_solution: fileType, + size: file.size, + type: file.type + }; + } + + onFileUpload(fileObject); + setUploadedFile(fileObject); + NotifOk({ icon: 'success', title: 'Berhasil', message: `${file.name} berhasil diupload!` }); + + setIsUploading(false); + return false; } else { NotifAlert({ icon: 'error', title: 'Gagal', - message: `Gagal mengupload ${file.name}` + message: `Gagal mengupload ${file.name}. Tidak dapat menemukan path file dalam response.`, }); + setIsUploading(false); + return false; } } catch (error) { - console.error('Error uploading file:', error); NotifAlert({ icon: 'error', title: 'Error', - message: `Gagal mengupload ${file.name}. Silakan coba lagi.` + message: `Gagal mengupload ${file.name}. Silakan coba lagi.`, }); + setIsUploading(false); + return false; + } + }; + + const handleFileChange = ({ fileList }) => { + if (fileList && fileList.length > 0 && fileList[0] && fileList[0].originFileObj) { + handleFileUpload(fileList[0].originFileObj); + } + }; + + const handleRemove = () => { + if (existingFile && onFileRemove) { + onFileRemove(existingFile); + } else if (onFileRemove) { + onFileRemove(null); + } + }; + + const renderExistingFile = () => { + const fileToShow = existingFile || uploadedFile; + if (!fileToShow) { + return null; } - return false; + const filePath = fileToShow.uploadPath || fileToShow.url || fileToShow.path_icon || fileToShow.path_solution; + const fileName = fileToShow.name || filePath?.split('/').pop() || 'Unknown file'; + const fileType = getFileType(fileName); + const isImage = fileType === 'image'; + + const handlePreview = () => { + if (!showPreview || !filePath) return; + + if (isImage) { + const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images'; + const filename = filePath.split('/').pop(); + const imageUrl = getFileUrl(folder, filename); + + if (imageUrl) { + setPreviewImage(imageUrl); + setPreviewOpen(true); + setPreviewTitle(fileName); + } else { + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Cannot generate image preview URL', + }); + } + } else { + const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images'; + const filename = filePath.split('/').pop(); + const fileUrl = getFileUrl(folder, filename); + + if (fileUrl) { + window.open(fileUrl, '_blank', 'noopener,noreferrer'); + } else { + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Cannot generate file preview URL', + }); + } + } + }; + + const getThumbnailUrl = () => { + if (!isImage || !filePath) return null; + + const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images'; + const filename = filePath.split('/').pop(); + return getFileUrl(folder, filename); + }; + + const thumbnailUrl = getThumbnailUrl(); + + return ( +
+
+ {isImage ? ( + {fileName} { + e.target.src = filePath; + }} + /> + ) : ( +
+ +
+ )} + +
+ + {fileName} + +
+ + {fileType === 'image' ? 'Image' : fileType === 'pdf' ? 'PDF' : 'File'} + {fileToShow.size && ` • ${(fileToShow.size / 1024).toFixed(1)} KB`} + +
+ +
+ {showPreview && ( +
+
+
+ ); }; const uploadProps = { - multiple: true, - accept: '.pdf,.jpg,.jpeg,.png,.gif', - onRemove: onFileRemove, - beforeUpload: handleFileUpload, - fileList, - onPreview: handleUploadPreview, + name: 'file', + multiple: false, + accept, + disabled: disabled || isUploading, + fileList: [], + beforeUpload: () => false, + onChange: handleFileChange, + onPreview: handlePreview, + maxCount, }; return ( - <> - -

- -

-

Click or drag file to this area to upload

-

Support for PDF and image files only

-
+
+ {!existingFile && ( + + {type === 'drag' ? ( + +

+ +

+

{uploadText}

+

{uploadHint}

+
+ ) : ( + + )} +
+ )} - setPreviewOpen(false)} - width="80%" - style={{ top: 20 }} - > - {previewImage && ( - {previewTitle} - )} - - + + + {showPreview && ( + setPreviewOpen(false)} + width={600} + style={{ top: 100 }} + > + {previewImage && ( + {previewTitle} + )} + + )} +
); }; diff --git a/src/pages/master/brandDevice/component/FormActions.jsx b/src/pages/master/brandDevice/component/FormActions.jsx deleted file mode 100644 index e3f8280..0000000 --- a/src/pages/master/brandDevice/component/FormActions.jsx +++ /dev/null @@ -1,70 +0,0 @@ -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 ( -
- - {showCancelButton && ( - - )} - {currentStep > 0 && ( - - )} - - - - {currentStep < 1 && ( - - )} - {currentStep === 1 && ( - - )} - -
- ); -}; - -export default FormActions; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/ListBrandDevice.jsx b/src/pages/master/brandDevice/component/ListBrandDevice.jsx index 884da82..d7a2f23 100644 --- a/src/pages/master/brandDevice/component/ListBrandDevice.jsx +++ b/src/pages/master/brandDevice/component/ListBrandDevice.jsx @@ -26,26 +26,12 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ key: 'brand_name', width: '20%', }, - { - title: 'Type', - dataIndex: 'brand_type', - key: 'brand_type', - width: '15%', - render: (text) => text || '-', - }, { title: 'Manufacturer', dataIndex: 'brand_manufacture', key: 'brand_manufacture', width: '20%', }, - { - title: 'Model', - dataIndex: 'brand_model', - key: 'brand_model', - width: '15%', - render: (text) => text || '-', - }, { title: 'Status', dataIndex: 'is_active', @@ -105,9 +91,9 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const ListBrandDevice = memo(function ListBrandDevice(props) { const [trigerFilter, setTrigerFilter] = useState(false); - const defaultFilter = { search: '' }; + const defaultFilter = { criteria: '' }; const [formDataFilter, setFormDataFilter] = useState(defaultFilter); - const [searchValue, setSearchValue] = useState(''); + const [searchText, setSearchText] = useState(''); const navigate = useNavigate(); @@ -128,23 +114,21 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { }; const handleSearch = () => { - setFormDataFilter({ search: searchValue }); + setFormDataFilter({ criteria: searchText }); setTrigerFilter((prev) => !prev); }; const handleSearchClear = () => { - setSearchValue(''); - setFormDataFilter({ search: '' }); + setSearchText(''); + setFormDataFilter({ criteria: '' }); setTrigerFilter((prev) => !prev); }; const showPreviewModal = (param) => { - // Direct navigation without loading, page will handle its own loading navigate(`/master/brand-device/view/${param.brand_id}`); }; const showEditModal = (param = null) => { - // Direct navigation without loading, page will handle its own loading if (param) { navigate(`/master/brand-device/edit/${param.brand_id}`); } else { @@ -158,7 +142,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { title: 'Konfirmasi', message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?', onConfirm: () => handleDelete(param.brand_id, param.brand_name), - onCancel: () => {}, + onCancel: () => { }, }); }; @@ -172,7 +156,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { title: 'Berhasil', message: `Brand ${brand_name} deleted successfully.`, }); - doFilter(); // Refresh data + doFilter(); } else { NotifAlert({ icon: 'error', @@ -181,7 +165,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { }); } } catch (error) { - console.error('Delete Brand Device Error:', error); NotifAlert({ icon: 'error', title: 'Error', @@ -199,13 +182,12 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { { const value = e.target.value; - setSearchValue(value); - // Auto search when clearing by backspace/delete + setSearchText(value); if (value === '') { - setFormDataFilter({ search: '' }); + setFormDataFilter({ criteria: '' }); setTrigerFilter((prev) => !prev); } }} @@ -251,7 +233,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { }} size="large" > - Add Brand Device + Add data diff --git a/src/pages/master/brandDevice/component/ListErrorCode.jsx b/src/pages/master/brandDevice/component/ListErrorCode.jsx index d69f3ef..85fe86c 100644 --- a/src/pages/master/brandDevice/component/ListErrorCode.jsx +++ b/src/pages/master/brandDevice/component/ListErrorCode.jsx @@ -1,84 +1,316 @@ -import React from 'react'; -import { Table, Button, Space } from 'antd'; -import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import React, { useState, useEffect, useMemo } from 'react'; +import { Card, Input, Button, Row, Col, Empty } from 'antd'; +import { PlusOutlined, SearchOutlined, DeleteOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { getErrorCodesByBrandId, deleteErrorCode } from '../../../../api/master-brand'; +import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; -const ErrorCodeTable = ({ - errorCodes, - loading, - onPreview, - onEdit, - onDelete, - onFileView +const ListErrorCode = ({ + brandId, + selectedErrorCode, + onErrorCodeSelect, + onAddNew, + tempErrorCodes = [], + trigerFilter, + searchText, + onSearchChange, + onSearch, + onSearchClear, + isReadOnly = false, + errorCodes: propErrorCodes = null }) => { - 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) => ( -
- {solutions && solutions.length > 0 ? ( - solutions.map((sol, index) => ( -
- - {sol.solution_name} - -
- )) - ) : ( - No solutions - )} -
- ) - }, - { - title: 'Action', - key: 'action', - render: (_, record) => ( - -
+ + { + const value = e.target.value; + if (onSearchChange) { + onSearchChange(value); + } + }} + onSearch={handleSearch} + allowClear + enterButton={ + + } + size="default" + style={{ + marginBottom: 12, + height: '32px', + width: '100%', + maxWidth: '300px' + }} + /> + +
+ {errorCodes.length === 0 ? ( + + ) : ( +
+ {errorCodes.map((item) => ( +
onErrorCodeSelect(item)} + > +
+
+
+ {item.error_code} +
+
+ {item.error_code_name} +
+
+ {item.status === 'existing' && ( +
+
+ ))} +
+ )} +
+ + {pagination.total_limit > 0 && ( + +
+ + Menampilkan {pagination.current_limit} data halaman{' '} + {pagination.current_page} dari total {pagination.total_limit} data + + + +
+ + + {pagination.current_page} / {pagination.total_page} + + +
+ + + )} + ); }; -export default ErrorCodeTable; \ No newline at end of file +export default ListErrorCode; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/SolutionField.jsx b/src/pages/master/brandDevice/component/SolutionField.jsx index 792db80..4ce54c1 100644 --- a/src/pages/master/brandDevice/component/SolutionField.jsx +++ b/src/pages/master/brandDevice/component/SolutionField.jsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect } from 'react'; -import { Form, Input, Button, Switch, Radio, Upload, Typography, Space } from 'antd'; -import { DeleteOutlined, UploadOutlined, EyeOutlined } from '@ant-design/icons'; -import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads'; +import React, { useState } from 'react'; +import { Form, Input, Button, Switch, Radio, Typography, Space, Card, ConfigProvider } from 'antd'; +import { DeleteOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons'; +import FileUploadHandler from './FileUploadHandler'; import { NotifAlert } from '../../../../components/Global/ToastNotif'; +import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads'; const { Text } = Typography; const { TextArea } = Input; @@ -20,223 +21,475 @@ const SolutionFieldNew = ({ onRemove, onFileUpload, onFileView, - fileList = [] + fileList = [], + originalSolutionData = null }) => { - const [currentStatus, setCurrentStatus] = useState(solutionStatus ?? true); + const form = Form.useFormInstance(); + const [currentFile, setCurrentFile] = useState(null); + const [isDeleted, setIsDeleted] = useState(false); - // Watch form values - const getFieldValue = () => { - try { - const form = document.querySelector(`[data-field="${fieldName}"]`)?.form; - if (form) { - const formData = new FormData(form); - return formData.get(`${fieldName}.status`) === 'on'; - } - return currentStatus; - } catch { - return currentStatus; + const fileUpload = Form.useWatch(['solution_items', fieldKey, 'fileUpload'], form); + const file = Form.useWatch(['solution_items', fieldKey, 'file'], form); + const nameValue = Form.useWatch(['solution_items', fieldKey, 'name'], form); + const fileNameValue = Form.useWatch(['solution_items', fieldKey, 'fileName'], form); + const statusValue = Form.useWatch(['solution_items', fieldKey, 'status'], form) ?? true; + + const pathSolution = Form.useWatch(['solution_items', fieldKey, 'path_solution'], form); + + const [deleteCounter, setDeleteCounter] = useState(0); + + React.useEffect(() => { + if (!nameValue || nameValue === '') { + setCurrentFile(null); + setIsDeleted(false); + setDeleteCounter(prev => prev + 1); } - }; + }, [nameValue]); - useEffect(() => { - setCurrentStatus(solutionStatus ?? true); - }, [solutionStatus]); - const handleFileUpload = async (file) => { - try { - const isAllowedType = [ - 'application/pdf', - 'image/jpeg', - 'image/png', - 'image/gif', - ].includes(file.type); + React.useEffect(() => { + const getFileFromFormValues = () => { + const hasValidFileUpload = fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0; + const hasValidFile = file && typeof file === 'object' && Object.keys(file).length > 0; + const hasValidPath = pathSolution && pathSolution.trim() !== ''; - if (!isAllowedType) { - NotifAlert({ - icon: 'error', - title: 'Error', - message: `${file.name} bukan file PDF atau gambar yang diizinkan.`, - }); - return; + const wasExplicitlyDeleted = + (fileUpload === null || file === null || pathSolution === null) && + !hasValidFileUpload && + !hasValidFile && + !hasValidPath; + + if (wasExplicitlyDeleted) { + return null; } - 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) { - // Store the file info with the solution field - file.uploadPath = actualPath; - file.solutionId = fieldKey; - file.type_solution = fileType; - onFileUpload(file); - NotifAlert({ - icon: 'success', - title: 'Berhasil', - message: `${file.name} berhasil diupload!`, - }); - } else { - NotifAlert({ - icon: 'error', - title: 'Gagal', - message: `Gagal mengupload ${file.name}`, - }); + if (solutionType === 'text') { + return null; } - } catch (error) { - console.error('Error uploading file:', error); - NotifAlert({ - icon: 'error', - title: 'Error', - message: `Gagal mengupload ${file.name}. Silakan coba lagi.`, - }); + + if (hasValidFileUpload) { + return fileUpload; + } + if (hasValidFile) { + return file; + } + if (hasValidPath) { + return { + name: fileNameValue || pathSolution.split('/').pop() || 'File', + uploadPath: pathSolution, + url: pathSolution, + path: pathSolution + }; + } + + return null; + }; + + const fileFromForm = getFileFromFormValues(); + + if (JSON.stringify(currentFile) !== JSON.stringify(fileFromForm)) { + setCurrentFile(fileFromForm); } - }; + }, [fileUpload, file, pathSolution, solutionType, deleteCounter, fileNameValue, fieldKey]); + const renderSolutionContent = () => { if (solutionType === 'text') { return (