From cf063822eb6ce9da74f0d8a58b31bb864d6a1a14 Mon Sep 17 00:00:00 2001 From: Iqbal Rizqi Kurniawan Date: Tue, 21 Oct 2025 17:07:36 +0700 Subject: [PATCH] feat: add brand device management with error code handling and navigation --- src/App.jsx | 2 + src/api/master-brand.jsx | 135 ++++++++ src/pages/master/brand/ErrorCode.jsx | 151 ++++++++ src/pages/master/brand/FormBrand.jsx | 59 ++++ .../master/brandDevice/AddBrandDevice.jsx | 325 ++++++++++++++++++ .../master/brandDevice/IndexBrandDevice.jsx | 51 +-- .../component/DetailBrandDevice.jsx | 310 ----------------- .../brandDevice/component/ListBrandDevice.jsx | 9 +- 8 files changed, 677 insertions(+), 365 deletions(-) create mode 100644 src/api/master-brand.jsx create mode 100644 src/pages/master/brand/ErrorCode.jsx create mode 100644 src/pages/master/brand/FormBrand.jsx create mode 100644 src/pages/master/brandDevice/AddBrandDevice.jsx delete mode 100644 src/pages/master/brandDevice/component/DetailBrandDevice.jsx diff --git a/src/App.jsx b/src/App.jsx index 511886c..9f77ca2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,6 +14,7 @@ import IndexDevice from './pages/master/device/IndexDevice'; import IndexTag from './pages/master/tag/IndexTag'; import IndexUnit from './pages/master/unit/IndexUnit'; import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; +import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; import IndexPlantSection from './pages/master/plantSection/IndexPlantSection'; import IndexStatus from './pages/master/status/IndexStatus'; import IndexShift from './pages/master/shift/IndexShift'; @@ -57,6 +58,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/api/master-brand.jsx b/src/api/master-brand.jsx new file mode 100644 index 0000000..e68b2bb --- /dev/null +++ b/src/api/master-brand.jsx @@ -0,0 +1,135 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllBrands = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `brand?${queryParams.toString()}`, + }); + if (response.paging) { + const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0; + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: response.paging.current_page || 1, + limit: response.paging.current_limit || 10, + total: totalData, + page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10)) + }, + total: totalData + } + }; + } + + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllBrands error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getBrandById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `brand/${id}`, + }); + return response.data; +}; + +const createBrand = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `brand`, + params: queryParams, + }); + if (Array.isArray(response) && response.length === 0) { + return { + statusCode: 500, + data: null, + message: 'Request failed', + rows: 0 + }; + } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateBrand = async (brand_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `brand/${brand_id}`, + params: queryParams, + }); + if (Array.isArray(response) && response.length === 0) { + return { + statusCode: 500, + data: null, + message: 'Request failed', + rows: 0 + }; + } + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteBrand = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `brand/${queryParams}`, + }); + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand }; diff --git a/src/pages/master/brand/ErrorCode.jsx b/src/pages/master/brand/ErrorCode.jsx new file mode 100644 index 0000000..81bc7da --- /dev/null +++ b/src/pages/master/brand/ErrorCode.jsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Card, Typography, Button, Modal, Form, Input, message } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import TableList from '../../../components/Global/TableList'; +// import { getAllErrorCodesByBrand, createErrorCode, updateErrorCode, deleteErrorCode } from '../../api/master-errorcode'; // Mock this later + +const { Title } = Typography; + +// Mock API functions for now +const mockApi = { + errorCodes: [ + { error_code_id: 1, brand_id: 1, error_code: 'E-001', description: 'Paper Jam' }, + { error_code_id: 2, brand_id: 1, error_code: 'E-002', description: 'Low Ink' }, + ], + getAllErrorCodesByBrand: async (brandId) => { + return { status: 200, data: { data: mockApi.errorCodes.filter(ec => ec.brand_id == brandId) } }; + }, + createErrorCode: async (data) => { + const newId = Math.max(...mockApi.errorCodes.map(ec => ec.error_code_id)) + 1; + const newErrorCode = { ...data, error_code_id: newId }; + mockApi.errorCodes.push(newErrorCode); + return { statusCode: 201, data: newErrorCode }; + }, + updateErrorCode: async (id, data) => { + const index = mockApi.errorCodes.findIndex(ec => ec.error_code_id === id); + if (index !== -1) { + mockApi.errorCodes[index] = { ...mockApi.errorCodes[index], ...data }; + return { statusCode: 200, data: mockApi.errorCodes[index] }; + } + return { statusCode: 404, message: 'Not Found' }; + }, + deleteErrorCode: async (id) => { + const index = mockApi.errorCodes.findIndex(ec => ec.error_code_id === id); + if (index !== -1) { + mockApi.errorCodes.splice(index, 1); + return { statusCode: 200 }; + } + return { statusCode: 404, message: 'Not Found' }; + } +}; + +const ErrorCodePage = () => { + const { brandId } = useParams(); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const [errorCodes, setErrorCodes] = useState([]); + const [loading, setLoading] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingErrorCode, setEditingErrorCode] = useState(null); + + const fetchData = async () => { + setLoading(true); + const response = await mockApi.getAllErrorCodesByBrand(brandId); + if (response.status === 200) { + setErrorCodes(response.data.data); + } + setLoading(false); + }; + + useEffect(() => { + fetchData(); + }, [brandId]); + + const columns = [ + { title: 'Error Code', dataIndex: 'error_code', key: 'error_code' }, + { title: 'Description', dataIndex: 'description', key: 'description' }, + { + title: 'Action', + key: 'action', + render: (_, record) => ( + <> + + + + ), + }, + ]; + + const handleAdd = () => { + setEditingErrorCode(null); + form.resetFields(); + setIsModalVisible(true); + }; + + const handleEdit = (errorCode) => { + setEditingErrorCode(errorCode); + form.setFieldsValue(errorCode); + setIsModalVisible(true); + }; + + const handleDelete = async (id) => { + await mockApi.deleteErrorCode(id); + message.success('Error code deleted successfully'); + fetchData(); + }; + + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + if (editingErrorCode) { + await mockApi.updateErrorCode(editingErrorCode.error_code_id, values); + message.success('Error code updated successfully'); + } else { + await mockApi.createErrorCode({ ...values, brand_id: brandId }); + message.success('Error code created successfully'); + } + setIsModalVisible(false); + fetchData(); + } catch (error) { + console.log('Validate Failed:', error); + } + }; + + return ( + + Manage Error Codes for Brand ID: {brandId} + + ({ data: { data: errorCodes } })} + triger={brandId} + /> + setIsModalVisible(false)} + > +
+ + + + + + +
+
+
+ ); +}; + +export default ErrorCodePage; diff --git a/src/pages/master/brand/FormBrand.jsx b/src/pages/master/brand/FormBrand.jsx new file mode 100644 index 0000000..13dabc2 --- /dev/null +++ b/src/pages/master/brand/FormBrand.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { Form, Input, Button, Typography, Card, message } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { createBrand } from '../../api/master-brand'; + +const { Title } = Typography; + +const FormBrand = () => { + const [form] = Form.useForm(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + + const onFinish = async (values) => { + setLoading(true); + try { + const response = await createBrand(values); + if (response.statusCode === 200 || response.statusCode === 201) { + message.success('Brand created successfully!'); + const newBrandId = response.data.brand_id; + // Redirect to the error code page for the new brand + navigate(`/master/brand/${newBrandId}/error-codes`); + } else { + message.error(response.message || 'Failed to create brand.'); + } + } catch (error) { + message.error('An error occurred while creating the brand.'); + console.error(error); + } + setLoading(false); + }; + + return ( + + Add New Brand +
+ + + + + + + +
+
+ ); +}; + +export default FormBrand; diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx new file mode 100644 index 0000000..984695f --- /dev/null +++ b/src/pages/master/brandDevice/AddBrandDevice.jsx @@ -0,0 +1,325 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Input, Divider, Typography, Switch, Button, Steps, Form, message, Table, Row, Col, Radio, Card, Tag, Upload, ConfigProvider } from 'antd'; +import { PlusOutlined, UploadOutlined } from '@ant-design/icons'; +import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; + +const { Text, Title } = Typography; +const { Step } = Steps; + +// Mock API for Error Codes (can be moved to a separate file later) +const mockErrorCodeApi = { + errorCodes: [], + createErrorCode: async (data) => { + const newId = mockErrorCodeApi.errorCodes.length > 0 ? Math.max(...mockErrorCodeApi.errorCodes.map(ec => ec.error_code_id)) + 1 : 1; + const newErrorCode = { ...data, error_code_id: newId }; + mockErrorCodeApi.errorCodes.push(newErrorCode); + return { statusCode: 201, data: newErrorCode }; + }, +}; + +const AddBrandDevice = () => { + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); + const [brandForm] = Form.useForm(); + const [errorCodeForm] = Form.useForm(); + const [confirmLoading, setConfirmLoading] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [anotherSolutionType, setAnotherSolutionType] = useState(null); + const [fileList, setFileList] = useState([]); + + // Watch for form values changes to update the switch color + const statusValue = Form.useWatch('status', errorCodeForm); + + const defaultData = { + brandName: '', + brandType: '', + manufacturer: '', + model: '', + status: true, + }; + + const [formData, setFormData] = useState(defaultData); + const [errorCodes, setErrorCodes] = useState([]); + + const handleCancel = () => { + navigate('/master/brand-device'); + }; + + const handleNextStep = async () => { + try { + await brandForm.validateFields(); + setCurrentStep(1); + } catch (error) { + console.log('Validate Failed:', error); + NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' }); + } + }; + + const handleFinish = async () => { + if (errorCodes.length === 0) { + NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Silakan tambahkan minimal satu error code.' }); + return; + } + + setConfirmLoading(true); + try { + const finalFormData = { ...formData, status: formData.status ? 'Active' : 'Inactive' }; + console.log("Saving brand device:", finalFormData); + await new Promise((resolve) => setTimeout(resolve, 500)); + const newBrandDeviceId = Date.now(); + console.log("Brand device saved with ID:", newBrandDeviceId); + + console.log("Saving error codes:", errorCodes); + for (const errorCode of errorCodes) { + if (errorCode.another_solution === 'image' && errorCode.image) { + console.log(`Uploading image for error code ${errorCode.error_code}:`, errorCode.image.name); + } + await mockErrorCodeApi.createErrorCode({ + ...errorCode, + brand_device_id: newBrandDeviceId + }); + console.log("Saved error code:", errorCode.error_code); + } + + setConfirmLoading(false); + NotifOk({ icon: 'success', title: 'Berhasil', message: 'Brand Device dan Error Code berhasil disimpan.' }); + navigate('/master/brand-device'); + } catch (error) { + setConfirmLoading(false); + console.error("Failed to save data:", error); + NotifAlert({ + icon: "error", + title: "Gagal", + message: "Gagal menyimpan data. Silakan coba lagi.", + }); + } + }; + + const handleAddErrorCode = async () => { + try { + const values = await errorCodeForm.validateFields(); + const newErrorCode = { + ...values, + status: values.status === undefined ? true : values.status, + image: fileList.length > 0 ? fileList[0] : null, + key: `temp-${Date.now()}` + }; + setErrorCodes([...errorCodes, newErrorCode]); + message.success('Error code berhasil ditambahkan'); + errorCodeForm.resetFields(); + setAnotherSolutionType(null); + setFileList([]); + } catch (error) { + console.log('Validate Failed:', error); + NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' }); + } + }; + + const handleDeleteErrorCode = (key) => { + setErrorCodes(errorCodes.filter(item => item.key !== key)); + message.success('Error code berhasil dihapus'); + }; + + const uploadProps = { + onRemove: (file) => { + setFileList([]); + }, + beforeUpload: (file) => { + setFileList([file]); + return false; // Prevent auto-upload + }, + fileList, + }; + + const errorCodeColumns = [ + { title: 'Error Code', dataIndex: 'error_code', key: 'error_code' }, + { title: 'Trouble Description', dataIndex: 'description', key: 'description' }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status) => ( + + {status ? 'Active' : 'Inactive'} + + ), + }, + { + title: 'Action', + key: 'action', + render: (_, record) => ( + + ), + }, + ]; + + useEffect(() => { + brandForm.setFieldsValue(formData); + }, [formData, brandForm]); + + useEffect(() => { + setBreadcrumbItems([ + { title: • Master }, + { title: navigate('/master/brand-device')}>Brand Device }, + { title: Tambah Brand Device } + ]); + }, [setBreadcrumbItems, navigate]); + + const renderStepContent = () => { + if (currentStep === 0) { + return ( +
setFormData(prev => ({...prev, ...allValues}))} initialValues={formData}> + + + + + + + + + + + + + + + +
+ ); + } + if (currentStep === 1) { + return ( +
+ Tambah Error Code {errorCodes.length + 1} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setAnotherSolutionType(e.target.value)}> + Image + Other + + + {anotherSolutionType === 'image' && ( + + + + + + )} + {anotherSolutionType === 'other' && ( + + + + )} + + + +
+ + Daftar Error Code + + + ); + } + return null; + }; + + return ( + + Tambah Brand Device + + + + + +
+ {renderStepContent()} +
+ +
+ + + {currentStep > 0 && ( + + )} + + + {currentStep < 1 && ( + + )} + {currentStep === 1 && ( + + )} + +
+
+ ); +}; + +export default AddBrandDevice; diff --git a/src/pages/master/brandDevice/IndexBrandDevice.jsx b/src/pages/master/brandDevice/IndexBrandDevice.jsx index 53042c1..2deecdf 100644 --- a/src/pages/master/brandDevice/IndexBrandDevice.jsx +++ b/src/pages/master/brandDevice/IndexBrandDevice.jsx @@ -1,8 +1,6 @@ - -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import ListBrandDevice from './component/ListBrandDevice'; -import DetailBrandDevice from './component/DetailBrandDevice'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { Typography } from 'antd'; @@ -12,35 +10,6 @@ const IndexBrandDevice = memo(function IndexBrandDevice() { const navigate = useNavigate(); const { setBreadcrumbItems } = useBreadcrumb(); - const [actionMode, setActionMode] = useState('list'); - const [selectedData, setSelectedData] = useState(null); - const [readOnly, setReadOnly] = useState(false); - const [showModal, setShowmodal] = useState(false); - - const setMode = (param) => { - setActionMode(param); - switch (param) { - case 'add': - setReadOnly(false); - setShowmodal(true); - break; - - case 'edit': - setReadOnly(false); - setShowmodal(true); - break; - - case 'preview': - setReadOnly(true); - setShowmodal(true); - break; - - default: - setShowmodal(false); - break; - } - }; - useEffect(() => { const token = localStorage.getItem('token'); if (token) { @@ -55,23 +24,9 @@ const IndexBrandDevice = memo(function IndexBrandDevice() { return ( - - + ); }); -export default IndexBrandDevice; +export default IndexBrandDevice; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/DetailBrandDevice.jsx b/src/pages/master/brandDevice/component/DetailBrandDevice.jsx deleted file mode 100644 index 137bf27..0000000 --- a/src/pages/master/brandDevice/component/DetailBrandDevice.jsx +++ /dev/null @@ -1,310 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd'; -import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; - -const { Text } = Typography; - -const DetailBrandDevice = (props) => { - const [confirmLoading, setConfirmLoading] = useState(false); - - const defaultData = { - brand_id: '', - brandName: '', - brandType: '', - manufacturer: '', - model: '', - status: 'Active', - }; - - const [FormData, setFormData] = useState(defaultData); - - const handleCancel = () => { - props.setSelectedData(null); - props.setActionMode('list'); - }; - - const handleSave = async () => { - setConfirmLoading(true); - - // Validasi required fields - if (!FormData.brandName) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Brand Name Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - if (!FormData.brandType) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Type Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - if (!FormData.manufacturer) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Manufacturer Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - if (!FormData.model) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Model Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - if (!FormData.status) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Status Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - const payload = { - brandName: FormData.brandName, - brandType: FormData.brandType, - manufacturer: FormData.manufacturer, - model: FormData.model, - status: FormData.status, - }; - - try { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 500)); - - const response = { - statusCode: FormData.brand_id ? 200 : 201, - data: { - brandName: FormData.brandName, - }, - }; - - console.log('Save Brand Device Response:', response); - - // Check if response is successful - if (response && (response.statusCode === 200 || response.statusCode === 201)) { - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: `Data Brand Device "${ - response.data?.brandName || FormData.brandName - }" berhasil ${FormData.brand_id ? 'diubah' : 'ditambahkan'}.`, - }); - - props.setActionMode('list'); - } else { - NotifAlert({ - icon: 'error', - title: 'Gagal', - message: response?.message || 'Terjadi kesalahan saat menyimpan data.', - }); - } - } catch (error) { - console.error('Save Brand Device Error:', error); - NotifAlert({ - icon: 'error', - title: 'Error', - message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', - }); - } - - setConfirmLoading(false); - }; - - const handleInputChange = (e) => { - const { name, value } = e.target; - setFormData({ - ...FormData, - [name]: value, - }); - }; - - const handleSelectChange = (name, value) => { - setFormData({ - ...FormData, - [name]: value, - }); - }; - - const handleStatusToggle = (event) => { - const isChecked = event; - setFormData({ - ...FormData, - status: isChecked ? true : false, - }); - }; - - useEffect(() => { - const token = localStorage.getItem('token'); - if (token) { - if (props.selectedData != null) { - setFormData(props.selectedData); - } else { - setFormData(defaultData); - } - } else { - // navigate('/signin'); // Uncomment if useNavigate is imported - } - }, [props.showModal]); - - return ( - - - - - - {!props.readOnly && ( - - )} - - , - ]} - > - {FormData && ( -
-
-
- Status -
-
-
- -
-
- {FormData.status === true ? 'Active' : 'Inactive'} -
-
-
- - -
- Brand Name - * - -
-
- Type - * - -
-
- Manufacturer - * - -
-
- Model - * - -
-
- )} -
- ); -}; - -export default DetailBrandDevice; diff --git a/src/pages/master/brandDevice/component/ListBrandDevice.jsx b/src/pages/master/brandDevice/component/ListBrandDevice.jsx index f53620f..9b1d740 100644 --- a/src/pages/master/brandDevice/component/ListBrandDevice.jsx +++ b/src/pages/master/brandDevice/component/ListBrandDevice.jsx @@ -231,11 +231,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { props.setActionMode('edit'); }; - const showAddModal = (param = null) => { - props.setSelectedData(param); - props.setActionMode('add'); - }; - const showDeleteDialog = (param) => { NotifConfirmDialog({ icon: 'question', @@ -320,7 +315,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { >