diff --git a/src/App.jsx b/src/App.jsx index 8493d06..4581a26 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import SignIn from './pages/auth/SignIn'; import SignUp from './pages/auth/Signup'; import { ProtectedRoute } from './ProtectedRoute'; import NotFound from './pages/blank/NotFound'; +import { BrandFormProvider } from './context/BrandFormContext'; // Dashboard import Home from './pages/home/Home'; @@ -21,6 +22,7 @@ import IndexShift from './pages/master/shift/IndexShift'; // Brand device import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice'; +import AddEditErrorCode from './pages/master/brandDevice/AddEditErrorCode'; import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice'; import ViewFilePage from './pages/master/brandDevice/ViewFilePage'; @@ -91,25 +93,37 @@ const App = () => { } /> } /> } /> - } /> - } /> - } /> - } /> - } - /> - } - /> - } - /> } /> } /> } /> + + {/* Brand Device Routes with BrandFormProvider */} + + + } /> + } /> + } /> + } /> + } + /> + } + /> + } + /> + } /> + } /> + } /> + } /> + + + } /> }> diff --git a/src/api/master-brand.jsx b/src/api/master-brand.jsx index 942e4e3..99db0ba 100644 --- a/src/api/master-brand.jsx +++ b/src/api/master-brand.jsx @@ -47,4 +47,23 @@ 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; +}; + +export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand, getErrorCodesByBrandId, getErrorCodeById }; diff --git a/src/context/BrandFormContext.jsx b/src/context/BrandFormContext.jsx new file mode 100644 index 0000000..66a1765 --- /dev/null +++ b/src/context/BrandFormContext.jsx @@ -0,0 +1,624 @@ +import React, { createContext, useContext, useReducer } from 'react'; +import { NotifAlert } from '../components/Global/ToastNotif'; + +// Initial state +const initialState = { + brandId: null, + routeBrandId: null, + errorCodeId: null, + currentStep: 1, + brandInfo: { + brand_name: '', + brand_type: '', + brand_manufacture: '', + brand_model: '', + is_active: true + }, + tempErrorCodes: [], + existingErrorCodes: [], + currentErrorCode: null, + isLoading: false, + error: null, + lastSaved: null +}; + +// Action types +const SET_BRAND_INFO = 'SET_BRAND_INFO'; +const SET_BRAND_ID = 'SET_BRAND_ID'; +const SET_ROUTE_BRAND_ID = 'SET_ROUTE_BRAND_ID'; +const SET_ERROR_CODE_ID = 'SET_ERROR_CODE_ID'; +const SET_CURRENT_STEP = 'SET_CURRENT_STEP'; +const UPDATE_BRAND_FIELD = 'UPDATE_BRAND_FIELD'; +const ADD_ERROR_CODE = 'ADD_ERROR_CODE'; +const UPDATE_ERROR_CODE = 'UPDATE_ERROR_CODE'; +const DELETE_ERROR_CODE = 'DELETE_ERROR_CODE'; +const MARK_AS_DELETED = 'MARK_AS_DELETED'; +const SET_TEMP_ERROR_CODES = 'SET_TEMP_ERROR_CODES'; +const SET_EXISTING_ERROR_CODES = 'SET_EXISTING_ERROR_CODES'; +const MERGE_ERROR_CODES = 'MERGE_ERROR_CODES'; +const SET_CURRENT_ERROR_CODE = 'SET_CURRENT_ERROR_CODE'; +const SET_LOADING = 'SET_LOADING'; +const SET_ERROR = 'SET_ERROR'; +const RESET_FORM = 'RESET_FORM'; +const SET_LAST_SAVED = 'SET_LAST_SAVED'; + +// Reducer function +const brandFormReducer = (state, action) => { + switch (action.type) { + case SET_BRAND_INFO: + return { + ...state, + brandInfo: action.payload + }; + + case SET_BRAND_ID: + return { + ...state, + brandId: action.payload + }; + + case SET_ROUTE_BRAND_ID: + return { + ...state, + routeBrandId: action.payload + }; + + case SET_ERROR_CODE_ID: + return { + ...state, + errorCodeId: action.payload + }; + + case SET_CURRENT_STEP: + return { + ...state, + currentStep: action.payload + }; + + case UPDATE_BRAND_FIELD: + return { + ...state, + brandInfo: { + ...state.brandInfo, + [action.payload.field]: action.payload.value + } + }; + + case ADD_ERROR_CODE: + const newErrorCode = { + tempId: Date.now(), + error_code: '', + error_code_name: '', + error_code_description: '', + error_code_color: '#000000ff', + path_icon: '', + is_active: true, + solutions: [], + spareparts: [], + status: 'new', + created_at: new Date().toISOString() + }; + return { + ...state, + tempErrorCodes: [...state.tempErrorCodes, newErrorCode] + }; + + case UPDATE_ERROR_CODE: + const { tempId, data } = action.payload; + + if (tempId.startsWith('existing_')) { + return { + ...state, + existingErrorCodes: state.existingErrorCodes.map(ec => + `existing_${ec.error_code_id}` === tempId + ? { + ...ec, + ...data, + status: 'modified', + updated_at: new Date().toISOString() + } + : ec + ) + }; + } else { + // Update in tempErrorCodes + return { + ...state, + tempErrorCodes: state.tempErrorCodes.map(ec => + ec.tempId === tempId + ? { + ...ec, + ...data, + status: ec.status === 'new' ? 'new' : 'modified', + updated_at: new Date().toISOString() + } + : ec + ) + }; + } + + case DELETE_ERROR_CODE: + return { + ...state, + tempErrorCodes: state.tempErrorCodes.filter(ec => ec.tempId !== action.payload) + }; + + case MARK_AS_DELETED: + const deleteTempId = action.payload; + + // Check if it's an existing error code (format: existing_${error_code_id}) + if (deleteTempId.startsWith('existing_')) { + return { + ...state, + existingErrorCodes: state.existingErrorCodes.map(ec => + `existing_${ec.error_code_id}` === deleteTempId + ? { + ...ec, + status: 'deleted', + is_active: false, + deleted_at: new Date().toISOString() + } + : ec + ) + }; + } else { + return { + ...state, + tempErrorCodes: state.tempErrorCodes.map(ec => + ec.tempId === deleteTempId + ? { + ...ec, + status: 'deleted', + is_active: false, + deleted_at: new Date().toISOString() + } + : ec + ) + }; + } + + case SET_TEMP_ERROR_CODES: + return { + ...state, + tempErrorCodes: action.payload + }; + + case SET_EXISTING_ERROR_CODES: + return { + ...state, + existingErrorCodes: action.payload + }; + + case MERGE_ERROR_CODES: + return { + ...state, + existingErrorCodes: action.payload.existing || [], + tempErrorCodes: action.payload.temporary || [] + }; + + case SET_LOADING: + return { + ...state, + isLoading: action.payload + }; + + case SET_ERROR: + return { + ...state, + error: action.payload + }; + + case RESET_FORM: + return { ...initialState, lastSaved: state.lastSaved }; + + case SET_CURRENT_ERROR_CODE: + return { + ...state, + currentErrorCode: action.payload + }; + + case SET_LAST_SAVED: + return { + ...state, + lastSaved: action.payload + }; + + default: + return state; + } +}; + +// Create context +const BrandFormContext = createContext(); + +export const BrandFormProvider = ({ children }) => { + const [state, dispatch] = useReducer(brandFormReducer, initialState); + + // Actions + const actions = { + setBrandId: (brandId) => { + dispatch({ type: SET_BRAND_ID, payload: brandId }); + }, + + setRouteBrandId: (routeBrandId) => { + dispatch({ type: SET_ROUTE_BRAND_ID, payload: routeBrandId }); + }, + + setErrorCodeId: (errorCodeId) => { + dispatch({ type: SET_ERROR_CODE_ID, payload: errorCodeId }); + }, + + setCurrentStep: (step) => { + dispatch({ type: SET_CURRENT_STEP, payload: step }); + }, + + getCurrentBrandId: () => { + return state.brandId || state.routeBrandId; + }, + + setBrandInfo: (brandInfo) => { + dispatch({ type: SET_BRAND_INFO, payload: brandInfo }); + }, + + updateBrandField: (field, value) => { + dispatch({ type: UPDATE_BRAND_FIELD, payload: { field, value } }); + }, + + addErrorCode: (errorCodeData = {}) => { + const defaultErrorCode = { + tempId: Date.now(), + error_code: '', + error_code_name: '', + error_code_description: '', + error_code_color: '#000000ff', + path_icon: '', + is_active: true, + solutions: [], + spareparts: [], + status: 'new', + created_at: new Date().toISOString() + }; + const newErrorCode = { ...defaultErrorCode, ...errorCodeData }; + dispatch({ type: ADD_ERROR_CODE, payload: newErrorCode }); + }, + + updateErrorCode: (tempId, data) => { + dispatch({ type: UPDATE_ERROR_CODE, payload: { tempId, data } }); + }, + + deleteErrorCode: (tempId, isPermanent = false) => { + if (isPermanent) { + dispatch({ type: DELETE_ERROR_CODE, payload: tempId }); + } else { + dispatch({ type: MARK_AS_DELETED, payload: tempId }); + } + }, + + markAsDeleted: (tempId) => { + dispatch({ type: MARK_AS_DELETED, payload: tempId }); + }, + + setTempErrorCodes: (errorCodes) => { + dispatch({ type: SET_TEMP_ERROR_CODES, payload: errorCodes }); + }, + + setExistingErrorCodes: (errorCodes) => { + dispatch({ type: SET_EXISTING_ERROR_CODES, payload: errorCodes }); + }, + + mergeErrorCodes: (existing, temporary) => { + dispatch({ type: MERGE_ERROR_CODES, payload: { existing, temporary } }); + }, + + setLoading: (isLoading) => { + dispatch({ type: SET_LOADING, payload: isLoading }); + }, + + setError: (error) => { + dispatch({ type: SET_ERROR, payload: error }); + }, + + resetForm: () => { + dispatch({ type: RESET_FORM }); + }, + + setLastSaved: (timestamp) => { + dispatch({ type: SET_LAST_SAVED, payload: timestamp }); + }, + + setCurrentErrorCode: (errorCode) => { + dispatch({ type: SET_CURRENT_ERROR_CODE, payload: errorCode }); + }, + + // Initialize context with route parameters + initializeFromRoute: (routeBrandId, errorCodeId = null) => { + dispatch({ type: SET_ROUTE_BRAND_ID, payload: routeBrandId }); + if (errorCodeId) { + dispatch({ type: SET_ERROR_CODE_ID, payload: errorCodeId }); + } + }, + + // Navigate to specific step with parameter handling + navigateToStep: (step, brandId = null, errorCodeId = null) => { + dispatch({ type: SET_CURRENT_STEP, payload: step }); + if (brandId) { + dispatch({ type: SET_BRAND_ID, payload: brandId }); + } + if (errorCodeId) { + dispatch({ type: SET_ERROR_CODE_ID, payload: errorCodeId }); + } + }, + + // Utility functions + getErrorCodeByTempId: (tempId) => { + return state.tempErrorCodes.find(ec => ec.tempId === tempId); + }, + + getActiveErrorCodes: () => { + return state.tempErrorCodes.filter(ec => ec.status !== 'deleted'); + }, + + getModifiedErrorCodes: () => { + return state.tempErrorCodes.filter(ec => + ec.status === 'new' || ec.status === 'modified' || ec.status === 'deleted' + ); + }, + + hasChanges: () => { + return state.tempErrorCodes.some(ec => + ec.status === 'new' || ec.status === 'modified' || ec.status === 'deleted' + ); + }, + + validateForm: () => { + const errors = {}; + + // Validate brand info + if (!state.brandInfo.brand_name?.trim()) { + errors.brand = 'Brand name is required'; + NotifAlert({ + icon: 'warning', + title: 'Validasi Gagal', + message: 'Brand name wajib diisi!', + }); + return { isValid: false, errors }; + } + + if (!state.brandInfo.brand_manufacture?.trim()) { + errors.brand_manufacture = 'Brand manufacture is required'; + NotifAlert({ + icon: 'warning', + title: 'Validasi Gagal', + message: 'Brand manufacture wajib diisi!', + }); + return { isValid: false, errors }; + } + + const allActiveErrorCodes = [ + ...state.existingErrorCodes, + ...state.tempErrorCodes.filter(ec => ec.status !== 'deleted') + ]; + + if (allActiveErrorCodes.length === 0) { + errors.errorCodes = 'At least one error code is required'; + NotifAlert({ + icon: 'warning', + title: 'Validasi Gagal', + message: 'Brand harus memiliki minimal 1 error code!', + }); + return { isValid: false, errors }; + } + + // Validate each error code + allActiveErrorCodes.forEach((ec, index) => { + if (!ec.error_code?.trim()) { + errors[`errorCode_${ec.tempId || ec.error_code_id}`] = 'Error code is required'; + } + if (!ec.error_code_name?.trim()) { + errors[`errorCodeName_${ec.tempId || ec.error_code_id}`] = 'Error code name is required'; + } + + let solutionsToCheck = []; + if (ec.solution && Array.isArray(ec.solution)) { + solutionsToCheck = ec.solution; + } else if (ec.solutions && Array.isArray(ec.solutions)) { + solutionsToCheck = ec.solutions; + } + + if (solutionsToCheck.length > 0) { + const activeSolutions = solutionsToCheck.filter(sol => sol.status !== 'deleted'); + if (activeSolutions.length === 0) { + errors[`solutions_${ec.tempId || ec.error_code_id}`] = 'Setiap error code harus memiliki minimal 1 solution'; + } + } else { + errors[`solutions_${ec.tempId || ec.error_code_id}`] = 'Setiap error code harus memiliki minimal 1 solution'; + } + + if (ec.spareparts && Array.isArray(ec.spareparts)) { + ec.spareparts.forEach((sp, spIndex) => { + if (sp === undefined || sp === null || sp === '') { + console.error(`❌ Error Code ${index}, Sparepart ${spIndex}: sparepart_id is undefined, null, or empty`); + } + }); + } + }); + + if (Object.keys(errors).length > 0) { + NotifAlert({ + icon: 'warning', + title: 'Validasi Gagal', + message: 'Perbaiki error yang ada sebelum melanjutkan!', + }); + } + + return { isValid: Object.keys(errors).length === 0, errors }; + }, + + prepareSubmissionData: (userId) => { + const allErrorCodes = [ + ...state.existingErrorCodes.map(ec => ({ + ...ec, + tempId: `existing_${ec.error_code_id}`, + status: 'existing' + })), + ...state.tempErrorCodes + ]; + + const finalErrorCodes = allErrorCodes + .filter(ec => ec.status !== 'deleted') + .map(ec => { + const cleanedCode = { + 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.is_active === true ? 1 : 0, + solution: [], + spareparts: [] + }; + + if (ec.error_code_id && ec.status === 'existing') { + cleanedCode.error_code_id = parseInt(ec.error_code_id); + } + + // Handle both solution and solutions fields for compatibility + let solutions = []; + if (ec.solution && Array.isArray(ec.solution)) { + solutions = ec.solution; + } else if (ec.solutions && Array.isArray(ec.solutions)) { + solutions = ec.solutions; + } + + if (solutions.length > 0) { + cleanedCode.solution = solutions + .filter(sol => sol && sol.solution_name) + .map(sol => ({ + solution_name: sol.solution_name || '', + type_solution: sol.type_solution || 'text', + text_solution: sol.text_solution || '', + path_solution: sol.path_solution || '', + is_active: sol.is_active === true ? 1 : 0 + })); + } + + if (ec.spareparts && Array.isArray(ec.spareparts)) { + cleanedCode.spareparts = ec.spareparts + .filter(sp => sp !== undefined && sp !== null && sp !== '' && sp !== 0) + .map(sp => { + let sparepartId = 0; + if (typeof sp === 'object' && sp !== null) { + sparepartId = sp.sparepart_id || sp.id || sp.sparepartId || 0; + } else if (typeof sp === 'string' || typeof sp === 'number') { + sparepartId = parseInt(sp) || 0; + } + return parseInt(sparepartId) || 0; + }) + .filter(id => id > 0); + } + + return cleanedCode; + }); + + const submissionData = { + brand_name: state.brandInfo.brand_name || '', + brand_type: state.brandInfo.brand_type || '', + brand_manufacture: state.brandInfo.brand_manufacture || '', + brand_model: state.brandInfo.brand_model || '', + is_active: state.brandInfo.is_active === true ? 1 : 0, + error_code: finalErrorCodes, + updated_by: parseInt(userId) || 1 + }; + + // console.log(' Prepared flat submission data:', JSON.stringify(submissionData, null, 2)); + + return submissionData; + }, + + getAllErrorCodes: () => { + return [ + ...state.existingErrorCodes.map(ec => ({ + ...ec, + tempId: `existing_${ec.error_code_id}`, + status: 'existing' + })), + ...state.tempErrorCodes + ]; + }, + + getErrorCodeById: (id) => { + const existingCode = state.existingErrorCodes.find(ec => ec.error_code_id == id); + if (existingCode) { + return { + ...existingCode, + tempId: `existing_${existingCode.error_code_id}`, + status: 'existing' + }; + } + + return state.tempErrorCodes.find(ec => ec.tempId == id); + }, + + // Enhanced error code management + loadErrorCodesForBrand: async (brandId) => { + dispatch({ type: SET_LOADING, payload: true }); + try { + const { getErrorCodesByBrandId } = await import('../api/master-brand'); + const response = await getErrorCodesByBrandId(brandId); + + if (response && response.data) { + dispatch({ type: SET_EXISTING_ERROR_CODES, payload: response.data }); + } + return response; + } catch (error) { + dispatch({ type: SET_ERROR, payload: error.message }); + throw error; + } finally { + dispatch({ type: SET_LOADING, payload: false }); + } + }, + + // Smart navigation helper + navigateToErrorCodes: (brandId) => { + const currentId = brandId || state.brandId || state.routeBrandId; + if (currentId) { + dispatch({ type: SET_BRAND_ID, payload: currentId }); + dispatch({ type: SET_CURRENT_STEP, payload: 2 }); + return currentId; + } + return null; + }, + + editErrorCode: (errorCodeId, brandId) => { + const currentBrandId = brandId || state.brandId || state.routeBrandId; + if (currentBrandId && errorCodeId) { + dispatch({ type: SET_BRAND_ID, payload: currentBrandId }); + dispatch({ type: SET_ERROR_CODE_ID, payload: errorCodeId }); + dispatch({ type: SET_CURRENT_STEP, payload: 3 }); + return { brandId: currentBrandId, errorCodeId }; + } + return null; + } + }; + + const value = { + ...state, + ...actions + }; + + return ( + + {children} + + ); +}; + +export const useBrandForm = () => { + const context = useContext(BrandFormContext); + if (!context) { + throw new Error('useBrandForm must be used within a BrandFormProvider'); + } + return context; +}; + +export default BrandFormContext; \ No newline at end of file diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx index 00603cf..00d1ddc 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 { useEffect, useState, useCallback, useMemo } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { Divider, Typography, @@ -10,53 +10,54 @@ import { Col, Card, Spin, - Table, Tag, Space, + Input, } from 'antd'; -import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { EyeOutlined, EditOutlined, DeleteOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'; +import TableList from '../../../components/Global/TableList'; +import { ConfigProvider } from 'antd'; +import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { createBrand } from '../../../api/master-brand'; import BrandForm from './component/BrandForm'; -import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm'; -import SolutionForm from './component/SolutionForm'; -import FormActions from './component/FormActions'; -import ListErrorCode from './component/ListErrorCode'; -import { useErrorCodeLogic } from './hooks/errorCode'; import { useSolutionLogic } from './hooks/solution'; -import { EditOutlined, DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons'; -import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic'; +import { useBrandForm } from '../../../context/BrandFormContext'; const { Title } = Typography; const { Step } = Steps; const AddBrandDevice = () => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const { setBreadcrumbItems } = useBreadcrumb(); const [brandForm] = Form.useForm(); - const [errorCodeForm] = Form.useForm(); - const [confirmLoading, setConfirmLoading] = useState(false); - const [currentStep, setCurrentStep] = useState(0); - const [loading, setLoading] = useState(false); - const [formData, setFormData] = useState({ - brand_name: '', - brand_type: '', - brand_model: '', - brand_manufacture: '', - is_active: true, - }); - const [errorCodes, setErrorCodes] = useState([]); - const [pendingErrorCodes, setPendingErrorCodes] = useState([]); - const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [solutionForm] = Form.useForm(); + const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [selectedSparepartIds, setSelectedSparepartIds] = useState([]); - const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null); - const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false); + const [loading, setLoading] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const [searchText, setSearchText] = useState(''); + const [trigerFilter, setTrigerFilter] = useState(false); - const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic( - errorCodeForm, - [] - ); + // Context integration + const { + brandId, + brandInfo, + setBrandInfo, + tempErrorCodes, + addErrorCode, + updateErrorCode, + deleteErrorCode, + prepareSubmissionData, + validateForm, + resetForm, + } = useBrandForm(); + + // Use step from query parameter or context + const tab = searchParams.get('tab'); + const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0); const { solutionFields, @@ -64,7 +65,6 @@ const AddBrandDevice = () => { solutionStatuses, solutionsToDelete, firstSolutionValid, - checkFirstSolutionValid, handleAddSolutionField, handleRemoveSolutionField, handleSolutionTypeChange, @@ -74,6 +74,354 @@ const AddBrandDevice = () => { setSolutionsForExistingRecord, } = useSolutionLogic(solutionForm); + // Navigation functions + 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 handlePrevStep = () => { + setCurrentStep(0); + }; + + const handleCancel = () => { + navigate('/master/brand-device'); + }; + + const handleAddErrorCode = () => { + navigate(`/master/brand-device/add/error-code/add`); + }; + + const handleEditErrorCodeNavigate = (record) => { + const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId; + if (errorCodeId) { + navigate(`/master/brand-device/add/error-code/edit/${errorCodeId}`); + } + }; + + const handleDeleteErrorCode = (record) => { + NotifConfirmDialog({ + icon: 'question', + title: 'Konfirmasi Hapus', + message: `Apakah Anda yakin ingin menghapus error code "${record.error_code}"?`, + onConfirm: () => { + const tempId = record.tempId || `existing_${record.error_code_id}`; + deleteErrorCode(tempId, false); // false = soft delete + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Error code berhasil dihapus!', + }); + setTrigerFilter(prev => !prev); + }, + onCancel: () => {} + }); + }; + + const handlePreviewErrorCode = (record) => { + console.log('Preview error code:', record); + }; + + const handleSearch = () => { + setSearchText(searchValue); + setTrigerFilter((prev) => !prev); + }; + + const handleSearchClear = () => { + setSearchValue(''); + setSearchText(''); + setTrigerFilter((prev) => !prev); + }; + + const handleBrandFormValuesChange = useCallback((changedValues, allValues) => { + setBrandInfo(allValues); + }, [setBrandInfo]); + + const getErrorCodesData = async (params) => { + try { + const search = params.get('search') || ''; + const page = parseInt(params.get('page')) || 1; + const limit = parseInt(params.get('limit')) || 10; + + const allErrorCodes = tempErrorCodes.filter(ec => ec.status !== 'deleted'); + + let filteredData = allErrorCodes; + + if (searchText) { + filteredData = allErrorCodes.filter(ec => + ec.error_code.toLowerCase().includes(searchText.toLowerCase()) || + ec.error_code_name.toLowerCase().includes(searchText.toLowerCase()) + ); + } + + const startIndex = 0; + const endIndex = startIndex + limit; + const paginatedData = filteredData.slice(startIndex, endIndex); + + return { + data: paginatedData, + pagination: { + current_page: page, + current_limit: limit, + total_limit: filteredData.length, + total_page: Math.ceil(filteredData.length / limit), + } + }; + } catch (error) { + console.error('Error getting error codes data:', error); + return { + data: [], + pagination: { + current_page: 1, + current_limit: 10, + total_limit: 0, + total_page: 0, + } + }; + } + }; + + // Error code columns + const errorCodeColumns = (showPreviewModal, showEditModal, showDeleteDialog) => [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Error Code', + dataIndex: 'error_code', + key: 'error_code', + width: '20%', + render: (text, record) => ( + + {text} + {record.status === 'new' && New} + + ), + }, + { + title: 'Error Name', + dataIndex: 'error_code_name', + key: 'error_code_name', + width: '25%', + }, + { + title: 'Description', + dataIndex: 'error_code_description', + key: 'error_code_description', + width: '30%', + ellipsis: true, + }, + { + title: 'Actions', + key: 'actions', + width: '20%', + render: (_, record) => ( + + + } + size="large" + /> + + + + + + + + + + + + + + + + ); + } + return null; + }; + useEffect(() => { setBreadcrumbItems([ { @@ -99,390 +447,6 @@ const AddBrandDevice = () => { ]); }, [setBreadcrumbItems, navigate]); - const handleCancel = () => { - navigate('/master/brand-device'); - }; - - const handleNextStep = async () => { - try { - const currentFormData = await brandForm.validateFields(); - - setFormData({ - brand_name: currentFormData.brand_name, - brand_type: currentFormData.brand_type || '', - brand_model: currentFormData.brand_model || '', - brand_manufacture: currentFormData.brand_manufacture || '', - is_active: currentFormData.is_active, - }); - - setCurrentStep(1); - } catch (error) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Harap isi semua kolom wajib untuk brand device!', - }); - } - }; - - const handleFinish = async () => { - setConfirmLoading(true); - try { - const transformedErrorCodes = pendingErrorCodes.length > 0 ? pendingErrorCodes.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 || '#ad4141ff', - 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 - })) - })) : []; - - const brandData = { - 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: selectedSparepartIds, - error_code: transformedErrorCodes, - }; - - const response = await createBrand(brandData); - - 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) { - 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, - }); - setErrorCodeIcon(record.errorCodeIcon || null); - setIsErrorCodeFormReadOnly(true); - setEditingErrorCodeKey(record.key); - - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); - } else { - resetSolutionFields(); - } - }; - - 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); - - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); - } - - const formElement = document.querySelector('.ant-form'); - if (formElement) { - formElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }; - - const handleAddErrorCode = async () => { - try { - const errorCodeValues = await errorCodeForm.validateFields(); - const solutionData = getSolutionData(); - - // Validate error code fields - if (!errorCodeValues.error_code || !errorCodeValues.error_code_name) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Error code dan error name wajib diisi!', - }); - return; - } - - // Validate solution data - if (!solutionData || solutionData.length === 0) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap error code harus memiliki minimal 1 solution!', - }); - return; - } - - // Validate each solution has name - const invalidSolution = solutionData.find(sol => !sol.solution_name || sol.solution_name.trim() === ''); - if (invalidSolution) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap solution harus memiliki nama!', - }); - return; - } - - 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 || '', - is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status, - solution: solutionData, - errorCodeIcon: errorCodeIcon, - key: editingErrorCodeKey || `temp-${Date.now()}`, - }; - - let updatedPendingErrorCodes; - if (editingErrorCodeKey) { - updatedPendingErrorCodes = pendingErrorCodes.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 { - updatedPendingErrorCodes = [...pendingErrorCodes, newErrorCode]; - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: 'Error code berhasil ditambahkan!', - }); - } - - setPendingErrorCodes(updatedPendingErrorCodes); - setErrorCodes(updatedPendingErrorCodes); - - 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, - }); - setErrorCodeIcon(null); - resetSolutionFields(); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(null); - }; - - const handleDeleteErrorCode = async (key) => { - if (errorCodes.length <= 1) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap brand harus memiliki minimal 1 error code!', - }); - return; - } - - const updatedErrorCodes = errorCodes.filter((item) => item.key !== key); - setErrorCodes(updatedErrorCodes); - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: 'Error code berhasil dihapus!', - }); - }; - - const handleCreateNewErrorCode = () => { - resetErrorCodeForm(); - resetSolutionFields(); - setErrorCodeIcon(null); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(null); - }; - - const handleErrorCodeIconUpload = (iconData) => { - setErrorCodeIcon(iconData); - }; - - const handleErrorCodeIconRemove = () => { - setErrorCodeIcon(null); - }; - - const renderStepContent = () => { - if (currentStep === 0) { - return ( - - setFormData((prev) => ({ ...prev, ...allValues })) - } - isEdit={false} - selectedSparepartIds={selectedSparepartIds} - onSparepartChange={setSelectedSparepartIds} - showSparepartSection={true} - /> - ); - } - - if (currentStep === 1) { - return ( - <> - - - - {isErrorCodeFormReadOnly - ? editingErrorCodeKey - ? 'View Error Code' - : 'Error Code Form' - : editingErrorCodeKey - ? 'Edit Error Code' - : 'Error Code'} - - } - size="small" - > -
- - -
- - - - Solutions - - } - size="small" - > -
- - -
- - - - Error Codes ({errorCodes.length}) - - } - size="small" - > - -
- -
-
- -
- - ); - } - return null; - }; - return ( @@ -524,15 +488,48 @@ const AddBrandDevice = () => { </div> </div> <Divider /> - <FormActions - currentStep={currentStep} - onPreviousStep={() => setCurrentStep(currentStep - 1)} - onNextStep={handleNextStep} - onSave={handleFinish} - onCancel={handleCancel} - confirmLoading={confirmLoading} - isEditMode={false} - /> + <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div> + <Button onClick={handleCancel}> + Cancel + </Button> + {currentStep === 1 && ( + <Button + onClick={handlePrevStep} + style={{ marginLeft: 8 }} + > + Back to Brand Info + </Button> + )} + </div> + <div> + {currentStep === 0 && ( + <Button + type="primary" + onClick={handleNextStep} + style={{ + backgroundColor: '#23A55A', + borderColor: '#23A55A', + }} + > + Next to Error Codes + </Button> + )} + {currentStep === 1 && ( + <Button + type="primary" + onClick={handleFinish} + loading={confirmLoading} + style={{ + backgroundColor: '#23A55A', + borderColor: '#23A55A', + }} + > + Save Brand Device + </Button> + )} + </div> + </div> </Card> ); }; diff --git a/src/pages/master/brandDevice/AddEditErrorCode.jsx b/src/pages/master/brandDevice/AddEditErrorCode.jsx new file mode 100644 index 0000000..99af522 --- /dev/null +++ b/src/pages/master/brandDevice/AddEditErrorCode.jsx @@ -0,0 +1,588 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Card, + Typography, + Button, + Form, + Row, + Col, + Spin, + Upload, +} from 'antd'; +import { ArrowLeftOutlined, UploadOutlined } from '@ant-design/icons'; +import { getBrandById, getErrorCodeById, updateBrand, getErrorCodesByBrandId } from '../../../api/master-brand'; +import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; +import { useBrandForm } from '../../../context/BrandFormContext'; +import { uploadFile } from '../../../api/file-uploads'; +import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm'; +import SolutionForm from './component/SolutionForm'; +import { useSolutionLogic } from './hooks/solution'; +import SingleSparepartSelect from './component/SingleSparepartSelect'; +import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; + +const { Title } = Typography; + +const AddEditErrorCode = () => { + const navigate = useNavigate(); + const { brandId: routeBrandId, errorCodeId } = useParams(); + const { setBreadcrumbItems } = useBreadcrumb(); + + // Use BrandForm context + const { + brandId: contextBrandId, + routeBrandId: contextRouteBrandId, + setRouteBrandId, + setErrorCodeId, + initializeFromRoute, + tempErrorCodes, + existingErrorCodes, + addErrorCode, + updateErrorCode, + setCurrentErrorCode + } = useBrandForm(); + + // Use brandId from context first, fallback to route + const currentBrandId = contextBrandId || routeBrandId; + + // Forms + const [errorCodeForm] = Form.useForm(); + const [solutionForm] = Form.useForm(); + + const [loading, setLoading] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const [errorCodeIcon, setErrorCodeIcon] = useState(null); + const [selectedSparepartIds, setSelectedSparepartIds] = useState([]); + const [isEdit, setIsEdit] = useState(false); + const [fileList, setFileList] = useState([]); + + const { + solutionFields, + solutionTypes, + solutionStatuses, + solutionsToDelete, + firstSolutionValid, + handleAddSolutionField, + handleRemoveSolutionField, + handleSolutionTypeChange, + handleSolutionStatusChange, + resetSolutionFields, + getSolutionData, + setSolutionsForExistingRecord, + } = useSolutionLogic(solutionForm); + + useEffect(() => { + const isEditMode = errorCodeId && errorCodeId !== 'add'; + setIsEdit(isEditMode); + + // Initialize context with route parameters + if (routeBrandId) { + initializeFromRoute(routeBrandId, errorCodeId); + } + + setBreadcrumbItems([ + { + title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>• Master</span> + }, + { + title: ( + <span + style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }} + onClick={() => navigate('/master/brand-device')} + > + Brand Device + </span> + ), + }, + { + title: ( + <span + style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }} + onClick={() => navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`)} + > + Edit Brand Device + </span> + ), + }, + { + title: ( + <span style={{ fontSize: '14px', fontWeight: 'bold' }}> + {isEditMode ? 'Edit Error Code' : 'Add Error Code'} + </span> + ), + }, + ]); + + if (isEditMode && errorCodeId) { + // For existing error codes, construct the proper tempId format + const tempId = errorCodeId.startsWith('existing_') ? errorCodeId : `existing_${errorCodeId}`; + loadExistingErrorCode(tempId); + } + }, [currentBrandId, errorCodeId, navigate, setBreadcrumbItems]); + + const loadExistingErrorCode = async (tempId) => { + try { + setLoading(true); + + // console.log(' Looking for error code with tempId:', tempId); + // console.log(' Available error codes in context:', tempErrorCodes); + + // Find error code in tempErrorCodes first + let existingErrorCode = tempErrorCodes.find(ec => ec.tempId === tempId); + + // If not found, check in existingErrorCodes with format existing_${error_code_id} + if (!existingErrorCode && tempId.startsWith('existing_')) { + const errorId = tempId.replace('existing_', ''); + existingErrorCode = existingErrorCodes.find(ec => ec.error_code_id == errorId); + if (existingErrorCode) { + existingErrorCode = { + ...existingErrorCode, + tempId: tempId + }; + } + } + + // console.log(' Found error code in context:', existingErrorCode); + + if (existingErrorCode) { + errorCodeForm.setFieldsValue({ + error_code: existingErrorCode.error_code, + error_code_name: existingErrorCode.error_code_name || '', + error_code_description: existingErrorCode.error_code_description || '', + error_code_color: existingErrorCode.error_code_color || '#000000', + status: existingErrorCode.is_active !== false, + }); + + if (existingErrorCode.path_icon) { + setErrorCodeIcon({ + name: existingErrorCode.path_icon.split('/').pop(), + uploadPath: existingErrorCode.path_icon, + url: existingErrorCode.path_icon, + }); + } + + if (existingErrorCode.solution && existingErrorCode.solution.length > 0) { + // console.log('🔍 Setting solutions from context:', existingErrorCode.solution); + setSolutionsForExistingRecord(existingErrorCode.solution, solutionForm); + } + + if (existingErrorCode.spareparts && existingErrorCode.spareparts.length > 0) { + // console.log('🔍 Setting spareparts from context:', existingErrorCode.spareparts); + setSelectedSparepartIds(existingErrorCode.spareparts); + } + } else { + // console.log('🔍 Error code not found in context, trying API...'); + + let errorIdToUse = tempId; + // Extract the actual error_code_id from tempId format + if (tempId.startsWith('existing_')) { + errorIdToUse = tempId.replace('existing_', ''); + } + + const errorCodeResponse = await getErrorCodeById(errorIdToUse); + + if (errorCodeResponse && errorCodeResponse.statusCode === 200) { + const errorData = errorCodeResponse.data; + + if (errorData) { + errorCodeForm.setFieldsValue({ + error_code: errorData.error_code, + error_code_name: errorData.error_code_name || '', + error_code_description: errorData.error_code_description || '', + error_code_color: errorData.error_code_color || '#000000', + status: errorData.is_active !== false, + }); + + if (errorData.path_icon) { + setErrorCodeIcon({ + name: errorData.path_icon.split('/').pop(), + uploadPath: errorData.path_icon, + url: errorData.path_icon, + }); + } + + // Set solutions from API data (include file data) + if (errorData.solution && errorData.solution.length > 0) { + setSolutionsForExistingRecord(errorData.solution, solutionForm); + } + + // Set spareparts from API data + if (errorData.spareparts && errorData.spareparts.length > 0) { + const sparepartIds = errorData.spareparts.map(sp => sp.sparepart_id); + setSelectedSparepartIds(sparepartIds); + } + + // Don't add to context - this is existing data from API + // The context should already have this error code from the brand data loading + } + } else { + // console.log('🔍 API Response error or not found:', errorCodeResponse); + errorCodeForm.setFieldsValue({ + error_code: '', + error_code_name: '', + error_code_description: '', + error_code_color: '#000000', + status: true, + }); + + NotifAlert({ + icon: 'warning', + title: 'Peringatan', + message: 'Error code not found. Creating new error code.', + }); + } + } + } catch (error) { + console.error('Failed to load error code:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Failed to load error code data', + }); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + try { + await errorCodeForm.validateFields(); + + const solutionValues = solutionForm.getFieldsValue(); + + const firstSolutionPath = `solution_items,${solutionFields[0]?.key || 0}`; + const firstSolution = solutionValues[firstSolutionPath]; + + let isValid = false; + if (firstSolution && firstSolution.name && firstSolution.name.trim() !== '') { + const firstSolutionType = solutionTypes[solutionFields[0]?.key || 0]; + if (firstSolutionType === 'text') { + isValid = firstSolution.text && firstSolution.text.trim() !== ''; + } else { + isValid = true; + } + } + + if (!isValid) { + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap lengkapi minimal 1 solution', + }); + return; + } + + const errorCodeValues = errorCodeForm.getFieldsValue(); + + const solutionData = getSolutionData(); + + // Determine the correct tempId for editing + let updateTempId; + if (isEdit) { + // For existing error codes, find the correct tempId + if (errorCodeId && !errorCodeId.startsWith('pending-')) { + // Look for existing error code in context + const existingEc = existingErrorCodes.find(ec => ec.error_code_id == errorCodeId); + if (existingEc && existingEc.tempId) { + updateTempId = existingEc.tempId; + } else { + updateTempId = `existing_${errorCodeId}`; + } + } else { + updateTempId = errorCodeId; + } + } else { + updateTempId = Date.now().toString(); + } + + const currentErrorCode = { + tempId: updateTempId, + error_code_id: isEdit && errorCodeId && !errorCodeId.startsWith('pending-') ? errorCodeId : null, + 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 ? errorCodeValues.status : true, + solution: solutionData || [], + spareparts: selectedSparepartIds || [], + errorCodeIcon: errorCodeIcon, + }; + + if (isEdit) { + updateErrorCode(updateTempId, currentErrorCode); + } else { + addErrorCode(currentErrorCode); + } + + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: isEdit ? 'Error Code berhasil diupdate!' : 'Error Code berhasil ditambahkan!', + }); + + navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`); + + } catch (error) { + console.error('Error saving error code:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: 'Gagal menyimpan error code. Silakan coba lagi.', + }); + } + }; + + const handleCancel = () => { + navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`); + }; + + const handleErrorCodeIconUpload = async (file) => { + if (!file) return null; + + try { + const folder = 'images'; + const response = await uploadFile(file, folder); + + if (response && response.statusCode === 200) { + const iconData = { + name: file.name, + uploadPath: response.data.path_document, + url: response.data.path_document, + }; + + setErrorCodeIcon(iconData); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Error code icon uploaded successfully', + }); + return iconData; + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Failed to upload error code icon', + }); + return null; + } + } catch (error) { + console.error('Error uploading icon:', error); + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: 'Failed to upload error code icon', + }); + return null; + } + }; + + const handleErrorCodeIconRemove = () => { + setErrorCodeIcon(null); + }; + + const handleSolutionFileUpload = async (file, solutionKey) => { + if (!file) return null; + + try { + // Determine folder based on file type + const fileExtension = file.name.split('.').pop().toLowerCase(); + const folder = ['pdf'].includes(fileExtension) ? 'pdf' : 'images'; + + const response = await uploadFile(file, folder); + + if (response && response.statusCode === 200) { + const fileData = { + name: file.name, + uploadPath: response.data.path_document, + url: response.data.path_document, + size: file.size, + type: file.type, + }; + + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Solution file uploaded successfully', + }); + return fileData; + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Failed to upload solution file', + }); + return null; + } + } catch (error) { + console.error('Error uploading solution file:', error); + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: 'Failed to upload solution file', + }); + return null; + } + }; + + const handleSolutionFileView = (fileData) => { + if (fileData && fileData.url) { + window.open(fileData.url, '_blank'); + } + }; + + const resetForm = () => { + errorCodeForm.resetFields(); + errorCodeForm.setFieldsValue({ + status: true, + }); + setErrorCodeIcon(null); + resetSolutionFields(); + setSelectedSparepartIds([]); + }; + + return ( + <Card> + {/* Header */} + <div style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 24 + }}> + <Title level={4} style={{ margin: 0 }}> + {isEdit ? 'Edit Error Code' : 'Add Error Code'} + + + + + {/* Content */} +
+ {loading && ( +
+ +
+ )} + + + {/* Error Code Form */} + + +
+ + +
+ + + {/* Solutions Form */} + + +
+ { + // console.log('🔍 checkFirstSolutionValid function:', typeof checkFirstSolutionValid); + return checkFirstSolutionValid(); + }} + onAddSolutionField={handleAddSolutionField} + onRemoveSolutionField={handleRemoveSolutionField} + onSolutionTypeChange={handleSolutionTypeChange} + onSolutionStatusChange={handleSolutionStatusChange} + onSolutionFileUpload={handleSolutionFileUpload} + onFileView={handleSolutionFileView} + fileList={fileList} + isReadOnly={false} + /> + +
+ + + {/* Sparepart Selection */} + + + + + +
+ + {/* Save Button */} +
+ +
+
+ +
+ ); +}; + +export default AddEditErrorCode; \ No newline at end of file diff --git a/src/pages/master/brandDevice/EditBrandDevice.jsx b/src/pages/master/brandDevice/EditBrandDevice.jsx index b69325f..7a79e97 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 } from 'react-router-dom'; +import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom'; import { Divider, Typography, @@ -10,10 +10,16 @@ import { Col, Card, Spin, + Tag, + Space, + Input, } from 'antd'; -import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; +import { EyeOutlined, EditOutlined, DeleteOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'; +import TableList from '../../../components/Global/TableList'; +import { ConfigProvider } from 'antd'; +import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; -import { getBrandById, updateBrand } from '../../../api/master-brand'; +import { getBrandById, updateBrand, getErrorCodesByBrandId } from '../../../api/master-brand'; import { getFileUrl } from '../../../api/file-uploads'; import BrandForm from './component/BrandForm'; import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm'; @@ -22,6 +28,8 @@ import FormActions from './component/FormActions'; import ListErrorCode from './component/ListErrorCode'; import { useSolutionLogic } from './hooks/solution'; import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic'; +import { useBrandForm } from '../../../context/BrandFormContext'; +import SingleSparepartSelect from './component/SingleSparepartSelect'; const { Title } = Typography; const { Step } = Steps; @@ -29,41 +37,53 @@ const { Step } = Steps; 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 [solutionForm] = Form.useForm(); const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [selectedSparepartIds, setSelectedSparepartIds] = useState([]); - const [formData, setFormData] = useState({ - brand_name: '', - brand_type: '', - brand_model: '', - brand_manufacture: '', - is_active: true, - }); + const [loading, setLoading] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); - // Custom hooks for edit mode + // Context integration const { - confirmLoading, - setConfirmLoading, - currentStep, - setCurrentStep, - loading, - setLoading, - errorCodes, - setErrorCodes, - pendingErrorCodes, - setPendingErrorCodes, - editingErrorCodeKey, - setEditingErrorCodeKey, - isErrorCodeFormReadOnly, - setIsErrorCodeFormReadOnly, - handleAddErrorCode, - handleDeleteErrorCode, - } = useBrandDeviceLogic(true, id); + routeBrandId, + setRouteBrandId, + initializeFromRoute, + navigateToErrorCodes, + editErrorCode, + isLoading: contextLoading + } = useBrandForm(); + + // Use step from query parameter or context + 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 [searchValue, setSearchValue] = useState(''); + + const { + brandId, + brandInfo, + setBrandInfo, + setBrandId, + tempErrorCodes, + existingErrorCodes, + addErrorCode, + updateErrorCode, + deleteErrorCode, + setExistingErrorCodes, + prepareSubmissionData, + validateForm, + resetForm, + } = useBrandForm(); - const { solutionFields, solutionTypes, @@ -88,6 +108,8 @@ const EditBrandDevice = () => { return; } + const tab = searchParams.get('tab') || 'brand'; + setBreadcrumbItems([ { title: • Master @@ -117,52 +139,40 @@ const EditBrandDevice = () => { if (response && response.statusCode === 200) { const brandData = response.data; - const newFormData = { + + const brandInfoData = { brand_name: brandData.brand_name, brand_type: brandData.brand_type || '', - brand_model: brandData.brand_model || '', brand_manufacture: brandData.brand_manufacture || '', - is_active: brandData.is_active, + brand_model: brandData.brand_model || '', + is_active: brandData.is_active }; - const existingErrorCodes = brandData.error_code - ? brandData.error_code.map((ec) => ({ - 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 || [], - errorCodeIcon: ec.path_icon - ? { - name: ec.path_icon.split('/').pop(), - 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); + setBrandId(brandData.brand_id); + brandForm.setFieldsValue(brandInfoData); - setFormData(newFormData); - brandForm.setFieldsValue(newFormData); - setErrorCodes(existingErrorCodes); - setPendingErrorCodes(existingErrorCodes); - - if (brandData.spareparts && brandData.spareparts.length > 0) { - const sparepartIds = brandData.spareparts.map(sp => sp.sparepart_id); - setSelectedSparepartIds(sparepartIds); - } else { - setSelectedSparepartIds([]); + if (brandData.brand_id) { + try { + const errorCodesResponse = await getErrorCodesByBrandId(brandId || 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) { + console.error('Error fetching error codes:', error); + } } + + setCurrentStep(tab === 'brand' ? 0 : 1); } else { NotifAlert({ icon: 'error', @@ -182,21 +192,35 @@ const EditBrandDevice = () => { }; fetchBrandData(); - }, [id, setBreadcrumbItems, navigate, brandForm]); + }, [id, navigate]); + + useEffect(() => { + const tab = searchParams.get('tab') || 'brand'; + setCurrentStep(tab === 'brand' ? 0 : 1); + }, [searchParams]); + + // Initialize context with route parameters + useEffect(() => { + if (id) { + initializeFromRoute(id); + setBrandId(id); + } + }, [id, initializeFromRoute, setBrandId]); + + useEffect(() => { + if (currentStep === 1 && brandId) { + setTrigerFilter(prev => !prev); + } + }, [currentStep, brandId]); const handleNextStep = async () => { try { - const currentFormData = await brandForm.validateFields(); - - setFormData({ - brand_name: currentFormData.brand_name, - brand_type: currentFormData.brand_type || '', - brand_model: currentFormData.brand_model || '', - brand_manufacture: currentFormData.brand_manufacture || '', - is_active: currentFormData.is_active, - }); - - setCurrentStep(1); + await brandForm.validateFields(); + const currentBrandId = brandId || id; + if (currentBrandId) { + navigateToErrorCodes(currentBrandId); + navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`); + } } catch (error) { NotifAlert({ icon: 'warning', @@ -211,46 +235,17 @@ const EditBrandDevice = () => { }; const handleFinish = async () => { - const currentFormData = formData; - - if (!currentFormData.brand_name || currentFormData.brand_name.trim() === '' || - !currentFormData.brand_manufacture || currentFormData.brand_manufacture.trim() === '') { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Harap lengkapi semua field wajib diisi (Brand Name dan Manufacturer)!', - }); + const validation = validateForm(); + if (!validation.isValid) { return; } setConfirmLoading(true); try { - const brandUpdateData = { - brand_name: currentFormData.brand_name, - brand_type: currentFormData.brand_type || '', - brand_model: currentFormData.brand_model || '', - brand_manufacture: currentFormData.brand_manufacture || '', - is_active: currentFormData.is_active, - spareparts: selectedSparepartIds, - error_code: pendingErrorCodes.length > 0 ? pendingErrorCodes.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, - what_action_to_take: ec.what_action_to_take || '', - 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 - })) - })) : [] - }; + const userId = JSON.parse(localStorage.getItem('user') || '{}').user_id || 1; + const submissionData = prepareSubmissionData(userId); - const response = await updateBrand(id, brandUpdateData); + const response = await updateBrand(id, submissionData); if (response && (response.statusCode === 200 || response.statusCode === 201)) { NotifOk({ @@ -258,6 +253,7 @@ const EditBrandDevice = () => { title: 'Berhasil', message: response.message || 'Brand Device dan Error Codes berhasil diupdate.', }); + resetForm(); navigate('/master/brand-device'); } else { NotifAlert({ @@ -294,22 +290,18 @@ const EditBrandDevice = () => { resetSolutionFields(); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(null); + setSelectedSparepartIds([]); }; const handleCreateNewErrorCode = () => { resetErrorCodeForm(); - resetSolutionFields(); - setIsErrorCodeFormReadOnly(false); - setEditingErrorCodeKey(null); }; - // Local wrapper for handleAddErrorCode from useBrandDeviceLogic - const handleAddErrorCodeLocal = async () => { + const handleSaveErrorCode = async () => { try { const errorCodeValues = await errorCodeForm.validateFields(); const solutionData = getSolutionData(); - // Validate error code fields if (!errorCodeValues.error_code || !errorCodeValues.error_code_name) { NotifAlert({ icon: 'warning', @@ -319,7 +311,6 @@ const EditBrandDevice = () => { return; } - // Validate solution data if (!solutionData || solutionData.length === 0) { NotifAlert({ icon: 'warning', @@ -329,17 +320,6 @@ const EditBrandDevice = () => { return; } - // Validate each solution has name - const invalidSolution = solutionData.find(sol => !sol.solution_name || sol.solution_name.trim() === ''); - if (invalidSolution) { - NotifAlert({ - icon: 'warning', - title: 'Perhatian', - message: 'Setiap solution harus memiliki nama!', - }); - return; - } - const newErrorCode = { error_code: errorCodeValues.error_code, error_code_name: errorCodeValues.error_code_name, @@ -348,29 +328,18 @@ const EditBrandDevice = () => { path_icon: errorCodeIcon?.uploadPath || '', is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status, solution: solutionData, - errorCodeIcon: errorCodeIcon, - key: editingErrorCodeKey || `temp-${Date.now()}`, + spareparts: selectedSparepartIds }; - let updatedPendingErrorCodes; if (editingErrorCodeKey) { - updatedPendingErrorCodes = pendingErrorCodes.map((item) => { - if (item.key === editingErrorCodeKey) { - return { - ...item, - ...newErrorCode, - error_code_id: item.error_code_id || newErrorCode.error_code_id, - }; - } - return item; - }); + updateErrorCode(editingErrorCodeKey, newErrorCode); NotifOk({ icon: 'success', title: 'Berhasil', message: 'Error code berhasil diupdate!', }); } else { - updatedPendingErrorCodes = [...pendingErrorCodes, newErrorCode]; + addErrorCode(newErrorCode); NotifOk({ icon: 'success', title: 'Berhasil', @@ -378,183 +347,452 @@ const EditBrandDevice = () => { }); } - setPendingErrorCodes(updatedPendingErrorCodes); - setErrorCodes(updatedPendingErrorCodes); - - setTimeout(() => { - resetErrorCodeForm(); - }, 100); + resetErrorCodeForm(); } catch (error) { NotifAlert({ icon: 'warning', title: 'Perhatian', - message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!', + message: 'Harap isi semua kolom wajib!', }); } }; 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); + const errorCode = getErrorCodeById(record.tempId || record.error_code_id); + if (errorCode) { + errorCodeForm.setFieldsValue({ + 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, + status: errorCode.is_active, + }); + setErrorCodeIcon(errorCode.path_icon ? { + name: errorCode.path_icon.split('/').pop(), + uploadPath: errorCode.path_icon, + } : null); + setIsErrorCodeFormReadOnly(true); + setEditingErrorCodeKey(errorCode.tempId); - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); - } else { - resetSolutionFields(); + if (errorCode.solution && errorCode.solution.length > 0) { + setSolutionsForExistingRecord(errorCode.solution, solutionForm); + } else { + resetSolutionFields(); + } + + if (errorCode.spareparts && errorCode.spareparts.length > 0) { + setSelectedSparepartIds(errorCode.spareparts); + } } }; 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); + const errorCode = getErrorCodeById(record.tempId || record.error_code_id); + if (errorCode) { + errorCodeForm.setFieldsValue({ + 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, + status: errorCode.is_active, + }); + setErrorCodeIcon(errorCode.path_icon ? { + name: errorCode.path_icon.split('/').pop(), + uploadPath: errorCode.path_icon, + } : null); + setIsErrorCodeFormReadOnly(false); + setEditingErrorCodeKey(errorCode.tempId); - if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, solutionForm); + if (errorCode.solution && errorCode.solution.length > 0) { + setSolutionsForExistingRecord(errorCode.solution, solutionForm); + } + + if (errorCode.spareparts && errorCode.spareparts.length > 0) { + setSelectedSparepartIds(errorCode.spareparts); + } + } + }; + + const handleEditErrorCodeNavigate = (record) => { + const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId; + const currentBrandId = brandId || id; + if (errorCodeId && currentBrandId) { + navigate(`/master/brand-device/${currentBrandId}/error-code/edit/${errorCodeId}`); + } + }; + + const handleDeleteErrorCode = (record) => { + NotifConfirmDialog({ + icon: 'question', + title: 'Konfirmasi Hapus', + message: `Apakah Anda yakin ingin menghapus error code "${record.error_code}"?`, + onConfirm: () => { + // Use soft delete (mark as deleted) instead of permanent delete + const tempId = record.tempId || `existing_${record.error_code_id}`; + deleteErrorCode(tempId, false); // false = soft delete + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Error code berhasil dihapus!', + }); + setTrigerFilter(prev => !prev); + }, + onCancel: () => {} + }); + }; + + const mergedErrorCodes = useMemo(() => { + const allErrorCodes = [ + ...existingErrorCodes.map(ec => ({ ...ec, status: 'existing' })), + ...tempErrorCodes + ]; + + if (searchText) { + return allErrorCodes.filter(ec => + ec.error_code.toLowerCase().includes(searchText.toLowerCase()) || + ec.error_code_name.toLowerCase().includes(searchText.toLowerCase()) + ); } - const formElement = document.querySelector('.ant-form'); - if (formElement) { - formElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + return allErrorCodes; + }, [existingErrorCodes, tempErrorCodes, searchText]); + + const errorCodeColumns = (showPreviewModal, showEditModal, showDeleteDialog) => [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Error Code', + dataIndex: 'error_code', + key: 'error_code', + width: '20%', + render: (text, record) => ( + + {text} + {record.status === 'new' && New} + {record.status === 'modified' && Modified} + + ), + }, + { + title: 'Error Name', + dataIndex: 'error_code_name', + key: 'error_code_name', + width: '25%', + }, + { + title: 'Description', + dataIndex: 'error_code_description', + key: 'error_code_description', + width: '25%', + render: (text) => text || '-', + }, + { + title: 'Solutions', + dataIndex: 'solution', + key: 'solutions', + width: '10%', + align: 'center', + render: (solutions) => { + const count = Array.isArray(solutions) ? solutions.length : 0; + return count; + }, + }, + { + title: 'Status', + dataIndex: 'is_active', + key: 'is_active', + width: '10%', + align: 'center', + render: (_, { is_active }) => ( + + {is_active ? 'Active' : 'Inactive'} + + ), + }, + { + title: 'Action', + key: 'action', + align: 'center', + width: '15%', + render: (_, record) => ( + + - - + onSearch={handleSearch} + allowClear={{ + clearIcon: , + }} + enterButton={ + + } + size="large" + /> + + + + + + + + + + + + - + ); } return null; @@ -569,47 +807,50 @@ const EditBrandDevice = () => { -
- {loading && ( -
- -
- )} -
- {renderStepContent()} + {renderStepContent()} + +
+
+ + {currentStep === 1 && ( + + )} +
+
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )}
- - setCurrentStep(currentStep - 1)} - onNextStep={handleNextStep} - onSave={handleFinish} - onCancel={handleCancel} - confirmLoading={confirmLoading} - isEditMode={true} - /> ); }; diff --git a/src/pages/master/brandDevice/component/BrandForm.jsx b/src/pages/master/brandDevice/component/BrandForm.jsx index 129cc50..c88bc75 100644 --- a/src/pages/master/brandDevice/component/BrandForm.jsx +++ b/src/pages/master/brandDevice/component/BrandForm.jsx @@ -1,21 +1,14 @@ -import React, { useState } from 'react'; -import { Form, Input, Row, Col, Typography, Switch, Button, Card, Divider } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; -import SingleSparepartSelect from './SingleSparepartSelect'; +import React from 'react'; +import { Form, Input, Row, Col, Typography, Switch } from 'antd'; const { Text } = Typography; const BrandForm = ({ form, - formData, onValuesChange, isEdit = false, - selectedSparepartIds = [], - onSparepartChange, - showSparepartSection = false }) => { - const isActive = Form.useWatch('is_active', form) ?? formData.is_active ?? true; - const [showSparepart, setShowSparepart] = useState(showSparepartSection); + const isActive = Form.useWatch('is_active', form) ?? true; return (
@@ -23,7 +16,13 @@ const BrandForm = ({ layout="vertical" form={form} onValuesChange={onValuesChange} - initialValues={formData} + initialValues={{ + brand_name: '', + brand_type: '', + brand_model: '', + brand_manufacture: '', + is_active: true, + }} >
@@ -83,39 +82,6 @@ const BrandForm = ({ - - - - {/* Add Sparepart Button */} -
- -
- - {/* Sparepart Selection Section */} - {showSparepart && ( - - - - )}
); }; diff --git a/src/pages/master/brandDevice/component/CustomSparepartCard.jsx b/src/pages/master/brandDevice/component/CustomSparepartCard.jsx new file mode 100644 index 0000000..f3c10b5 --- /dev/null +++ b/src/pages/master/brandDevice/component/CustomSparepartCard.jsx @@ -0,0 +1,483 @@ +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); + + // Construct image source with proper fallback + 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 handleCardClick = () => { + if (!isReadOnly && onCardClick) { + onCardClick(sparepart); + } + }; + + const getCardActions = () => { + const actions = []; + + // Preview button + if (showPreview) { + actions.push( + + ]} + width={700} + centered + bodyStyle={{ padding: '24px' }} + > + + +
+
+ {sparepart.sparepart_name { + e.target.src = 'https://via.placeholder.com/200x200/d9d9d9/666666?text=No+Image'; + }} + /> +
+ + {sparepart.sparepart_item_type && ( + + {sparepart.sparepart_item_type} + + )} +
+ + + +
+ + {sparepart.sparepart_name || 'Unnamed'} + + + + +
+ + Code: + + + {sparepart.sparepart_code || 'N/A'} + +
+ + +
+ + Status: + + + {sparepart.is_active ? 'Active' : 'Inactive'} + +
+ +
+ + {sparepart.sparepart_description && ( +
+ + Description: + + + {sparepart.sparepart_description} + +
+ )} + +
+ + Stock: + + + {sparepart.sparepart_stock || sparepart.sparepart_stok || '0'} + {sparepart.sparepart_unit ? ` ${sparepart.sparepart_unit}` : ' units'} + +
+ + + {sparepart.sparepart_merk && ( + +
+ + Brand: + + + {sparepart.sparepart_merk} + +
+ + )} + {sparepart.sparepart_model && ( + +
+ + Model: + + + {sparepart.sparepart_model} + +
+ + )} + {sparepart.sparepart_unit && ( + +
+ + Unit: + + + {sparepart.sparepart_unit} + +
+ + )} +
+ + {sparepart.updated_at && ( +
+ + Last updated: {dayjs(sparepart.updated_at).format('DD MMMM YYYY, HH:mm')} + +
+ )} +
+ +
+ + + ); +}; + +export default CustomSparepartCard; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/ErrorCodeTable.jsx b/src/pages/master/brandDevice/component/ErrorCodeTable.jsx new file mode 100644 index 0000000..b75e327 --- /dev/null +++ b/src/pages/master/brandDevice/component/ErrorCodeTable.jsx @@ -0,0 +1,297 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag, Spin, Modal, Form, Typography } from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + SearchOutlined, + EyeOutlined, + SolutionOutlined, + ToolOutlined, +} from '@ant-design/icons'; +import { NotifAlert, NotifConfirmDialog, NotifOk } from '../../../../components/Global/ToastNotif'; +import TableList from '../../../../components/Global/TableList'; + +const { Title } = Typography; + +const columns = (onView, onEdit, onDelete) => [ + { + title: 'No', + key: 'no', + width: '5%', + align: 'center', + render: (_, __, index) => index + 1, + }, + { + title: 'Error Code', + dataIndex: 'error_code', + key: 'error_code', + width: '15%', + render: (text, record) => ( +
+ {record.path_icon && ( + icon + )} + + {text} + +
+ ), + }, + { + title: 'Error Name', + dataIndex: 'error_code_name', + key: 'error_code_name', + width: '25%', + }, + { + title: 'Description', + dataIndex: 'error_code_description', + key: 'error_code_description', + width: '20%', + render: (text) => text || '-', + ellipsis: true, + }, + { + title: 'Solutions', + key: 'solutions_count', + width: '10%', + align: 'center', + render: (_, record) => ( + + {record.solution ? record.solution.length : 0} Solutions + + ), + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + width: '12%', + align: 'center', + render: (_, record) => { + const statusColor = record.is_active ? 'green' : 'red'; + const statusText = record.is_active ? 'Active' : 'Inactive'; + + // Show modification status if applicable + if (record.status === 'new') { + return ( + + NEW + {statusText} + + ); + } else if (record.status === 'modified') { + return ( + + MODIFIED + {statusText} + + ); + } else if (record.status === 'deleted') { + return ( + + DELETED + Inactive + + ); + } else { + return ( + + {statusText} + + ); + } + }, + }, + { + title: 'Action', + key: 'action', + align: 'center', + width: '15%', + render: (_, record) => ( + + + } + size="large" + style={{ marginBottom: 16 }} + /> + + + + + + + + + + + + + + ); +}); + +export default ErrorCodeTable; \ 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 4c80945..1705c0c 100644 --- a/src/pages/master/brandDevice/component/ListBrandDevice.jsx +++ b/src/pages/master/brandDevice/component/ListBrandDevice.jsx @@ -250,7 +250,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) { }} size="large" > - Add Brand Device + Add data diff --git a/src/pages/master/brandDevice/component/SingleSparepartSelect.jsx b/src/pages/master/brandDevice/component/SingleSparepartSelect.jsx index 42037b1..5bb6971 100644 --- a/src/pages/master/brandDevice/component/SingleSparepartSelect.jsx +++ b/src/pages/master/brandDevice/component/SingleSparepartSelect.jsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { Select, Card, Typography, Tag, Spin, Empty, Button, Image, Row, Col, Modal } from 'antd'; +import { Select, Typography, Tag, Spin, Empty, Button, Row, Col } from 'antd'; import { PlusOutlined, DeleteOutlined, CheckOutlined, EyeOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { getAllSparepart } from '../../../../api/sparepart'; -import dayjs from 'dayjs'; +import CustomSparepartCard from './CustomSparepartCard'; const { Text, Title } = Typography; const { Option } = Select; @@ -16,7 +16,6 @@ const SingleSparepartSelect = ({ const [loading, setLoading] = useState(false); const [selectedSpareparts, setSelectedSpareparts] = useState([]); const [dropdownOpen, setDropdownOpen] = useState(false); - const [previewModal, setPreviewModal] = useState({ visible: false, sparepart: null }); useEffect(() => { fetchSpareparts(); @@ -102,219 +101,23 @@ const SingleSparepartSelect = ({ onSparepartChange(newSelectedIds); }; - const handlePreviewSparepart = (sparepart) => { - setPreviewModal({ visible: true, sparepart }); - }; - - const handlePreviewModalClose = () => { - setPreviewModal({ visible: false, sparepart: null }); - }; - const renderSparepartCard = (sparepart, isSelected = false) => { const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id); - let imgSrc; - if (sparepart.sparepart_foto) { - if (sparepart.sparepart_foto.startsWith('http')) { - imgSrc = sparepart.sparepart_foto; - } else { - const fileName = sparepart.sparepart_foto.split('/').pop(); - if (fileName === 'defaultSparepartImg.jpg') { - imgSrc = `/assets/defaultSparepartImg.jpg`; - } else { - const token = localStorage.getItem('token'); - const baseURL = import.meta.env.VITE_API_SERVER || ''; - imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`; - } - } - } else { - imgSrc = 'https://via.placeholder.com/150'; - } - return ( - - + handleSparepartSelect(sparepart.sparepart_id) : undefined} + onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)} style={{ - borderRadius: '8px', - overflow: 'hidden', - border: isSelected ? '2px solid #1890ff' : isAlreadySelected ? '2px solid #52c41a' : '1px solid #E0E0E0', + border: isAlreadySelected ? '2px solid #52c41a' : undefined, }} - bodyStyle={{ padding: 0 }} - onClick={!isSelected && !isReadOnly && !isAlreadySelected ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined} - actions={[ - // Preview action (selalu available) - { - e.stopPropagation(); - handlePreviewSparepart(sparepart); - }} - />, - // Delete action (hanya untuk selected items) - isSelected && !isReadOnly && ( - { - e.stopPropagation(); - handleRemoveSparepart(sparepart.sparepart_id); - }} - /> - ), - ].filter(Boolean)} - > - - -
- {sparepart.sparepart_item_type && ( - - {sparepart.sparepart_item_type} - - )} -
-
- {sparepart.sparepart_name { - e.target.src = 'https://via.placeholder.com/150'; - }} - /> -
- {isAlreadySelected && ( -
- -
- )} -
-
- - -
- {/* Title dengan proper hierarchy */} - - {sparepart.sparepart_name || sparepart.name || 'Unnamed'} - - - {/* Stock Information */} - - Available Stock: {sparepart.sparepart_stock || '0'} - - -
- - {/* Code */} -
- - {sparepart.sparepart_code || 'No code'} - -
- - {/* Brand/Model/Unit Information */} - {(sparepart.sparepart_merk || sparepart.sparepart_model || sparepart.sparepart_unit) && ( -
- {sparepart.sparepart_merk && ( -
Brand: {sparepart.sparepart_merk}
- )} - {sparepart.sparepart_model && ( -
Model: {sparepart.sparepart_model}
- )} - {sparepart.sparepart_unit && ( -
Unit: {sparepart.sparepart_unit}
- )} -
- )} - - {/* Last Updated */} - {sparepart.updated_at && ( - - Last updated: {dayjs(sparepart.updated_at).format('DD MMM YYYY')} - - )} -
- - - + /> ); }; @@ -360,7 +163,7 @@ const SingleSparepartSelect = ({ Selected Spareparts ({selectedSpareparts.length}) - + {selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
@@ -373,175 +176,6 @@ const SingleSparepartSelect = ({ )}
- - {/* Preview Modal */} - - Close - - ]} - width={800} - centered - bodyStyle={{ padding: '24px' }} - > - {previewModal.sparepart && ( - - -
-
- {previewModal.sparepart.sparepart_foto ? ( - {previewModal.sparepart.sparepart_name { - e.target.src = 'https://via.placeholder.com/200x200/d9d9d9/666666?text=No+Image'; - }} - /> - ) : ( -
- No Image -
- )} -
- - {previewModal.sparepart.sparepart_item_type && ( - - {previewModal.sparepart.sparepart_item_type} - - )} -
- - - -
- - {previewModal.sparepart.sparepart_name || 'Unnamed'} - - -
- - Code: - - - {previewModal.sparepart.sparepart_code || 'N/A'} - -
- - {previewModal.sparepart.sparepart_description && ( -
- - Description: - - - {previewModal.sparepart.sparepart_description} - -
- )} - -
- - Status: - - - {previewModal.sparepart.is_active ? 'Active' : 'Inactive'} - -
- -
- - Stock: - - - {previewModal.sparepart.sparepart_stock || '0'} - {previewModal.sparepart_unit ? ` ${previewModal.sparepart.unit}` : ' units'} - -
- - - {previewModal.sparepart.sparepart_merk && ( - -
- - Brand: - - - {previewModal.sparepart.sparepart_merk} - -
- - )} - {previewModal.sparepart.sparepart_model && ( - -
- - Model: - - - {previewModal.sparepart.sparepart_model} - -
- - )} - {previewModal.sparepart.sparepart_unit && ( - -
- - Unit: - - - {previewModal.sparepart.sparepart_unit} - -
- - )} -
- - {previewModal.sparepart.updated_at && ( -
- - Last updated: {dayjs(previewModal.sparepart.updated_at).format('DD MMMM YYYY, HH:mm')} - -
- )} -
- -
- )} -
); }; diff --git a/src/pages/master/brandDevice/component/SolutionForm.jsx b/src/pages/master/brandDevice/component/SolutionForm.jsx index 1e7734e..f6a7a9f 100644 --- a/src/pages/master/brandDevice/component/SolutionForm.jsx +++ b/src/pages/master/brandDevice/component/SolutionForm.jsx @@ -10,18 +10,31 @@ const SolutionForm = ({ solutionFields, solutionTypes, solutionStatuses, - fileList, - solutionsToDelete, firstSolutionValid, onAddSolutionField, onRemoveSolutionField, onSolutionTypeChange, onSolutionStatusChange, + checkFirstSolutionValid, onSolutionFileUpload, onFileView, + fileList, isReadOnly = false, - onAddSolution, }) => { + // Debug props + console.log('🔍 SolutionForm props:', { + solutionFields, + solutionTypes, + solutionStatuses, + firstSolutionValid, + onAddSolutionField: typeof onAddSolutionField, + onRemoveSolutionField: typeof onRemoveSolutionField, + checkFirstSolutionValid: typeof checkFirstSolutionValid, + onSolutionFileUpload: typeof onSolutionFileUpload, + onFileView: typeof onFileView, + fileList: fileList ? fileList.length : 0 + }); + return (
- {solutionFields.map((field, index) => ( + {solutionFields.map((field) => ( onRemoveSolutionField(field.key)} + onRemove={() => onRemoveSolutionField(field)} onFileUpload={onSolutionFileUpload} onFileView={onFileView} fileList={fileList} diff --git a/src/pages/master/brandDevice/component/SparepartCardSelect.jsx b/src/pages/master/brandDevice/component/SparepartCardSelect.jsx index 43c0827..d95f84d 100644 --- a/src/pages/master/brandDevice/component/SparepartCardSelect.jsx +++ b/src/pages/master/brandDevice/component/SparepartCardSelect.jsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Card, Row, Col, Image, Typography, Tag, Space, Spin, Button, Empty, message } from 'antd'; -import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons'; +import { Card, Row, Col, Typography, Tag, Space, Spin, Button, Empty, message } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; import { getAllSparepart } from '../../../../api/sparepart'; import { addSparepartToBrand, removeSparepartFromBrand } from '../../../../api/master-brand'; +import CustomSparepartCard from './CustomSparepartCard'; const { Text, Title } = Typography; @@ -196,121 +197,27 @@ const SparepartCardSelect = ({ ) : ( {filteredSpareparts.map(sparepart => ( - - + handleSparepartToggle(sparepart.sparepart_id)} + onDelete={() => { + // When delete button is clicked, remove from selection + const newSelectedIds = selectedSparepartIds.filter(id => id !== sparepart.sparepart_id); + onSparepartChange(newSelectedIds); + + // Also remove from database if brandId exists + if (brandId) { + removeSparepartFromBrand(brandId, sparepart.sparepart_id) + .then(() => message.success('Sparepart removed successfully')) + .catch(error => message.error(error.message || 'Failed to remove sparepart')); + } }} - onClick={() => handleSparepartToggle(sparepart.sparepart_id)} - > -
- {isSelected(sparepart.sparepart_id) ? ( - - ) : ( - - )} -
- -
-
- {sparepart.sparepart_foto ? ( - {sparepart.sparepart_name} - ) : ( -
- No Image -
- )} -
-
- -
- - {sparepart.sparepart_name} - - - - {sparepart.sparepart_description || 'No description'} - - - - - {sparepart.sparepart_code} - - - {sparepart.sparepart_merk || 'N/A'} - - - - {sparepart.sparepart_model && ( -
- Model: {sparepart.sparepart_model} -
- )} -
-
+ /> ))}
diff --git a/src/pages/master/brandDevice/component/SparepartForm.jsx b/src/pages/master/brandDevice/component/SparepartForm.jsx index 67bd33f..dea4344 100644 --- a/src/pages/master/brandDevice/component/SparepartForm.jsx +++ b/src/pages/master/brandDevice/component/SparepartForm.jsx @@ -11,8 +11,8 @@ const SparepartForm = ({ isReadOnly = false }) => { return ( -
- +
+ { - const [solutionFields, setSolutionFields] = useState([ - { name: ['solution_items', 0], key: 0 } - ]); + const [solutionFields, setSolutionFields] = useState([0]); const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' }); const [solutionStatuses, setSolutionStatuses] = useState({ 0: true }); const [solutionsToDelete, setSolutionsToDelete] = useState([]); const handleAddSolutionField = () => { - const newKey = Date.now(); // Use timestamp for unique key - const newField = { name: ['solution_items', newKey], key: newKey }; + const newKey = Date.now(); - setSolutionFields(prev => [...prev, newField]); + setSolutionFields(prev => [...prev, newKey]); setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' })); setSolutionStatuses(prev => ({ ...prev, [newKey]: true })); @@ -27,7 +24,7 @@ export const useSolutionLogic = (solutionForm) => { const handleRemoveSolutionField = (key) => { if (solutionFields.length <= 1) { - return; // Keep at least one solution field + return; } setSolutionFields(prev => prev.filter(field => field.key !== key)); @@ -67,13 +64,21 @@ export const useSolutionLogic = (solutionForm) => { const checkFirstSolutionValid = () => { const values = solutionForm.getFieldsValue(); - const firstSolution = values.solution_items?.[0]; + + const firstField = solutionFields[0]; + if (!firstField) { + return false; + } + + const solutionKey = firstField.key || firstField; + const solutionPath = `solution_items,${solutionKey}`; + const firstSolution = values[solutionPath]; if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') { return false; } - if (solutionTypes[0] === 'text' && (!firstSolution.text || firstSolution.text.trim() === '')) { + if (solutionTypes[solutionKey] === 'text' && (!firstSolution.text || firstSolution.text.trim() === '')) { return false; } @@ -83,13 +88,12 @@ export const useSolutionLogic = (solutionForm) => { const getSolutionData = () => { const values = solutionForm.getFieldsValue(); - const result = solutionFields.map(field => { - const key = field.key; - // Access form values using the key from field.name (AntD stores with comma) - const solutionPath = field.name.join(','); - const solution = values[solutionPath]; + const result = solutionFields.map(key => { + const solution = values[`solution_items,${key}`]; - const validSolution = solution && solution.name && solution.name.trim() !== ''; + if (!solution) return null; + + const validSolution = solution.name && solution.name.trim() !== ''; if (validSolution) { return { @@ -97,7 +101,7 @@ export const useSolutionLogic = (solutionForm) => { type_solution: solutionTypes[key] || 'text', text_solution: solution.text || '', path_solution: solution.file || '', - is_active: solution.status !== false, // Use form value directly + is_active: solution.status !== false, }; } return null; @@ -109,14 +113,10 @@ export const useSolutionLogic = (solutionForm) => { const setSolutionsForExistingRecord = (solutions, form) => { if (!solutions || solutions.length === 0) return; - const newFields = solutions.map((solution, index) => ({ - name: ['solution_items', solution.id || index], - key: solution.id || index - })); + const newFields = solutions.map((solution, index) => solution.id || index); setSolutionFields(newFields); - // Set solution values const solutionsValues = {}; const newTypes = {}; const newStatuses = {};