repair: add edit brand device

This commit is contained in:
2025-12-08 16:45:49 +07:00
parent 03be3a6a99
commit 5703ff0e8d
16 changed files with 1194 additions and 2198 deletions

View File

@@ -4,7 +4,6 @@ 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';
@@ -97,33 +96,26 @@ const App = () => {
<Route path="shift" element={<IndexShift />} />
<Route path="status" element={<IndexStatus />} />
{/* Brand Device Routes with BrandFormProvider */}
<Route path="brand-device/*" element={
<BrandFormProvider>
<Routes>
<Route path="" element={<IndexBrandDevice />} />
<Route path="add" element={<AddBrandDevice />} />
<Route path="edit/:id" element={<EditBrandDevice />} />
<Route path="view/:id" element={<ViewBrandDevice />} />
<Route
path="edit/:id/files/:fileType/:fileName"
element={<ViewFilePage />}
/>
<Route
path="view/:id/files/:fileType/:fileName"
element={<ViewFilePage />}
/>
<Route
path="view/temp/files/:fileName"
element={<ViewFilePage />}
/>
<Route path=":brandId/error-code/add" element={<AddEditErrorCode />} />
<Route path=":brandId/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
<Route path="add/error-code/add" element={<AddEditErrorCode />} />
<Route path="add/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
</Routes>
</BrandFormProvider>
} />
{/* Brand Device Routes */}
<Route path="brand-device" element={<IndexBrandDevice />} />
<Route path="brand-device/add" element={<AddBrandDevice />} />
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
<Route path="brand-device/view/:id" element={<ViewBrandDevice />} />
<Route
path="brand-device/edit/:id/files/:fileType/:fileName"
element={<ViewFilePage />}
/>
<Route
path="brand-device/view/:id/files/:fileType/:fileName"
element={<ViewFilePage />}
/>
<Route
path="brand-device/view/temp/files/:fileName"
element={<ViewFilePage />}
/>
<Route path="brand-device/:brandId/error-code/add" element={<AddEditErrorCode />} />
<Route path="brand-device/:brandId/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
<Route path="brand-device/add/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
</Route>
<Route path="/report" element={<ProtectedRoute />}>

View File

@@ -66,4 +66,44 @@ const getErrorCodeById = async (id) => {
return response.data;
};
export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand, getErrorCodesByBrandId, getErrorCodeById };
const createErrorCode = async (brandId, queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `error-code/brand/${brandId}`,
params: queryParams,
});
return response.data;
};
const updateErrorCode = async (brandId, errorCodeId, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `error-code/brand/${brandId}/${errorCodeId}`,
params: queryParams,
});
return response.data;
};
const deleteErrorCode = async (brandId, errorCode) => {
const response = await SendRequest({
method: 'delete',
prefix: `error-code/brand/${brandId}/${errorCode}`,
});
return response.data;
};
export {
getAllBrands,
getBrandById,
createBrand,
updateBrand,
deleteBrand,
getErrorCodesByBrandId,
getErrorCodeById,
createErrorCode,
updateErrorCode,
deleteErrorCode
};

View File

@@ -1,624 +0,0 @@
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 (
<BrandFormContext.Provider value={value}>
{children}
</BrandFormContext.Provider>
);
};
export const useBrandForm = () => {
const context = useContext(BrandFormContext);
if (!context) {
throw new Error('useBrandForm must be used within a BrandFormProvider');
}
return context;
};
export default BrandFormContext;

View File

