repair: add edit brand device
This commit is contained in:
48
src/App.jsx
48
src/App.jsx
@@ -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 />}>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user