624 lines
18 KiB
JavaScript
624 lines
18 KiB
JavaScript
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; |