@@ -19,10 +19,9 @@ 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 { createBrand, createErrorCode, getErrorCodesByBrandId, updateErrorCode, deleteErrorCode } from '../../../api/master-brand';
import BrandForm from './component/BrandForm';
import { useSolutionLogic } from './hooks/solution';
import { useBrandForm } from '../../../context/BrandFormContext';
const { Title } = Typography;
const { Step } = Steps;
@@ -32,32 +31,24 @@ const AddBrandDevice = () => {
const [searchParams] = useSearchParams();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandForm] = Form.useForm();
const [solutionForm] = Form.useForm();
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [loading, setLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [searchText, setSearchText] = useState('');
const [trigerFilter, setTrigerFilter] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [createdBrandId, setCreatedBrandId] = useState(null);
const [brandData, setBrandData] = useState({});
const [tempErrorCodes, setTempErrorCodes] = useState([]);
// 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);
// Error code management states
const [errorCodeForm] = Form.useForm();
const [solutionForm] = Form.useForm();
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const [isAddingNewErrorCode, setIsAddingNewErrorCode] = useState(false);
const {
solutionFields,
@@ -77,14 +68,54 @@ const AddBrandDevice = () => {
// Navigation functions
const handleNextStep = async () => {
try {
await brandForm.validateFields();
setCurrentStep(1);
setConfirmLoading(true);
const brandValues = await brandForm.validateFields();
const userId = JSON.parse(localStorage.getItem('user') || '{}').user_id || 1;
// Prepare brand data for API
const brandApiData = {
brand_name: brandValues.brand_name,
brand_type: brandValues.brand_type || '',
brand_manufacture: brandValues.brand_manufacture || '',
brand_model: brandValues.brand_model || '',
is_active: brandValues.is_active !== undefined ? brandValues.is_active : true
};
// Create brand via API
const response = await createBrand(brandApiData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const createdBrand = response.data;
setCreatedBrandId(createdBrand.brand_id);
setBrandData(createdBrand);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Brand device berhasil dibuat. Silakan tambahkan error codes.',
});
setCurrentStep(1);
// Trigger refresh untuk error codes table di fase 2
setTimeout(() => {
setTrigerFilter(prev => !prev);
}, 100);
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal membuat brand device',
});
}
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal membuat brand device',
});
} finally {
setConfirmLoading(false);
}
};
@@ -97,13 +128,50 @@ const AddBrandDevice = () => {
};
const handleAddErrorCode = () => {
navigate(`/master/brand-device/add/error-code/add`);
if (createdBrandId) {
resetErrorCodeForm();
setIsAddingNewErrorCode(true);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
} else {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Brand device harus dibuat terlebih dahulu.',
});
}
};
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 handleEditErrorCode = (record) => {
if (createdBrandId) {
setIsAddingNewErrorCode(false);
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(record.error_code_id);
// Load error code data into form
errorCodeForm.setFieldsValue({
error_code: record.error_code,
error_code_name: record.error_code_name || '',
error_code_description: record.error_code_description || '',
error_code_color: record.error_code_color || '#000000',
status: record.is_active !== false,
});
if (record.path_icon) {
setErrorCodeIcon({
name: record.path_icon.split('/').pop(),
uploadPath: record.path_icon,
url: record.path_icon,
});
}
if (record.solution && record.solution.length > 0) {
setSolutionsForExistingRecord(record.solution, solutionForm);
}
if (record.spareparts && record.spareparts.length > 0) {
setSelectedSparepartIds(record.spareparts);
}
}
};
@@ -112,15 +180,33 @@ const AddBrandDevice = () => {
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);
onConfirm: async () => {
try {
const errorCodeToDelete = record.error_code_id;
const response = await deleteErrorCode(createdBrandId, errorCodeToDelete);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!',
});
setTrigerFilter(prev => !prev);
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menghapus error code',
});
}
} catch (error) {
console.error('Error deleting error code:', error);
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menghapus error code',
});
}
},
onCancel: () => {}
});
@@ -142,8 +228,8 @@ const AddBrandDevice = () => {
};
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
setBrandInfo(allValues);
}, [setBrandInfo]);
setBrandData(allValues);
}, []);
const getErrorCodesData = async (params) => {
try {
@@ -151,12 +237,33 @@ const AddBrandDevice = () => {
const page = parseInt(params.get('page')) || 1;
const limit = parseInt(params.get('limit')) || 10;
const allErrorCodes = tempErrorCodes.filter(ec => ec.status !== 'deleted');
let allErrorCodes = [];
let filteredData = allErrorCodes;
// Get error codes from API if brand is created
if (createdBrandId) {
const queryParams = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
...(search && { search })
});
const response = await getErrorCodesByBrandId(createdBrandId, queryParams);
if (response && response.statusCode === 200) {
const apiErrorData = response.data || [];
allErrorCodes = apiErrorData.map(ec => ({
...ec,
tempId: `existing_${ec.error_code_id}`,
status: 'existing'
}));
}
}
// Add temp error codes
allErrorCodes = [...allErrorCodes, ...tempErrorCodes.filter(ec => ec.status !== 'deleted')];
// Filter by search text if needed
if (searchText) {
filteredData = allErrorCodes.filter(ec =>
allErrorCodes = allErrorCodes.filter(ec =>
ec.error_code.toLowerCase().includes(searchText.toLowerCase()) ||
ec.error_code_name.toLowerCase().includes(searchText.toLowerCase())
);
@@ -164,15 +271,15 @@ const AddBrandDevice = () => {
const startIndex = 0;
const endIndex = startIndex + limit;
const paginatedData = filteredData.slice(startIndex, endIndex);
const paginatedData = allErrorCodes.slice(startIndex, endIndex);
return {
data: paginatedData,
pagination: {
current_page: page,
current_limit: limit,
total_limit: filteredData.length,
total_page: Math.ceil(filteredData.length / limit),
total_limit: allErrorCodes.length,
total_page: Math.ceil(allErrorCodes.length / limit),
}
};
} catch (error) {
@@ -264,39 +371,120 @@ const AddBrandDevice = () => {
return params;
}, [searchValue]);
const handleFinish = async () => {
setConfirmLoading(true);
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
status: true,
});
setErrorCodeIcon(null);
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
setSelectedSparepartIds([]);
setIsAddingNewErrorCode(false);
};
const handleSaveErrorCode = async () => {
try {
// Validate form using context
const validation = validateForm();
if (!validation.isValid) {
await errorCodeForm.validateFields();
const solutionValues = solutionForm.getFieldsValue();
const commaPath = `solution_items,${solutionFields[0]?.key || 0}`;
const dotPath = `solution_items.${solutionFields[0]?.key || 0}`;
const firstSolution = solutionValues[commaPath] || solutionValues[dotPath];
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 submissionData = prepareSubmissionData(1);
const errorCodeValues = errorCodeForm.getFieldsValue();
const solutionData = getSolutionData();
const response = await createBrand(submissionData);
const payload = {
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 || []
};
// For create, include error_code field (required)
if (isAddingNewErrorCode) {
payload.error_code = errorCodeValues.error_code;
}
let response;
if (isAddingNewErrorCode) {
response = await createErrorCode(createdBrandId, payload);
} else {
response = await updateErrorCode(createdBrandId, editingErrorCodeKey, payload);
}
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Brand Device berhasil ditambahkan.',
message: isAddingNewErrorCode ? 'Error Code berhasil ditambahkan!' : 'Error Code berhasil diupdate!',
});
resetForm();
navigate('/master/brand-device');
resetErrorCodeForm();
setTrigerFilter(prev => !prev);
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menambahkan Brand Device',
message: response?.message || 'Gagal menyimpan error code',
});
}
} catch (error) {
console.error('Error saving error code:', error);
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan error code. Silakan coba lagi.',
});
}
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const handleFinish = async () => {
setConfirmLoading(true);
try {
// Fase 2 completion - brand sudah dibuat di fase 1
NotifOk({
icon: 'success',
title: 'Brand Device Tersimpan',
message: 'Brand device telah berhasil disimpan dengan error codes yang ditambahkan.',
});
navigate('/master/brand-device');
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
message: error.message || 'Terjadi kesalahan. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
@@ -391,7 +579,7 @@ const AddBrandDevice = () => {
>
<Button
icon={<PlusOutlined />}
onClick={handleAddErrorCode}
onClick={() => navigate(`/master/brand-device/${createdBrandId}/error-code/add`)}
size="large"
>
Add Error Code
@@ -407,11 +595,11 @@ const AddBrandDevice = () => {
cardColor={'#42AAFF'}
header={'error_code'}
showPreviewModal={handlePreviewErrorCode}
showEditModal={handleEditErrorCodeNavigate}
showEditModal={(record) => navigate(`/master/brand-device/${createdBrandId}/error-code/edit/${record.error_code_id}`)}
showDeleteDialog={handleDeleteErrorCode}
getData={getErrorCodesData}
queryParams={queryParams}
columns={errorCodeColumns(handlePreviewErrorCode, handleEditErrorCodeNavigate, handleDeleteErrorCode)}
columns={errorCodeColumns(handlePreviewErrorCode, (record) => navigate(`/master/brand-device/${createdBrandId}/error-code/edit/${record.error_code_id}`), handleDeleteErrorCode)}
triger={trigerFilter}
/>
</Col>
@@ -447,6 +635,12 @@ const AddBrandDevice = () => {
]);
}, [setBreadcrumbItems, navigate]);
useEffect(() => {
if (createdBrandId && currentStep === 1) {
setTrigerFilter(prev => !prev);
}
}, [createdBrandId, currentStep]);
return (
<Card>
<Title level={4} style={{ margin: '0 0 24px 0' }}>
@@ -525,7 +719,7 @@ const AddBrandDevice = () => {
borderColor: '#23A55A',
}}
>
Save Brand Device
Selesai
</Button>
)}
</div>

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import {
Card,
Typography,
@@ -11,10 +11,8 @@ import {
Upload,
} from 'antd';
import { ArrowLeftOutlined, UploadOutlined } from '@ant-design/icons';
import { getBrandById, getErrorCodeById, updateBrand, getErrorCodesByBrandId } from '../../../api/master-brand';
import { getBrandById, getErrorCodeById, updateBrand, getErrorCodesByBrandId, createErrorCode, updateErrorCode } 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';
@@ -27,23 +25,12 @@ const AddEditErrorCode = () => {
const navigate = useNavigate();
const { brandId: routeBrandId, errorCodeId } = useParams();
const { setBreadcrumbItems } = useBreadcrumb();
const location = useLocation();
// Use BrandForm context
const {
brandId: contextBrandId,
routeBrandId: contextRouteBrandId,
setRouteBrandId,
setErrorCodeId,
initializeFromRoute,
tempErrorCodes,
existingErrorCodes,
addErrorCode,
updateErrorCode,
setCurrentErrorCode
} = useBrandForm();
const currentBrandId = routeBrandId;
// Use brandId from context first, fallback to route
const currentBrandId = contextBrandId || routeBrandId;
const isFromAddBrand = location.pathname.includes('/master/brand-device/') && location.pathname.includes('/error-code/') &&
(location.pathname.includes('/add') || (location.pathname.includes('/edit/') && !location.pathname.includes('/edit/')));
// Forms
const [errorCodeForm] = Form.useForm();
@@ -75,11 +62,7 @@ const AddEditErrorCode = () => {
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>
@@ -114,7 +97,6 @@ const AddEditErrorCode = () => {
]);
if (isEditMode && errorCodeId) {
// For existing error codes, construct the proper tempId format
const tempId = errorCodeId.startsWith('existing_') ? errorCodeId : `existing_${errorCodeId}`;
loadExistingErrorCode(tempId);
}
@@ -124,113 +106,56 @@ const AddEditErrorCode = () => {
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
};
}
let errorIdToUse = tempId;
if (tempId.startsWith('existing_')) {
errorIdToUse = tempId.replace('existing_', '');
}
// console.log(' Found error code in context:', existingErrorCode);
const errorCodeResponse = await getErrorCodeById(errorIdToUse);
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 (errorCodeResponse && errorCodeResponse.statusCode === 200) {
const errorData = errorCodeResponse.data;
if (existingErrorCode.path_icon) {
setErrorCodeIcon({
name: existingErrorCode.path_icon.split('/').pop(),
uploadPath: existingErrorCode.path_icon,
url: existingErrorCode.path_icon,
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 (existingErrorCode.solution && existingErrorCode.solution.length > 0) {
// console.log('🔍 Setting solutions from context:', existingErrorCode.solution);
setSolutionsForExistingRecord(existingErrorCode.solution, solutionForm);
}
if (errorData.path_icon) {
setErrorCodeIcon({
name: errorData.path_icon.split('/').pop(),
uploadPath: errorData.path_icon,
url: errorData.path_icon,
});
}
if (existingErrorCode.spareparts && existingErrorCode.spareparts.length > 0) {
// console.log('🔍 Setting spareparts from context:', existingErrorCode.spareparts);
setSelectedSparepartIds(existingErrorCode.spareparts);
if (errorData.solution && errorData.solution.length > 0) {
setSolutionsForExistingRecord(errorData.solution, solutionForm);
}
if (errorData.spareparts && errorData.spareparts.length > 0) {
const sparepartIds = errorData.spareparts.map(sp => sp.sparepart_id);
setSelectedSparepartIds(sparepartIds);
}
}
} else {
// console.log('🔍 Error code not found in context, trying API...');
errorCodeForm.setFieldsValue({
error_code: '',
error_code_name: '',
error_code_description: '',
error_code_color: '#000000',
status: true,
});
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.',
});
}
NotifAlert({
icon: 'warning',
title: 'Peringatan',
message: 'Error code not found. Creating new error code.',
});
}
} catch (error) {
console.error('Failed to load error code:', error);
@@ -250,8 +175,9 @@ const AddEditErrorCode = () => {
const solutionValues = solutionForm.getFieldsValue();
const firstSolutionPath = `solution_items,${solutionFields[0]?.key || 0}`;
const firstSolution = solutionValues[firstSolutionPath];
const commaPath = `solution_items,${solutionFields[0]?.key || 0}`;
const dotPath = `solution_items.${solutionFields[0]?.key || 0}`;
const firstSolution = solutionValues[commaPath] || solutionValues[dotPath];
let isValid = false;
if (firstSolution && firstSolution.name && firstSolution.name.trim() !== '') {
@@ -276,55 +202,82 @@ const AddEditErrorCode = () => {
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;
setConfirmLoading(true);
try {
const payload = {
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?.path_icon || errorCodeIcon?.uploadPath || '',
is_active: errorCodeValues.status !== undefined ? errorCodeValues.status : true,
solution: solutionData || [],
spareparts: selectedSparepartIds || []
};
if (!isEdit) {
payload.error_code = errorCodeValues.error_code;
}
let response;
if (isEdit && errorCodeId) {
console.log('Updating error code:', errorCodeId);
console.log('Current brand ID:', currentBrandId);
console.log('Update payload:', JSON.stringify(payload, null, 2));
console.log('API URL:', `error-code/brand/${currentBrandId}/${errorCodeId}`);
response = await updateErrorCode(currentBrandId, errorCodeId, payload);
console.log('Full API response:', response);
} else {
console.log('Creating new error code');
console.log('Create payload:', JSON.stringify(payload, null, 2));
response = await createErrorCode(currentBrandId, payload);
console.log('Full API response:', response);
}
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: isEdit ? 'Error Code berhasil diupdate!' : 'Error Code berhasil ditambahkan!',
});
if (isFromAddBrand) {
navigate(`/master/brand-device/add`);
} else {
updateTempId = `existing_${errorCodeId}`;
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
}
} else {
updateTempId = errorCodeId;
console.log('Error response:', response);
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menyimpan error code',
});
}
} else {
updateTempId = Date.now().toString();
} catch (error) {
console.error('Error saving error code:', error);
console.error('Full error object:', JSON.stringify(error, null, 2));
if (error.response) {
console.error('Error response data:', error.response.data);
console.error('Error response status:', error.response.status);
}
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan error code. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
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);
console.error('Full error object:', JSON.stringify(error, null, 2));
if (error.response) {
console.error('Error response data:', error.response.data);
console.error('Error response status:', error.response.status);
}
NotifAlert({
icon: 'error',
title: 'Error',
@@ -334,103 +287,46 @@ const AddEditErrorCode = () => {
};
const handleCancel = () => {
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
if (isFromAddBrand) {
navigate(`/master/brand-device/add`);
} else {
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
}
};
const handleErrorCodeIconUpload = async (file) => {
if (!file) return null;
const handleErrorCodeIconUpload = (iconData) => {
console.log('handleErrorCodeIconUpload received:', iconData);
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',
});
if (!iconData || !iconData.uploadPath) {
console.error('❌ Invalid icon data received:', iconData);
return null;
}
const formattedIconData = {
name: iconData.name,
uploadPath: iconData.uploadPath,
url: iconData.uploadPath,
};
setErrorCodeIcon(formattedIconData);
console.log('Icon data stored from upload (no second upload):', formattedIconData);
return formattedIconData;
};
const handleErrorCodeIconRemove = () => {
console.log('🗑️ Removing error code icon');
setErrorCodeIcon(null);
console.log('✅ Error code icon removed');
};
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 handleSolutionFileUpload = (fileObject) => {
console.log('Solution file uploaded:', fileObject);
console.log('File object path_solution:', fileObject?.path_solution);
console.log('File object uploadPath:', fileObject?.uploadPath);
};
const resetForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
@@ -504,6 +400,7 @@ const AddEditErrorCode = () => {
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={handleErrorCodeIconUpload}
onErrorCodeIconRemove={handleErrorCodeIconRemove}
isEdit={isEdit}
/>
</Form>
</Card>
@@ -541,8 +438,11 @@ const AddEditErrorCode = () => {
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
onSolutionFileUpload={handleSolutionFileUpload}
onFileView={handleSolutionFileView}
fileList={fileList}
onFileView={(fileData) => {
if (fileData && fileData.url) {
window.open(fileData.url, '_blank');
}
}}
isReadOnly={false}
/>
</Form>
@@ -570,6 +470,7 @@ const AddEditErrorCode = () => {
<div style={{ marginTop: 24, textAlign: 'right' }}>
<Button
type="primary"
loading={confirmLoading}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',

View File

@@ -19,16 +19,13 @@ 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, getErrorCodesByBrandId } from '../../../api/master-brand';
import { getBrandById, getErrorCodesByBrandId } from '../../../api/master-brand';
import { getFileUrl } from '../../../api/file-uploads';
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 { useSolutionLogic } from './hooks/solution';
import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic';
import { useBrandForm } from '../../../context/BrandFormContext';
import SingleSparepartSelect from './component/SingleSparepartSelect';
const { Title } = Typography;
@@ -46,19 +43,9 @@ const EditBrandDevice = () => {
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [loading, setLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
// Context integration
const {
routeBrandId,
setRouteBrandId,
initializeFromRoute,
navigateToErrorCodes,
editErrorCode,
isLoading: contextLoading
} = useBrandForm();
// Use step from query parameter or context
// Use step from query parameter
const tab = searchParams.get('tab');
const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
@@ -67,22 +54,9 @@ const EditBrandDevice = () => {
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 [brandInfo, setBrandInfo] = useState({});
const [tempErrorCodes, setTempErrorCodes] = useState([]);
const [existingErrorCodes, setExistingErrorCodes] = useState([]);
const {
solutionFields,
@@ -141,6 +115,7 @@ const EditBrandDevice = () => {
const brandData = response.data;
const brandInfoData = {
brand_code: brandData.brand_code,
brand_name: brandData.brand_name,
brand_type: brandData.brand_type || '',
brand_manufacture: brandData.brand_manufacture || '',
@@ -149,12 +124,11 @@ const EditBrandDevice = () => {
};
setBrandInfo(brandInfoData);
setBrandId(brandData.brand_id);
brandForm.setFieldsValue(brandInfoData);
if (brandData.brand_id) {
try {
const errorCodesResponse = await getErrorCodesByBrandId(brandId || brandData.brand_id);
const errorCodesResponse = await getErrorCodesByBrandId(id || brandData.brand_id);
if (errorCodesResponse && errorCodesResponse.statusCode === 200) {
const apiErrorData = errorCodesResponse.data || [];
const existingCodes = apiErrorData.map(ec => ({
@@ -199,26 +173,59 @@ const EditBrandDevice = () => {
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) {
if (currentStep === 1 && id) {
setTrigerFilter(prev => !prev);
}
}, [currentStep, brandId]);
}, [currentStep, id]);
// Local functions to replace context methods
const addErrorCode = (newErrorCode) => {
const errorCodeWithId = {
...newErrorCode,
tempId: Date.now().toString(),
status: 'new'
};
setTempErrorCodes(prev => [...prev, errorCodeWithId]);
};
const updateErrorCode = (tempId, updatedData) => {
setTempErrorCodes(prev =>
prev.map(ec => ec.tempId === tempId ? { ...ec, ...updatedData, status: 'modified' } : ec)
);
setExistingErrorCodes(prev =>
prev.map(ec => ec.tempId === tempId ? { ...ec, ...updatedData, status: 'modified' } : ec)
);
};
const deleteErrorCode = (tempId, permanent = false) => {
if (permanent) {
setTempErrorCodes(prev => prev.filter(ec => ec.tempId !== tempId));
setExistingErrorCodes(prev => prev.filter(ec => ec.tempId !== tempId));
} else {
setTempErrorCodes(prev =>
prev.map(ec => ec.tempId === tempId ? { ...ec, status: 'deleted' } : ec)
);
setExistingErrorCodes(prev =>
prev.map(ec => ec.tempId === tempId ? { ...ec, status: 'deleted' } : ec)
);
}
};
const getErrorCodeById = (tempId) => {
const inTemp = tempErrorCodes.find(ec => ec.tempId === tempId);
if (inTemp) return inTemp;
const inExisting = existingErrorCodes.find(ec => ec.tempId === tempId);
return inExisting;
};
const handleNextStep = async () => {
try {
await brandForm.validateFields();
const currentBrandId = brandId || id;
const currentBrandId = id;
if (currentBrandId) {
navigateToErrorCodes(currentBrandId);
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
}
} catch (error) {
@@ -234,45 +241,7 @@ const EditBrandDevice = () => {
navigate('/master/brand-device');
};
const handleFinish = async () => {
const validation = validateForm();
if (!validation.isValid) {
return;
}
setConfirmLoading(true);
try {
const userId = JSON.parse(localStorage.getItem('user') || '{}').user_id || 1;
const submissionData = prepareSubmissionData(userId);
const response = await updateBrand(id, submissionData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Brand Device dan Error Codes berhasil diupdate.',
});
resetForm();
navigate('/master/brand-device');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal mengupdate Brand Device',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal mengupdate data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
};
const handleErrorCodeIconUpload = (iconData) => {
setErrorCodeIcon(iconData);
};
@@ -415,7 +384,7 @@ const EditBrandDevice = () => {
const handleEditErrorCodeNavigate = (record) => {
const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId;
const currentBrandId = brandId || id;
const currentBrandId = id;
if (errorCodeId && currentBrandId) {
navigate(`/master/brand-device/${currentBrandId}/error-code/edit/${errorCodeId}`);
}
@@ -473,8 +442,6 @@ const EditBrandDevice = () => {
render: (text, record) => (
<Space>
{text}
{record.status === 'new' && <Tag color="green">New</Tag>}
{record.status === 'modified' && <Tag color="orange">Modified</Tag>}
</Space>
),
},
@@ -484,24 +451,6 @@ const EditBrandDevice = () => {
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',
@@ -570,7 +519,7 @@ const EditBrandDevice = () => {
const search = params.get('search') || '';
const page = parseInt(params.get('page')) || 1;
const limit = parseInt(params.get('limit')) || 10;
const currentBrandId = brandId || id;
const currentBrandId = id;
if (!currentBrandId) {
console.warn('Brand ID is not available');
@@ -767,7 +716,7 @@ const EditBrandDevice = () => {
>
<Button
icon={<PlusOutlined />}
onClick={() => navigate(`/master/brand-device/${brandId || id}/error-code/add`)}
onClick={() => navigate(`/master/brand-device/${id}/error-code/add`)}
size="large"
>
Add Error Code
@@ -811,12 +760,9 @@ const EditBrandDevice = () => {
<Divider />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
<Button onClick={handleCancel}>
Cancel
</Button>
{currentStep === 1 && (
<Button
onClick={() => navigate(`/master/brand-device/edit/${brandId || id}?tab=brand`)}
onClick={() => navigate(`/master/brand-device/edit/${id}?tab=brand`)}
style={{ marginLeft: 8 }}
>
Back to Brand Info
@@ -833,20 +779,19 @@ const EditBrandDevice = () => {
borderColor: '#23A55A',
}}
>
Next to Error Codes
Error Code
</Button>
)}
{currentStep === 1 && (
<Button
type="primary"
onClick={handleFinish}
loading={confirmLoading}
onClick={handleCancel}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Update Brand Device
Kembali
</Button>
)}
</div>

View File

@@ -39,7 +39,6 @@ const BrandForm = ({
<Form.Item label="Brand Code" name="brand_code">
<Input
placeholder={'Auto Fill Brand Code'}
disabled={true}
style={{
backgroundColor: '#f5f5f5',

View File

@@ -1,6 +1,7 @@
import { Form, Input, Switch, Upload, Button, Typography, message, ConfigProvider } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { uploadFile } from '../../../../api/file-uploads';
import React from 'react';
import { Form, Input, Switch, Typography, ConfigProvider } from 'antd';
import FileUploadHandler from './FileUploadHandler';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
const { Text } = Typography;
@@ -11,53 +12,10 @@ const ErrorCodeSimpleForm = ({
onErrorCodeIconUpload,
onErrorCodeIconRemove,
onAddErrorCode,
isEdit = false, // Add isEdit prop to check if we're in edit mode
}) => {
const statusValue = Form.useWatch('status', errorCodeForm);
const handleIconUpload = async (file) => {
// Check if file is an image
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('You can only upload image files!');
return Upload.LIST_IGNORE;
}
// Check file size (max 2MB)
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must be smaller than 2MB!');
return Upload.LIST_IGNORE;
}
try {
const fileExtension = file.name.split('.').pop().toLowerCase();
const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(
fileExtension
);
const fileType = isImageFile ? 'image' : 'pdf';
const folder = 'images';
const uploadResponse = await uploadFile(file, folder);
const iconPath =
uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || '';
if (iconPath) {
onErrorCodeIconUpload({
name: file.name,
uploadPath: iconPath,
fileExtension,
isImage: isImageFile,
size: file.size,
});
message.success(`${file.name} uploaded successfully!`);
} else {
message.error(`Failed to upload ${file.name}`);
}
} catch (error) {
message.error(`Failed to upload ${file.name}`);
}
};
const handleIconRemove = () => {
onErrorCodeIconRemove();
};
@@ -69,7 +27,6 @@ const ErrorCodeSimpleForm = ({
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="status" valuePropName="checked" noStyle>
<Switch
disabled={isErrorCodeFormReadOnly}
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
@@ -83,7 +40,10 @@ const ErrorCodeSimpleForm = ({
name="error_code"
rules={[{ required: true, message: 'Error code wajib diisi!' }]}
>
<Input placeholder="Enter error code" disabled={isErrorCodeFormReadOnly} />
<Input
placeholder="Enter error code"
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
{/* Error Name */}
@@ -92,7 +52,7 @@ const ErrorCodeSimpleForm = ({
name="error_code_name"
rules={[{ required: true, message: 'Error name wajib diisi!' }]}
>
<Input placeholder="Enter error name" disabled={isErrorCodeFormReadOnly} />
<Input placeholder="Enter error name" />
</Form.Item>
{/* Error Description */}
@@ -100,126 +60,47 @@ const ErrorCodeSimpleForm = ({
<Input.TextArea
placeholder="Enter error description"
rows={3}
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
{/* Color and Icon in same row */}
{/* Color and Icon */}
<Form.Item label="Color & Icon">
<Input.Group compact>
<Form.Item name="error_code_color" noStyle>
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start' }}>
<Form.Item name="error_code_color" noStyle style={{ flex: '0 0 auto' }}>
<input
type="color"
disabled={isErrorCodeFormReadOnly}
style={{
width: '30%',
width: '120px',
height: '40px',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
defaultValue="#000000"
/>
</Form.Item>
<Form.Item noStyle style={{ width: '70%', paddingLeft: 8, }}>
{!isErrorCodeFormReadOnly ? (
<Upload
beforeUpload={handleIconUpload}
showUploadList={false}
accept="image/*"
style={{ width: '100%' }}
>
<Button
icon={<UploadOutlined />}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
Upload Icon
</Button>
</Upload>
) : (
<div
style={{
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
>
<Text type="secondary">No upload allowed</Text>
</div>
)}
</Form.Item>
</Input.Group>
{errorCodeIcon && (
<div style={{ marginTop: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<img
src={errorCodeIcon.url || errorCodeIcon.uploadPath}
alt="Error Code Icon"
style={{
width: 50,
height: 50,
objectFit: 'cover',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
/>
<div>
<Text style={{ fontSize: 12 }}>{errorCodeIcon.name}</Text>
<br />
<Text type="secondary" style={{ fontSize: 10 }}>
Size: {(errorCodeIcon.size / 1024).toFixed(1)} KB
</Text>
</div>
{!isErrorCodeFormReadOnly && (
<Button type="text" danger size="small" onClick={handleIconRemove}>
Remove
</Button>
)}
</div>
</div>
)}
</Form.Item>
{/* Add Error Code Button */}
{!isErrorCodeFormReadOnly && (
<Form.Item>
<ConfigProvider
theme={{
token: { colorBgContainer: '#23a55ade' },
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
<Button
htmlType="button"
onClick={() => {
// Call parent function to add error code
onAddErrorCode();
<Form.Item noStyle style={{ flex: '1 1 auto' }}>
<FileUploadHandler
type="error_code"
existingFile={errorCodeIcon}
accept="image/*"
onFileUpload={onErrorCodeIconUpload}
onFileRemove={handleIconRemove}
buttonText="Upload Icon"
buttonStyle={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
style={{ width: '100%' }}
>
Simpan Error Code
</Button>
</ConfigProvider>
</Form.Item>
)}
uploadText="Upload error code icon"
/>
</Form.Item>
</div>
</Form.Item>
</>
);
};

View File

@@ -1,297 +0,0 @@
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) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{record.path_icon && (
<img
src={record.path_icon}
alt="icon"
style={{ width: 24, height: 24, objectFit: 'cover' }}
/>
)}
<span style={{
color: record.error_code_color || '#000000',
fontWeight: 'bold'
}}>
{text}
</span>
</div>
),
},
{
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) => (
<Tag color="blue">
{record.solution ? record.solution.length : 0} Solutions
</Tag>
),
},
{
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 (
<Space direction="vertical" size={2}>
<Tag color="blue" style={{ margin: 0 }}>NEW</Tag>
<Tag color={statusColor} style={{ margin: 0 }}>{statusText}</Tag>
</Space>
);
} else if (record.status === 'modified') {
return (
<Space direction="vertical" size={2}>
<Tag color="orange" style={{ margin: 0 }}>MODIFIED</Tag>
<Tag color={statusColor} style={{ margin: 0 }}>{statusText}</Tag>
</Space>
);
} else if (record.status === 'deleted') {
return (
<Space direction="vertical" size={2}>
<Tag color="red" style={{ margin: 0 }}>DELETED</Tag>
<Tag color="default" style={{ margin: 0 }}>Inactive</Tag>
</Space>
);
} else {
return (
<Tag color={statusColor}>
{statusText}
</Tag>
);
}
},
},
{
title: 'Action',
key: 'action',
align: 'center',
width: '15%',
render: (_, record) => (
<Space size="small">
<Button
icon={<EyeOutlined />}
onClick={() => onView(record)}
size="small"
style={{
color: '#1890ff',
borderColor: '#1890ff',
}}
title="View Details"
/>
<Button
icon={<EditOutlined />}
onClick={() => onEdit(record)}
size="small"
style={{
color: '#faad14',
borderColor: '#faad14',
}}
title="Edit Error Code"
/>
<Button
danger
icon={<DeleteOutlined />}
onClick={() => onDelete(record)}
size="small"
title="Delete Error Code"
/>
</Space>
),
},
];
const ErrorCodeTable = memo(function ErrorCodeTable({
errorCodes = [],
loading = false,
brandId,
onAddErrorCode,
onEditErrorCode,
onDeleteErrorCode,
onViewErrorCode,
trigger,
}) {
const [trigerFilter, setTrigerFilter] = useState(false);
const [formDataFilter, setFormDataFilter] = useState({ search: '' });
const [searchValue, setSearchValue] = useState('');
// Trigger table refresh when parent component data changes
useEffect(() => {
if (trigger !== undefined) {
setTrigerFilter(prev => !prev);
}
}, [trigger]);
// Simulate API data for error codes
const getErrorCodesData = async (params) => {
console.log('📋 ErrorCodeTable getErrorCodesData called with:', params);
console.log('📋 Available errorCodes:', errorCodes);
// This would be your actual API call
const filteredData = errorCodes.filter(code =>
!params.search ||
code.error_code.toLowerCase().includes(params.search.toLowerCase()) ||
code.error_code_name.toLowerCase().includes(params.search.toLowerCase())
);
console.log('📋 Filtered result:', filteredData);
return {
data: filteredData,
total: filteredData.length,
};
};
const handleSearch = () => {
setFormDataFilter({ search: searchValue });
setTrigerFilter(prev => !prev);
};
const handleSearchClear = () => {
setSearchValue('');
setFormDataFilter({ search: '' });
setTrigerFilter(prev => !prev);
};
return (
<Card
title="Error Codes"
size="small"
>
<Row>
<Col xs={24}>
<Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={16} lg={18}>
<Input.Search
placeholder="Search error code..."
value={searchValue}
onChange={(e) => {
const value = e.target.value;
setSearchValue(value);
if (value === '') {
setFormDataFilter({ search: '' });
setTrigerFilter(prev => !prev);
}
}}
onSearch={handleSearch}
allowClear={{
clearIcon: <span onClick={handleSearchClear}></span>,
}}
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Search
</Button>
}
size="large"
style={{ marginBottom: 16 }}
/>
</Col>
<Col>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={onAddErrorCode}
size="large"
>
Add Error Code
</Button>
</ConfigProvider>
</Col>
</Row>
</Col>
<Col xs={24}>
<TableList
mobile
cardColor={'#42AAFF'}
header={'error_code_name'}
showPreviewModal={onViewErrorCode}
showEditModal={onEditErrorCode}
showDeleteDialog={onDeleteErrorCode}
getData={getErrorCodesData}
queryParams={formDataFilter}
columns={columns(
onViewErrorCode,
onEditErrorCode,
onDeleteErrorCode
)}
triger={trigerFilter}
loading={loading}
/>
</Col>
</Row>
</Card>
);
});
export default ErrorCodeTable;

View File

@@ -1,18 +1,38 @@
import { useState } from 'react';
import { Upload, Modal } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import React, { useState } from 'react';
import { Upload, Modal, Button, Typography, Space, Image } from 'antd';
import { UploadOutlined, EyeOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
import { uploadFile, getFolderFromFileType, getFileUrl, getFileType } from '../../../../api/file-uploads';
const { Text } = Typography;
const FileUploadHandler = ({
solutionFields,
fileList,
type = 'solution',
maxCount = 1,
accept = '.pdf,.jpg,.jpeg,.png,.gif',
disabled = false,
// File management
fileList = [],
onFileUpload,
onFileRemove
onFileRemove,
existingFile = null,
uploadText = 'Click or drag file to this area to upload',
uploadHint = 'Support for PDF and image files only',
buttonText = 'Upload File',
buttonType = 'default',
containerStyle = {},
buttonStyle = {},
showPreview = true
}) => {
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
const [isUploading, setIsUploading] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
const getBase64 = (file) =>
new Promise((resolve, reject) => {
@@ -22,99 +42,389 @@ const FileUploadHandler = ({
reader.onerror = (error) => reject(error);
});
const handleUploadPreview = async (file) => {
const preview = await getBase64(file);
setPreviewImage(preview);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
const handlePreview = async (file) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setPreviewImage(file.url || file.preview);
setPreviewOpen(true);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
};
const handleFileUpload = async (file) => {
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type);
const validateFile = (file) => {
const isAllowedType = [
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
].includes(file.type);
if (!isAllowedType) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
});
return Upload.LIST_IGNORE;
return false;
}
return true;
};
const handleFileUpload = async (file) => {
if (isUploading) {
return false;
}
if (!validateFile(file)) {
return false;
}
try {
setIsUploading(true);
const fileExtension = file.name.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
const fileType = isImage ? 'image' : 'pdf';
const folder = getFolderFromFileType(fileType);
const uploadResponse = await uploadFile(file, folder);
const actualPath = uploadResponse.data?.path_solution || '';
const isSuccess = uploadResponse && (
uploadResponse.statusCode === 200 ||
uploadResponse.statusCode === 201
);
if (!isSuccess) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: uploadResponse?.message || `Gagal mengupload ${file.name}`,
});
setIsUploading(false);
return false;
}
let actualPath = '';
if (uploadResponse && typeof uploadResponse === 'object') {
if (uploadResponse.data && uploadResponse.data.path_document) {
actualPath = uploadResponse.data.path_document;
}
else if (uploadResponse.path_document) {
actualPath = uploadResponse.path_document;
}
else if (uploadResponse.data && uploadResponse.data.path_solution) {
actualPath = uploadResponse.data.path_solution;
}
else if (uploadResponse.data && typeof uploadResponse.data === 'object') {
if (uploadResponse.data.file_url) {
actualPath = uploadResponse.data.file_url;
} else if (uploadResponse.data.url) {
actualPath = uploadResponse.data.url;
} else if (uploadResponse.data.path) {
actualPath = uploadResponse.data.path;
} else if (uploadResponse.data.location) {
actualPath = uploadResponse.data.location;
} else if (uploadResponse.data.filePath) {
actualPath = uploadResponse.data.filePath;
} else if (uploadResponse.data.file_path) {
actualPath = uploadResponse.data.file_path;
} else if (uploadResponse.data.publicUrl) {
actualPath = uploadResponse.data.publicUrl;
} else if (uploadResponse.data.public_url) {
actualPath = uploadResponse.data.public_url;
}
}
else if (uploadResponse && typeof uploadResponse === 'string') {
actualPath = uploadResponse;
}
}
if (actualPath) {
file.uploadPath = actualPath;
file.solution_name = file.name;
file.solutionId = solutionFields[0];
file.type_solution = fileType;
onFileUpload(file);
let fileObject;
if (type === 'error_code') {
fileObject = {
name: file.name,
path_icon: actualPath,
uploadPath: actualPath,
url: actualPath,
size: file.size,
type: file.type,
fileExtension
};
} else {
fileObject = {
name: file.name,
path_solution: actualPath,
uploadPath: actualPath,
type_solution: fileType,
size: file.size,
type: file.type
};
}
onFileUpload(fileObject);
setUploadedFile(fileObject);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `${file.name} berhasil diupload!`
});
setIsUploading(false);
return false;
} else {
console.error('Failed to extract file path from upload response:', uploadResponse);
NotifAlert({
icon: 'error',
title: 'Gagal',
message: `Gagal mengupload ${file.name}`
message: `Gagal mengupload ${file.name}. Tidak dapat menemukan path file dalam response.`,
});
setIsUploading(false);
return false;
}
} catch (error) {
console.error('Error uploading file:', error);
console.error('Upload error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
});
setIsUploading(false);
return false;
}
};
const handleFileChange = ({ fileList }) => {
if (fileList && fileList.length > 0 && fileList[0] && fileList[0].originFileObj) {
handleFileUpload(fileList[0].originFileObj);
}
};
const handleRemove = () => {
console.log('🗑️ FileUploadHandler handleRemove called:', {
existingFile,
onFileRemove: typeof onFileRemove,
hasExistingFile: !!existingFile
});
if (existingFile && onFileRemove) {
onFileRemove(existingFile);
} else if (onFileRemove) {
// Call onFileRemove even without existingFile to trigger form cleanup
onFileRemove(null);
}
};
const renderExistingFile = () => {
const fileToShow = existingFile || uploadedFile;
if (!fileToShow) {
console.log('❌ FileUploadHandler renderExistingFile: No file to render');
return null;
}
return false;
console.log('✅ FileUploadHandler renderExistingFile: File found', {
existingFile: !!existingFile,
uploadedFile: !!uploadedFile,
fileName: fileToShow.name,
shouldShowDeleteButton: true
});
const filePath = fileToShow.uploadPath || fileToShow.url || fileToShow.path_icon || fileToShow.path_solution;
const fileName = fileToShow.name || filePath?.split('/').pop() || 'Unknown file';
const fileType = getFileType(fileName);
const isImage = fileType === 'image';
const handlePreview = () => {
if (!showPreview || !filePath) return;
if (isImage) {
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
const filename = filePath.split('/').pop();
const imageUrl = getFileUrl(folder, filename);
if (imageUrl) {
setPreviewImage(imageUrl);
setPreviewOpen(true);
setPreviewTitle(fileName);
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Cannot generate image preview URL',
});
}
} else {
// For PDFs and other files, open in new tab
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
const filename = filePath.split('/').pop();
const fileUrl = getFileUrl(folder, filename);
if (fileUrl) {
window.open(fileUrl, '_blank', 'noopener,noreferrer');
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Cannot generate file preview URL',
});
}
}
};
const getThumbnailUrl = () => {
if (!isImage || !filePath) return null;
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
const filename = filePath.split('/').pop();
return getFileUrl(folder, filename);
};
const thumbnailUrl = getThumbnailUrl();
return (
<div style={{ marginTop: 12 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px',
border: '1px solid #d9d9d9',
borderRadius: 4,
backgroundColor: '#fafafa'
}}>
{isImage ? (
<img
src={thumbnailUrl || filePath}
alt={fileName}
style={{
width: 50,
height: 50,
objectFit: 'cover',
border: '1px solid #d9d9d9',
borderRadius: 4,
cursor: showPreview ? 'pointer' : 'default'
}}
onClick={handlePreview}
onError={(e) => {
e.target.src = filePath;
}}
/>
) : (
<div
style={{
width: 50,
height: 50,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #d9d9d9',
borderRadius: 4,
backgroundColor: '#f5f5f5',
cursor: showPreview ? 'pointer' : 'default'
}}
onClick={handlePreview}
>
<FileOutlined style={{ fontSize: 24, color: '#666' }} />
</div>
)}
<div style={{ flex: 1 }}>
<Text style={{ fontSize: 12, fontWeight: 500 }}>
{fileName}
</Text>
<br />
<Text type="secondary" style={{ fontSize: 10 }}>
{fileType === 'image' ? 'Image' : fileType === 'pdf' ? 'PDF' : 'File'}
{fileToShow.size && `${(fileToShow.size / 1024).toFixed(1)} KB`}
</Text>
</div>
<div style={{ display: 'flex', gap: 4 }}>
{showPreview && (
<Button
type="text"
icon={<EyeOutlined />}
size="small"
onClick={handlePreview}
title={isImage ? "Preview Image" : "Open File"}
/>
)}
<Button
type="text"
danger
icon={<DeleteOutlined />}
size="small"
onClick={handleRemove}
title="Remove File"
/>
</div>
</div>
</div>
);
};
const uploadProps = {
multiple: true,
accept: '.pdf,.jpg,.jpeg,.png,.gif',
onRemove: onFileRemove,
beforeUpload: handleFileUpload,
fileList,
onPreview: handleUploadPreview,
name: 'file',
multiple: false,
accept,
disabled: disabled || isUploading,
fileList: [],
beforeUpload: () => false,
onChange: handleFileChange,
onPreview: handlePreview,
maxCount,
};
return (
<>
<Upload.Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">Support for PDF and image files only</p>
</Upload.Dragger>
<div style={{ ...containerStyle }}>
{!existingFile && (
<Upload {...uploadProps}>
{type === 'drag' ? (
<Upload.Dragger>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">{uploadText}</p>
<p className="ant-upload-hint">{uploadHint}</p>
</Upload.Dragger>
) : (
<Button
type={buttonType}
icon={<UploadOutlined />}
loading={isUploading}
style={{ ...buttonStyle }}
>
{isUploading ? 'Uploading...' : buttonText}
</Button>
)}
</Upload>
)}
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={() => setPreviewOpen(false)}
width="80%"
style={{ top: 20 }}
>
{previewImage && (
<img
alt={previewTitle}
style={{ width: '100%' }}
src={previewImage}
/>
)}
</Modal>
</>
{renderExistingFile()}
{showPreview && (
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={() => setPreviewOpen(false)}
width={600}
style={{ top: 100 }}
>
{previewImage && (
<img
alt={previewTitle}
style={{ width: '100%' }}
src={previewImage}
/>
)}
</Modal>
)}
</div>
);
};

View File

@@ -26,26 +26,12 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
key: 'brand_name',
width: '20%',
},
{
title: 'Type',
dataIndex: 'brand_type',
key: 'brand_type',
width: '15%',
render: (text) => text || '-',
},
{
title: 'Manufacturer',
dataIndex: 'brand_manufacture',
key: 'brand_manufacture',
width: '20%',
},
{
title: 'Model',
dataIndex: 'brand_model',
key: 'brand_model',
width: '15%',
render: (text) => text || '-',
},
{
title: 'Status',
dataIndex: 'is_active',
@@ -139,12 +125,10 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
};
const showPreviewModal = (param) => {
// Direct navigation without loading, page will handle its own loading
navigate(`/master/brand-device/view/${param.brand_id}`);
};
const showEditModal = (param = null) => {
// Direct navigation without loading, page will handle its own loading
if (param) {
navigate(`/master/brand-device/edit/${param.brand_id}`);
} else {
@@ -202,7 +186,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
onChange={(e) => {
const value = e.target.value;
setSearchValue(value);
// Auto search when clearing by backspace/delete
if (value === '') {
setFormDataFilter({ search: '' });
setTrigerFilter((prev) => !prev);

View File

@@ -1,84 +0,0 @@
import React from 'react';
import { Table, Button, Space } from 'antd';
import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
const ErrorCodeTable = ({
errorCodes,
loading,
onPreview,
onEdit,
onDelete,
onFileView
}) => {
const errorCodeColumns = [
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
{ title: 'Error Code Name', dataIndex: 'error_code_name', key: 'error_code_name' },
{
title: 'Solutions',
dataIndex: 'solution',
key: 'solution',
render: (solutions) => (
<div>
{solutions && solutions.length > 0 ? (
solutions.map((sol, index) => (
<div key={index} style={{ marginBottom: 4 }}>
<span style={{ fontSize: '12px' }}>
{sol.solution_name}
</span>
</div>
))
) : (
<span style={{ color: '#999', fontSize: '12px' }}>No solutions</span>
)}
</div>
)
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => onPreview(record)}
style={{ color: '#1890ff', borderColor: '#1890ff' }}
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEdit(record)}
style={{ color: '#faad14', borderColor: '#faad14' }}
/>
<Button
danger
type="text"
icon={<DeleteOutlined />}
onClick={() => onDelete(record.key)}
style={{ borderColor: '#ff4d4f' }}
/>
</Space>
),
},
];
const dataSource = loading
? Array.from({ length: 3 }, (_, index) => ({
key: `loading-${index}`,
error_code: 'Loading...',
error_code_name: 'Loading...',
solution: []
}))
: errorCodes;
return (
<Table
columns={errorCodeColumns}
dataSource={dataSource}
rowKey="key"
pagination={false}
/>
);
};
export default ErrorCodeTable;

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Switch, Radio, Upload, Typography, Space } from 'antd';
import { DeleteOutlined, UploadOutlined, EyeOutlined } from '@ant-design/icons';
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
import React, { useState } from 'react';
import { Form, Input, Button, Switch, Radio, Typography, Space } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import FileUploadHandler from './FileUploadHandler';
import { NotifAlert } from '../../../../components/Global/ToastNotif';
const { Text } = Typography;
@@ -22,64 +22,43 @@ const SolutionFieldNew = ({
onFileView,
fileList = []
}) => {
const handleFileUpload = async (file) => {
try {
const isAllowedType = [
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
].includes(file.type);
const form = Form.useFormInstance();
const existingFile = Form.useWatch([`solution_items,${fieldKey}`, 'fileUpload'], form) ||
Form.useWatch([`solution_items,${fieldKey}`, 'file'], form);
if (!isAllowedType) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
});
return;
}
// Get form values for debugging and file data extraction
const allFormValues = form.getFieldsValue(true);
const solutionData = allFormValues[`solution_items,${fieldKey}`] || {};
const fileExtension = file.name.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
const fileType = isImage ? 'image' : 'pdf';
const folder = getFolderFromFileType(fileType);
const uploadResponse = await uploadFile(file, folder);
const actualPath = uploadResponse.data?.path_solution || '';
if (actualPath) {
// Store the file info with the solution field
file.uploadPath = actualPath;
file.solutionId = fieldKey;
file.type_solution = fileType;
onFileUpload(file);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `${file.name} berhasil diupload!`,
});
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: `Gagal mengupload ${file.name}`,
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
});
// Extract file data from form values for preview
const getFileFromFormValues = () => {
if (solutionData.fileUpload && typeof solutionData.fileUpload === 'object' && Object.keys(solutionData.fileUpload).length > 0) {
return solutionData.fileUpload;
}
if (solutionData.file && typeof solutionData.file === 'object' && Object.keys(solutionData.file).length > 0) {
return solutionData.file;
}
return null;
};
const fileFromForm = getFileFromFormValues();
const displayFile = existingFile || fileFromForm;
console.log(`🔍 SolutionField ${fieldKey}:`, {
solutionType,
hasPathSolution: !!solutionData.path_solution,
pathSolution: solutionData.path_solution,
fileFromForm,
existingFile,
displayFile,
shouldRenderPreview: !!displayFile
});
const renderSolutionContent = () => {
if (solutionType === 'text') {
return (
<Form.Item
name={[fieldName, 'text']}
name={[`solution_items,${fieldKey}`, 'text']}
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
>
<TextArea
@@ -93,59 +72,44 @@ const SolutionFieldNew = ({
}
if (solutionType === 'file') {
const currentFiles = fileList.filter(file => file.solutionId === fieldKey);
return (
<div>
<Form.Item
name={[fieldName, 'file']}
rules={[{ required: true, message: 'File solution wajib diupload!' }]}
>
<Upload
beforeUpload={handleFileUpload}
showUploadList={false}
accept=".pdf,.jpg,.jpeg,.png,.gif"
disabled={isReadOnly}
>
<Button
icon={<UploadOutlined />}
disabled={isReadOnly}
size="small"
style={{ width: '100%', fontSize: 12 }}
>
Upload File
</Button>
</Upload>
</Form.Item>
<FileUploadHandler
type="solution"
existingFile={displayFile}
onFileUpload={(fileObject) => {
const fileWithKey = {
...fileObject,
solutionId: fieldKey
};
{currentFiles.length > 0 && (
<div style={{ marginTop: 8 }}>
{currentFiles.map((file, index) => (
<div key={index} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '4px 8px',
border: '1px solid #d9d9d9',
borderRadius: 4,
marginBottom: 4
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 12 }}>{file.name}</Text>
<Text type="secondary" style={{ fontSize: 10 }}>
({(file.size / 1024).toFixed(1)} KB)
</Text>
</div>
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={() => onFileView(file.uploadPath, file.type_solution)}
/>
</div>
))}
</div>
)}
if (onFileUpload && typeof onFileUpload === 'function') {
onFileUpload(fileWithKey);
}
form.setFieldValue([`solution_items,${fieldKey}`, 'fileUpload'], fileWithKey);
form.setFieldValue([`solution_items,${fieldKey}`, 'file'], fileWithKey);
form.setFieldValue([`solution_items,${fieldKey}`, 'type'], 'file');
}}
onFileRemove={() => {
console.log(`🗑️ Removing file from solution ${fieldKey}`);
// Clear file form values only, keep type as file and status active
form.setFieldValue([`solution_items,${fieldKey}`, 'fileUpload'], null);
form.setFieldValue([`solution_items,${fieldKey}`, 'file'], null);
// Call parent callback if exists
if (onFileUpload && typeof onFileUpload === 'function') {
onFileUpload(null);
}
console.log(`✅ File removed from solution ${fieldKey} - type and status preserved`);
}}
disabled={isReadOnly}
buttonText={displayFile ? 'Replace File' : 'Upload File'}
buttonStyle={{ width: '100%', fontSize: 12 }}
uploadText="Upload solution file"
/>
</div>
);
}
@@ -176,15 +140,16 @@ const SolutionFieldNew = ({
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Form.Item name={[fieldName, 'status']} valuePropName="checked" noStyle>
<Form.Item name={[`solution_items,${fieldKey}`, 'status']} valuePropName="checked" noStyle>
<Switch
size="small"
disabled={isReadOnly}
onChange={(checked) => {
onStatusChange(fieldKey, checked);
}}
defaultChecked={solutionStatus !== false}
style={{
backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf'
backgroundColor: solutionStatus !== false ? '#23A55A' : '#bfbfbf'
}}
/>
</Form.Item>
@@ -193,7 +158,7 @@ const SolutionFieldNew = ({
color: '#666',
whiteSpace: 'nowrap'
}}>
{solutionStatus ? 'Active' : 'Inactive'}
{solutionStatus !== false ? 'Active' : 'Inactive'}
</Text>
</div>
@@ -215,7 +180,7 @@ const SolutionFieldNew = ({
</div>
<Form.Item
name={[fieldName, 'name']}
name={[`solution_items,${fieldKey}`, 'name']}
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
style={{ margin: 0 }}
>
@@ -226,10 +191,11 @@ const SolutionFieldNew = ({
style={{ fontSize: 13 }}
/>
</Form.Item>
</div>
</div>
<Form.Item
name={[fieldName, 'type']}
name={[`solution_items,${fieldKey}`, 'type']}
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
style={{ marginBottom: 8 }}
initialValue={solutionType || 'text'}
@@ -238,13 +204,20 @@ const SolutionFieldNew = ({
onChange={(e) => onTypeChange(fieldKey, e.target.value)}
disabled={isReadOnly}
size="small"
defaultValue={solutionType || 'text'}
>
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name={[`solution_items,${fieldKey}`, 'status']}
initialValue={solutionStatus !== false ? true : false}
noStyle
>
<input type="hidden" />
</Form.Item>
{renderSolutionContent()}
</div>
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Form, Card, Typography, Divider, Button } from 'antd';
import { Typography, Divider, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import SolutionFieldNew from './SolutionField';
@@ -21,83 +21,66 @@ const SolutionForm = ({
fileList,
isReadOnly = false,
}) => {
// 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
});
// 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 (
<div style={{ marginBottom: 0 }}>
<Form
form={solutionForm}
layout="vertical"
initialValues={{
solution_items: [{
status: true,
type: 'text',
}]
}}
style={{
marginBottom: 0
}}
>
<Divider orientation="left">Solution Items</Divider>
<Divider orientation="left">Solution Items</Divider>
<div style={{
maxHeight: '400px',
overflowY: 'auto',
paddingRight: '8px'
}}>
{solutionFields.map((field) => (
<SolutionFieldNew
key={field}
fieldKey={field}
fieldName={['solution_items', field]}
index={field}
solutionType={solutionTypes[field]}
solutionStatus={solutionStatuses[field]}
onTypeChange={onSolutionTypeChange}
onStatusChange={onSolutionStatusChange}
onRemove={() => onRemoveSolutionField(field)}
onFileUpload={onSolutionFileUpload}
onFileView={onFileView}
fileList={fileList}
isReadOnly={isReadOnly}
canRemove={solutionFields.length > 1}
/>
))}
<div style={{
maxHeight: '400px',
overflowY: 'auto',
paddingRight: '8px'
}}>
{solutionFields.map((field, displayIndex) => (
<SolutionFieldNew
key={field}
fieldKey={field}
fieldName={['solution_items', field]}
index={displayIndex}
solutionType={solutionTypes[field]}
solutionStatus={solutionStatuses[field]}
onTypeChange={onSolutionTypeChange}
onStatusChange={onSolutionStatusChange}
onRemove={() => onRemoveSolutionField(field)}
onFileUpload={onSolutionFileUpload}
onFileView={onFileView}
fileList={fileList}
isReadOnly={isReadOnly}
canRemove={solutionFields.length > 1 && displayIndex > 0}
/>
))}
</div>
{!isReadOnly && (
<div style={{ marginBottom: 8 }}>
<Button
type="dashed"
onClick={onAddSolutionField}
icon={<PlusOutlined />}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
height: '32px',
fontSize: '12px'
}}
>
Add More Solution
</Button>
</div>
{!isReadOnly && (
<>
<Form.Item style={{ marginBottom: 8 }}>
<Button
type="dashed"
onClick={onAddSolutionField}
icon={<PlusOutlined />}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
height: '32px',
fontSize: '12px'
}}
>
Add More Sollution
</Button>
</Form.Item>
</>
)}
</Form>
)}
</div>
);
};

View File

@@ -13,7 +13,6 @@ export const useSolutionLogic = (solutionForm) => {
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
// Set default values for the new field
setTimeout(() => {
solutionForm.setFieldValue(['solution_items', newKey, 'name'], '');
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
@@ -24,10 +23,10 @@ export const useSolutionLogic = (solutionForm) => {
const handleRemoveSolutionField = (key) => {
if (solutionFields.length <= 1) {
return;
return;
}
setSolutionFields(prev => prev.filter(field => field.key !== key));
setSolutionFields(prev => prev.filter(field => field !== key));
// Clean up type and status
const newTypes = { ...solutionTypes };
@@ -41,6 +40,26 @@ export const useSolutionLogic = (solutionForm) => {
const handleSolutionTypeChange = (key, value) => {
setSolutionTypes(prev => ({ ...prev, [key]: value }));
if (value === 'text') {
setTimeout(() => {
const fieldName = ['solution_items', key];
const currentSolutionData = solutionForm.getFieldsValue([fieldName]) || {};
const solutionData = currentSolutionData[`solution_items,${key}`] || currentSolutionData[`solution_items.${key}`] || {};
const updatedSolutionData = {
...solutionData,
fileUpload: null,
file: null
};
// Update form with cleared file data
const commaPath = fieldName.join(',');
solutionForm.setFieldValue(commaPath, updatedSolutionData);
solutionForm.setFieldValue([...fieldName, 'fileUpload'], null);
solutionForm.setFieldValue([...fieldName, 'file'], null);
}, 0);
}
};
const handleSolutionStatusChange = (key, value) => {
@@ -48,7 +67,7 @@ export const useSolutionLogic = (solutionForm) => {
};
const resetSolutionFields = () => {
setSolutionFields([{ name: ['solution_items', 0], key: 0 }]);
setSolutionFields([0]);
setSolutionTypes({ 0: 'text' });
setSolutionStatuses({ 0: true });
@@ -71,8 +90,10 @@ export const useSolutionLogic = (solutionForm) => {
}
const solutionKey = firstField.key || firstField;
const solutionPath = `solution_items,${solutionKey}`;
const firstSolution = values[solutionPath];
// Try both notations for compatibility
const commaPath = `solution_items,${solutionKey}`;
const dotPath = `solution_items.${solutionKey}`;
const firstSolution = values[commaPath] || values[dotPath];
if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') {
return false;
@@ -86,25 +107,73 @@ export const useSolutionLogic = (solutionForm) => {
};
const getSolutionData = () => {
const values = solutionForm.getFieldsValue();
const values = solutionForm.getFieldsValue(true);
const result = solutionFields.map(key => {
const solution = values[`solution_items,${key}`];
let solution = null;
// Try nested format first (preferred)
if (values.solution_items && values.solution_items[key]) {
solution = values.solution_items[key];
}
// Fallback to comma notation
if (!solution) {
const commaKey = `solution_items,${key}`;
solution = values[commaKey];
}
// Fallback to dot notation
if (!solution) {
const dotKey = `solution_items.${key}`;
solution = values[dotKey];
}
if (!solution) {
// Last resort: search in all keys
const allKeys = Object.keys(values);
const foundKey = allKeys.find(k => k.includes(key.toString()) && k.includes('solution_items'));
if (foundKey) {
solution = values[foundKey];
}
}
if (!solution) return null;
const validSolution = solution.name && solution.name.trim() !== '';
if (validSolution) {
return {
solution_name: solution.name || 'Default Solution',
type_solution: solutionTypes[key] || 'text',
text_solution: solution.text || '',
path_solution: solution.file || '',
is_active: solution.status !== false,
};
if (!validSolution) return null;
let pathSolution = '';
let fileObject = null;
if (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0) {
pathSolution = solution.fileUpload.path_solution || solution.fileUpload.uploadPath || '';
fileObject = solution.fileUpload;
} else if (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0) {
pathSolution = solution.file.path_solution || solution.file.uploadPath || '';
fileObject = solution.file;
} else if (solution.file && typeof solution.file === 'string' && solution.file.trim() !== '') {
pathSolution = solution.file;
}
return null;
let typeSolution = solutionTypes[key] || solution.type || 'text';
if (typeSolution === 'file') {
if (fileObject && fileObject.type_solution) {
typeSolution = fileObject.type_solution;
} else {
typeSolution = 'image';
}
}
return {
solution_name: solution.name,
type_solution: typeSolution,
text_solution: solution.text || '',
path_solution: pathSolution,
is_active: solution.status !== false && solution.status !== undefined ? solution.status : (solutionStatuses[key] !== false),
};
}).filter(Boolean);
return result;
@@ -123,17 +192,44 @@ export const useSolutionLogic = (solutionForm) => {
solutions.forEach((solution, index) => {
const key = solution.id || index;
console.log(`🔧 Processing solution ${key}:`, solution);
let fileObject = null;
if (solution.path_solution && solution.path_solution.trim() !== '') {
const fileName = solution.file_upload_name || solution.path_solution.split('/').pop() || `file_${index}`;
fileObject = {
uploadPath: solution.path_solution,
path_solution: solution.path_solution,
name: fileName,
type_solution: solution.type_solution || 'image',
isExisting: true,
size: 0,
type: solution.type_solution === 'pdf' ? 'application/pdf' : 'image/jpeg',
fileExtension: solution.type_solution === 'pdf' ? 'pdf' :
(fileName.split('.').pop().toLowerCase() || 'jpg')
};
console.log(`✅ Created file object for ${key}:`, fileObject);
}
const isFileType = solution.type_solution && solution.type_solution !== 'text' && fileObject;
solutionsValues[key] = {
name: solution.solution_name || '',
type: solution.type_solution || 'text',
type: isFileType ? 'file' : 'text',
text: solution.text_solution || '',
file: solution.path_solution || '',
file: fileObject,
fileUpload: fileObject,
status: solution.is_active !== false
};
newTypes[key] = solution.type_solution || 'text';
newTypes[key] = isFileType ? 'file' : 'text';
newStatuses[key] = solution.is_active !== false;
});
// Set all form values at once
console.log('🔧 Final solutions values:', solutionsValues);
console.log('🔧 Final solution types:', newTypes);
const formValues = {};
Object.keys(solutionsValues).forEach(key => {
const solution = solutionsValues[key];
@@ -142,10 +238,12 @@ export const useSolutionLogic = (solutionForm) => {
type: solution.type,
text: solution.text,
file: solution.file,
status: solution.is_active !== false
fileUpload: solution.fileUpload,
status: solution.status
};
});
console.log('🔧 Setting form values:', formValues);
form.setFieldsValue(formValues);
setSolutionTypes(newTypes);
setSolutionStatuses(newStatuses);

View File

@@ -1,298 +0,0 @@
import { useState } from 'react';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
export const useBrandDeviceLogic = (isEditMode = false, brandId = null) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [errorCodes, setErrorCodes] = useState([]);
const [pendingErrorCodes, setPendingErrorCodes] = useState([]);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const handleCancel = () => {
};
const handleNextStep = async (validateForm, setFormData, currentFormData) => {
try {
const validatedFormData = await validateForm();
setFormData({
brand_name: validatedFormData.brand_name,
brand_type: validatedFormData.brand_type || '',
brand_model: validatedFormData.brand_model || '',
brand_manufacture: validatedFormData.brand_manufacture || '',
is_active: validatedFormData.is_active,
});
setCurrentStep(1);
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib untuk brand device!',
});
return false;
}
};
const handleFinish = async (
formData,
selectedSparepartIds,
apiCall,
successMessage,
navigatePath
) => {
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 || '#000000',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map(sol => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active
}))
})) : (isEditMode ? errorCodes.map(ec => ({
error_code: ec.error_code,
error_code_name: ec.error_code_name,
error_code_description: ec.error_code_description || '',
error_code_color: ec.error_code_color || '#000000',
path_icon: ec.path_icon || '',
is_active: ec.status !== undefined ? ec.status : true,
solution: (ec.solution || []).map(sol => ({
solution_name: sol.solution_name,
type_solution: sol.type_solution,
text_solution: sol.text_solution || '',
path_solution: sol.path_solution || '',
is_active: sol.is_active
}))
})) : []);
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 apiCall(brandId, brandData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: response.message || successMessage,
});
navigate(navigatePath);
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menyimpan data.',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
});
} finally {
setConfirmLoading(false);
}
};
const handleAddErrorCode = async (
validateErrorCodeForm,
getSolutionData,
resetErrorCodeForm
) => {
try {
const errorCodeValues = await validateErrorCodeForm();
const solutionData = getSolutionData();
if (solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!',
});
return false;
}
const newErrorCode = {
key: editingErrorCodeKey || `temp-${Date.now()}`,
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: errorCodeValues.path_icon || '',
is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: solutionData,
};
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);
return true;
} catch (error) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!',
});
return false;
}
};
const handleDeleteErrorCode = (key) => {
if (errorCodes.length <= 1) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap brand harus memiliki minimal 1 error code!',
});
return false;
}
const updatedErrorCodes = errorCodes.filter((item) => item.key !== key);
setErrorCodes(updatedErrorCodes);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus!',
});
return true;
};
const handleCreateNewErrorCode = (resetErrorCodeForm, resetSolutionFields) => {
resetErrorCodeForm();
resetSolutionFields();
setIsErrorCodeFormReadOnly(false);
setEditingErrorCodeKey(null);
};
const handlePreviewErrorCode = (
record,
setErrorCodeIcon,
setIsErrorCodeFormReadOnly,
setEditingErrorCodeKey,
setSolutionsForExistingRecord,
resetSolutionFields,
solutionForm
) => {
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,
setErrorCodeIcon,
setIsErrorCodeFormReadOnly,
setEditingErrorCodeKey,
setSolutionsForExistingRecord,
resetSolutionFields,
solutionForm
) => {
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' });
}
};
return {
// State
confirmLoading,
setConfirmLoading,
currentStep,
setCurrentStep,
loading,
setLoading,
errorCodes,
setErrorCodes,
pendingErrorCodes,
setPendingErrorCodes,
editingErrorCodeKey,
setEditingErrorCodeKey,
isErrorCodeFormReadOnly,
setIsErrorCodeFormReadOnly,
// Handlers
handleCancel,
handleNextStep,
handleFinish,
handleAddErrorCode,
handleDeleteErrorCode,
handleCreateNewErrorCode,
handlePreviewErrorCode,
handleEditErrorCode,
};
};