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 SignUp from './pages/auth/Signup';
|
||||||
import { ProtectedRoute } from './ProtectedRoute';
|
import { ProtectedRoute } from './ProtectedRoute';
|
||||||
import NotFound from './pages/blank/NotFound';
|
import NotFound from './pages/blank/NotFound';
|
||||||
import { BrandFormProvider } from './context/BrandFormContext';
|
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
import Home from './pages/home/Home';
|
import Home from './pages/home/Home';
|
||||||
@@ -97,33 +96,26 @@ const App = () => {
|
|||||||
<Route path="shift" element={<IndexShift />} />
|
<Route path="shift" element={<IndexShift />} />
|
||||||
<Route path="status" element={<IndexStatus />} />
|
<Route path="status" element={<IndexStatus />} />
|
||||||
|
|
||||||
{/* Brand Device Routes with BrandFormProvider */}
|
{/* Brand Device Routes */}
|
||||||
<Route path="brand-device/*" element={
|
<Route path="brand-device" element={<IndexBrandDevice />} />
|
||||||
<BrandFormProvider>
|
<Route path="brand-device/add" element={<AddBrandDevice />} />
|
||||||
<Routes>
|
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
|
||||||
<Route path="" element={<IndexBrandDevice />} />
|
<Route path="brand-device/view/:id" element={<ViewBrandDevice />} />
|
||||||
<Route path="add" element={<AddBrandDevice />} />
|
<Route
|
||||||
<Route path="edit/:id" element={<EditBrandDevice />} />
|
path="brand-device/edit/:id/files/:fileType/:fileName"
|
||||||
<Route path="view/:id" element={<ViewBrandDevice />} />
|
element={<ViewFilePage />}
|
||||||
<Route
|
/>
|
||||||
path="edit/:id/files/:fileType/:fileName"
|
<Route
|
||||||
element={<ViewFilePage />}
|
path="brand-device/view/:id/files/:fileType/:fileName"
|
||||||
/>
|
element={<ViewFilePage />}
|
||||||
<Route
|
/>
|
||||||
path="view/:id/files/:fileType/:fileName"
|
<Route
|
||||||
element={<ViewFilePage />}
|
path="brand-device/view/temp/files/:fileName"
|
||||||
/>
|
element={<ViewFilePage />}
|
||||||
<Route
|
/>
|
||||||
path="view/temp/files/:fileName"
|
<Route path="brand-device/:brandId/error-code/add" element={<AddEditErrorCode />} />
|
||||||
element={<ViewFilePage />}
|
<Route path="brand-device/:brandId/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
|
||||||
/>
|
<Route path="brand-device/add/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
|
||||||
<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>
|
|
||||||
} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/report" element={<ProtectedRoute />}>
|
<Route path="/report" element={<ProtectedRoute />}>
|
||||||
|
|||||||
@@ -66,4 +66,44 @@ const getErrorCodeById = async (id) => {
|
|||||||
return response.data;
|
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 { ConfigProvider } from 'antd';
|
||||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
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 BrandForm from './component/BrandForm';
|
||||||
import { useSolutionLogic } from './hooks/solution';
|
import { useSolutionLogic } from './hooks/solution';
|
||||||
import { useBrandForm } from '../../../context/BrandFormContext';
|
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
@@ -32,32 +31,24 @@ const AddBrandDevice = () => {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { setBreadcrumbItems } = useBreadcrumb();
|
const { setBreadcrumbItems } = useBreadcrumb();
|
||||||
const [brandForm] = Form.useForm();
|
const [brandForm] = Form.useForm();
|
||||||
const [solutionForm] = Form.useForm();
|
|
||||||
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
|
||||||
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
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
|
// Error code management states
|
||||||
const {
|
const [errorCodeForm] = Form.useForm();
|
||||||
brandId,
|
const [solutionForm] = Form.useForm();
|
||||||
brandInfo,
|
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
||||||
setBrandInfo,
|
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
|
||||||
tempErrorCodes,
|
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
|
||||||
addErrorCode,
|
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
|
||||||
updateErrorCode,
|
const [isAddingNewErrorCode, setIsAddingNewErrorCode] = useState(false);
|
||||||
deleteErrorCode,
|
|
||||||
prepareSubmissionData,
|
|
||||||
validateForm,
|
|
||||||
resetForm,
|
|
||||||
} = useBrandForm();
|
|
||||||
|
|
||||||
// Use step from query parameter or context
|
|
||||||
const tab = searchParams.get('tab');
|
|
||||||
const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
solutionFields,
|
solutionFields,
|
||||||
@@ -77,14 +68,54 @@ const AddBrandDevice = () => {
|
|||||||
// Navigation functions
|
// Navigation functions
|
||||||
const handleNextStep = async () => {
|
const handleNextStep = async () => {
|
||||||
try {
|
try {
|
||||||
await brandForm.validateFields();
|
setConfirmLoading(true);
|
||||||
setCurrentStep(1);
|
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) {
|
} catch (error) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'warning',
|
icon: 'error',
|
||||||
title: 'Perhatian',
|
title: 'Gagal',
|
||||||
message: 'Harap isi semua kolom wajib untuk brand device!',
|
message: error.message || 'Gagal membuat brand device',
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,13 +128,50 @@ const AddBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddErrorCode = () => {
|
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 handleEditErrorCode = (record) => {
|
||||||
const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId;
|
if (createdBrandId) {
|
||||||
if (errorCodeId) {
|
setIsAddingNewErrorCode(false);
|
||||||
navigate(`/master/brand-device/add/error-code/edit/${errorCodeId}`);
|
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',
|
icon: 'question',
|
||||||
title: 'Konfirmasi Hapus',
|
title: 'Konfirmasi Hapus',
|
||||||
message: `Apakah Anda yakin ingin menghapus error code "${record.error_code}"?`,
|
message: `Apakah Anda yakin ingin menghapus error code "${record.error_code}"?`,
|
||||||
onConfirm: () => {
|
onConfirm: async () => {
|
||||||
const tempId = record.tempId || `existing_${record.error_code_id}`;
|
try {
|
||||||
deleteErrorCode(tempId, false); // false = soft delete
|
const errorCodeToDelete = record.error_code_id;
|
||||||
NotifOk({
|
const response = await deleteErrorCode(createdBrandId, errorCodeToDelete);
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
message: 'Error code berhasil dihapus!',
|
NotifOk({
|
||||||
});
|
icon: 'success',
|
||||||
setTrigerFilter(prev => !prev);
|
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: () => {}
|
onCancel: () => {}
|
||||||
});
|
});
|
||||||
@@ -142,8 +228,8 @@ const AddBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
|
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
|
||||||
setBrandInfo(allValues);
|
setBrandData(allValues);
|
||||||
}, [setBrandInfo]);
|
}, []);
|
||||||
|
|
||||||
const getErrorCodesData = async (params) => {
|
const getErrorCodesData = async (params) => {
|
||||||
try {
|
try {
|
||||||
@@ -151,12 +237,33 @@ const AddBrandDevice = () => {
|
|||||||
const page = parseInt(params.get('page')) || 1;
|
const page = parseInt(params.get('page')) || 1;
|
||||||
const limit = parseInt(params.get('limit')) || 10;
|
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) {
|
if (searchText) {
|
||||||
filteredData = allErrorCodes.filter(ec =>
|
allErrorCodes = allErrorCodes.filter(ec =>
|
||||||
ec.error_code.toLowerCase().includes(searchText.toLowerCase()) ||
|
ec.error_code.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
ec.error_code_name.toLowerCase().includes(searchText.toLowerCase())
|
ec.error_code_name.toLowerCase().includes(searchText.toLowerCase())
|
||||||
);
|
);
|
||||||
@@ -164,15 +271,15 @@ const AddBrandDevice = () => {
|
|||||||
|
|
||||||
const startIndex = 0;
|
const startIndex = 0;
|
||||||
const endIndex = startIndex + limit;
|
const endIndex = startIndex + limit;
|
||||||
const paginatedData = filteredData.slice(startIndex, endIndex);
|
const paginatedData = allErrorCodes.slice(startIndex, endIndex);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: paginatedData,
|
data: paginatedData,
|
||||||
pagination: {
|
pagination: {
|
||||||
current_page: page,
|
current_page: page,
|
||||||
current_limit: limit,
|
current_limit: limit,
|
||||||
total_limit: filteredData.length,
|
total_limit: allErrorCodes.length,
|
||||||
total_page: Math.ceil(filteredData.length / limit),
|
total_page: Math.ceil(allErrorCodes.length / limit),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -264,39 +371,120 @@ const AddBrandDevice = () => {
|
|||||||
return params;
|
return params;
|
||||||
}, [searchValue]);
|
}, [searchValue]);
|
||||||
|
|
||||||
const handleFinish = async () => {
|
const resetErrorCodeForm = () => {
|
||||||
setConfirmLoading(true);
|
errorCodeForm.resetFields();
|
||||||
|
errorCodeForm.setFieldsValue({
|
||||||
|
status: true,
|
||||||
|
});
|
||||||
|
setErrorCodeIcon(null);
|
||||||
|
resetSolutionFields();
|
||||||
|
setIsErrorCodeFormReadOnly(false);
|
||||||
|
setEditingErrorCodeKey(null);
|
||||||
|
setSelectedSparepartIds([]);
|
||||||
|
setIsAddingNewErrorCode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveErrorCode = async () => {
|
||||||
try {
|
try {
|
||||||
// Validate form using context
|
await errorCodeForm.validateFields();
|
||||||
const validation = validateForm();
|
|
||||||
if (!validation.isValid) {
|
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;
|
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)) {
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
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 {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Gagal',
|
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) {
|
} catch (error) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Gagal',
|
title: 'Gagal',
|
||||||
message: error.message || 'Gagal menyimpan data. Silakan coba lagi.',
|
message: error.message || 'Terjadi kesalahan. Silakan coba lagi.',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
@@ -391,7 +579,7 @@ const AddBrandDevice = () => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleAddErrorCode}
|
onClick={() => navigate(`/master/brand-device/${createdBrandId}/error-code/add`)}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
Add Error Code
|
Add Error Code
|
||||||
@@ -407,11 +595,11 @@ const AddBrandDevice = () => {
|
|||||||
cardColor={'#42AAFF'}
|
cardColor={'#42AAFF'}
|
||||||
header={'error_code'}
|
header={'error_code'}
|
||||||
showPreviewModal={handlePreviewErrorCode}
|
showPreviewModal={handlePreviewErrorCode}
|
||||||
showEditModal={handleEditErrorCodeNavigate}
|
showEditModal={(record) => navigate(`/master/brand-device/${createdBrandId}/error-code/edit/${record.error_code_id}`)}
|
||||||
showDeleteDialog={handleDeleteErrorCode}
|
showDeleteDialog={handleDeleteErrorCode}
|
||||||
getData={getErrorCodesData}
|
getData={getErrorCodesData}
|
||||||
queryParams={queryParams}
|
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}
|
triger={trigerFilter}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -447,6 +635,12 @@ const AddBrandDevice = () => {
|
|||||||
]);
|
]);
|
||||||
}, [setBreadcrumbItems, navigate]);
|
}, [setBreadcrumbItems, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (createdBrandId && currentStep === 1) {
|
||||||
|
setTrigerFilter(prev => !prev);
|
||||||
|
}
|
||||||
|
}, [createdBrandId, currentStep]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Title level={4} style={{ margin: '0 0 24px 0' }}>
|
<Title level={4} style={{ margin: '0 0 24px 0' }}>
|
||||||
@@ -525,7 +719,7 @@ const AddBrandDevice = () => {
|
|||||||
borderColor: '#23A55A',
|
borderColor: '#23A55A',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save Brand Device
|
Selesai
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -11,10 +11,8 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ArrowLeftOutlined, UploadOutlined } from '@ant-design/icons';
|
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 { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||||
import { useBrandForm } from '../../../context/BrandFormContext';
|
|
||||||
import { uploadFile } from '../../../api/file-uploads';
|
|
||||||
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
|
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
|
||||||
import SolutionForm from './component/SolutionForm';
|
import SolutionForm from './component/SolutionForm';
|
||||||
import { useSolutionLogic } from './hooks/solution';
|
import { useSolutionLogic } from './hooks/solution';
|
||||||
@@ -27,23 +25,12 @@ const AddEditErrorCode = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { brandId: routeBrandId, errorCodeId } = useParams();
|
const { brandId: routeBrandId, errorCodeId } = useParams();
|
||||||
const { setBreadcrumbItems } = useBreadcrumb();
|
const { setBreadcrumbItems } = useBreadcrumb();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
// Use BrandForm context
|
const currentBrandId = routeBrandId;
|
||||||
const {
|
|
||||||
brandId: contextBrandId,
|
|
||||||
routeBrandId: contextRouteBrandId,
|
|
||||||
setRouteBrandId,
|
|
||||||
setErrorCodeId,
|
|
||||||
initializeFromRoute,
|
|
||||||
tempErrorCodes,
|
|
||||||
existingErrorCodes,
|
|
||||||
addErrorCode,
|
|
||||||
updateErrorCode,
|
|
||||||
setCurrentErrorCode
|
|
||||||
} = useBrandForm();
|
|
||||||
|
|
||||||
// Use brandId from context first, fallback to route
|
const isFromAddBrand = location.pathname.includes('/master/brand-device/') && location.pathname.includes('/error-code/') &&
|
||||||
const currentBrandId = contextBrandId || routeBrandId;
|
(location.pathname.includes('/add') || (location.pathname.includes('/edit/') && !location.pathname.includes('/edit/')));
|
||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
const [errorCodeForm] = Form.useForm();
|
const [errorCodeForm] = Form.useForm();
|
||||||
@@ -75,10 +62,6 @@ const AddEditErrorCode = () => {
|
|||||||
const isEditMode = errorCodeId && errorCodeId !== 'add';
|
const isEditMode = errorCodeId && errorCodeId !== 'add';
|
||||||
setIsEdit(isEditMode);
|
setIsEdit(isEditMode);
|
||||||
|
|
||||||
// Initialize context with route parameters
|
|
||||||
if (routeBrandId) {
|
|
||||||
initializeFromRoute(routeBrandId, errorCodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBreadcrumbItems([
|
setBreadcrumbItems([
|
||||||
{
|
{
|
||||||
@@ -114,7 +97,6 @@ const AddEditErrorCode = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (isEditMode && errorCodeId) {
|
if (isEditMode && errorCodeId) {
|
||||||
// For existing error codes, construct the proper tempId format
|
|
||||||
const tempId = errorCodeId.startsWith('existing_') ? errorCodeId : `existing_${errorCodeId}`;
|
const tempId = errorCodeId.startsWith('existing_') ? errorCodeId : `existing_${errorCodeId}`;
|
||||||
loadExistingErrorCode(tempId);
|
loadExistingErrorCode(tempId);
|
||||||
}
|
}
|
||||||
@@ -124,113 +106,56 @@ const AddEditErrorCode = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// console.log(' Looking for error code with tempId:', tempId);
|
let errorIdToUse = tempId;
|
||||||
// console.log(' Available error codes in context:', tempErrorCodes);
|
if (tempId.startsWith('existing_')) {
|
||||||
|
errorIdToUse = tempId.replace('existing_', '');
|
||||||
// Find error code in tempErrorCodes first
|
|
||||||
let existingErrorCode = tempErrorCodes.find(ec => ec.tempId === tempId);
|
|
||||||
|
|
||||||
// If not found, check in existingErrorCodes with format existing_${error_code_id}
|
|
||||||
if (!existingErrorCode && tempId.startsWith('existing_')) {
|
|
||||||
const errorId = tempId.replace('existing_', '');
|
|
||||||
existingErrorCode = existingErrorCodes.find(ec => ec.error_code_id == errorId);
|
|
||||||
if (existingErrorCode) {
|
|
||||||
existingErrorCode = {
|
|
||||||
...existingErrorCode,
|
|
||||||
tempId: tempId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(' Found error code in context:', existingErrorCode);
|
const errorCodeResponse = await getErrorCodeById(errorIdToUse);
|
||||||
|
|
||||||
if (existingErrorCode) {
|
if (errorCodeResponse && errorCodeResponse.statusCode === 200) {
|
||||||
errorCodeForm.setFieldsValue({
|
const errorData = errorCodeResponse.data;
|
||||||
error_code: existingErrorCode.error_code,
|
|
||||||
error_code_name: existingErrorCode.error_code_name || '',
|
|
||||||
error_code_description: existingErrorCode.error_code_description || '',
|
|
||||||
error_code_color: existingErrorCode.error_code_color || '#000000',
|
|
||||||
status: existingErrorCode.is_active !== false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingErrorCode.path_icon) {
|
if (errorData) {
|
||||||
setErrorCodeIcon({
|
errorCodeForm.setFieldsValue({
|
||||||
name: existingErrorCode.path_icon.split('/').pop(),
|
error_code: errorData.error_code,
|
||||||
uploadPath: existingErrorCode.path_icon,
|
error_code_name: errorData.error_code_name || '',
|
||||||
url: existingErrorCode.path_icon,
|
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) {
|
if (errorData.path_icon) {
|
||||||
// console.log('🔍 Setting solutions from context:', existingErrorCode.solution);
|
setErrorCodeIcon({
|
||||||
setSolutionsForExistingRecord(existingErrorCode.solution, solutionForm);
|
name: errorData.path_icon.split('/').pop(),
|
||||||
}
|
uploadPath: errorData.path_icon,
|
||||||
|
url: errorData.path_icon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (existingErrorCode.spareparts && existingErrorCode.spareparts.length > 0) {
|
if (errorData.solution && errorData.solution.length > 0) {
|
||||||
// console.log('🔍 Setting spareparts from context:', existingErrorCode.spareparts);
|
setSolutionsForExistingRecord(errorData.solution, solutionForm);
|
||||||
setSelectedSparepartIds(existingErrorCode.spareparts);
|
}
|
||||||
|
|
||||||
|
if (errorData.spareparts && errorData.spareparts.length > 0) {
|
||||||
|
const sparepartIds = errorData.spareparts.map(sp => sp.sparepart_id);
|
||||||
|
setSelectedSparepartIds(sparepartIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
NotifAlert({
|
||||||
// Extract the actual error_code_id from tempId format
|
icon: 'warning',
|
||||||
if (tempId.startsWith('existing_')) {
|
title: 'Peringatan',
|
||||||
errorIdToUse = tempId.replace('existing_', '');
|
message: 'Error code not found. Creating new error code.',
|
||||||
}
|
});
|
||||||
|
|
||||||
const errorCodeResponse = await getErrorCodeById(errorIdToUse);
|
|
||||||
|
|
||||||
if (errorCodeResponse && errorCodeResponse.statusCode === 200) {
|
|
||||||
const errorData = errorCodeResponse.data;
|
|
||||||
|
|
||||||
if (errorData) {
|
|
||||||
errorCodeForm.setFieldsValue({
|
|
||||||
error_code: errorData.error_code,
|
|
||||||
error_code_name: errorData.error_code_name || '',
|
|
||||||
error_code_description: errorData.error_code_description || '',
|
|
||||||
error_code_color: errorData.error_code_color || '#000000',
|
|
||||||
status: errorData.is_active !== false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errorData.path_icon) {
|
|
||||||
setErrorCodeIcon({
|
|
||||||
name: errorData.path_icon.split('/').pop(),
|
|
||||||
uploadPath: errorData.path_icon,
|
|
||||||
url: errorData.path_icon,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set solutions from API data (include file data)
|
|
||||||
if (errorData.solution && errorData.solution.length > 0) {
|
|
||||||
setSolutionsForExistingRecord(errorData.solution, solutionForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set spareparts from API data
|
|
||||||
if (errorData.spareparts && errorData.spareparts.length > 0) {
|
|
||||||
const sparepartIds = errorData.spareparts.map(sp => sp.sparepart_id);
|
|
||||||
setSelectedSparepartIds(sparepartIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't add to context - this is existing data from API
|
|
||||||
// The context should already have this error code from the brand data loading
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// console.log('🔍 API Response error or not found:', errorCodeResponse);
|
|
||||||
errorCodeForm.setFieldsValue({
|
|
||||||
error_code: '',
|
|
||||||
error_code_name: '',
|
|
||||||
error_code_description: '',
|
|
||||||
error_code_color: '#000000',
|
|
||||||
status: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Peringatan',
|
|
||||||
message: 'Error code not found. Creating new error code.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load error code:', error);
|
console.error('Failed to load error code:', error);
|
||||||
@@ -250,8 +175,9 @@ const AddEditErrorCode = () => {
|
|||||||
|
|
||||||
const solutionValues = solutionForm.getFieldsValue();
|
const solutionValues = solutionForm.getFieldsValue();
|
||||||
|
|
||||||
const firstSolutionPath = `solution_items,${solutionFields[0]?.key || 0}`;
|
const commaPath = `solution_items,${solutionFields[0]?.key || 0}`;
|
||||||
const firstSolution = solutionValues[firstSolutionPath];
|
const dotPath = `solution_items.${solutionFields[0]?.key || 0}`;
|
||||||
|
const firstSolution = solutionValues[commaPath] || solutionValues[dotPath];
|
||||||
|
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
if (firstSolution && firstSolution.name && firstSolution.name.trim() !== '') {
|
if (firstSolution && firstSolution.name && firstSolution.name.trim() !== '') {
|
||||||
@@ -276,55 +202,82 @@ const AddEditErrorCode = () => {
|
|||||||
|
|
||||||
const solutionData = getSolutionData();
|
const solutionData = getSolutionData();
|
||||||
|
|
||||||
// Determine the correct tempId for editing
|
setConfirmLoading(true);
|
||||||
let updateTempId;
|
|
||||||
if (isEdit) {
|
try {
|
||||||
// For existing error codes, find the correct tempId
|
const payload = {
|
||||||
if (errorCodeId && !errorCodeId.startsWith('pending-')) {
|
error_code_name: errorCodeValues.error_code_name,
|
||||||
// Look for existing error code in context
|
error_code_description: errorCodeValues.error_code_description || '',
|
||||||
const existingEc = existingErrorCodes.find(ec => ec.error_code_id == errorCodeId);
|
error_code_color: errorCodeValues.error_code_color || '#000000',
|
||||||
if (existingEc && existingEc.tempId) {
|
path_icon: errorCodeIcon?.path_icon || errorCodeIcon?.uploadPath || '',
|
||||||
updateTempId = existingEc.tempId;
|
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 {
|
} else {
|
||||||
updateTempId = `existing_${errorCodeId}`;
|
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateTempId = errorCodeId;
|
console.log('Error response:', response);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.message || 'Gagal menyimpan error code',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
updateTempId = Date.now().toString();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error saving error code:', 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({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
@@ -334,102 +287,45 @@ const AddEditErrorCode = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
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) => {
|
const handleErrorCodeIconUpload = (iconData) => {
|
||||||
if (!file) return null;
|
console.log('handleErrorCodeIconUpload received:', iconData);
|
||||||
|
|
||||||
try {
|
if (!iconData || !iconData.uploadPath) {
|
||||||
const folder = 'images';
|
console.error('❌ Invalid icon data received:', iconData);
|
||||||
const response = await uploadFile(file, folder);
|
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
|
||||||
const iconData = {
|
|
||||||
name: file.name,
|
|
||||||
uploadPath: response.data.path_document,
|
|
||||||
url: response.data.path_document,
|
|
||||||
};
|
|
||||||
|
|
||||||
setErrorCodeIcon(iconData);
|
|
||||||
NotifOk({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: 'Error code icon uploaded successfully',
|
|
||||||
});
|
|
||||||
return iconData;
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: response?.message || 'Failed to upload error code icon',
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading icon:', error);
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: 'Failed to upload error code icon',
|
|
||||||
});
|
|
||||||
return null;
|
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 = () => {
|
const handleErrorCodeIconRemove = () => {
|
||||||
|
console.log('🗑️ Removing error code icon');
|
||||||
setErrorCodeIcon(null);
|
setErrorCodeIcon(null);
|
||||||
|
console.log('✅ Error code icon removed');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSolutionFileUpload = async (file, solutionKey) => {
|
const handleSolutionFileUpload = (fileObject) => {
|
||||||
if (!file) return null;
|
console.log('Solution file uploaded:', fileObject);
|
||||||
|
console.log('File object path_solution:', fileObject?.path_solution);
|
||||||
try {
|
console.log('File object uploadPath:', fileObject?.uploadPath);
|
||||||
// Determine folder based on file type
|
|
||||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
|
||||||
const folder = ['pdf'].includes(fileExtension) ? 'pdf' : 'images';
|
|
||||||
|
|
||||||
const response = await uploadFile(file, folder);
|
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
|
||||||
const fileData = {
|
|
||||||
name: file.name,
|
|
||||||
uploadPath: response.data.path_document,
|
|
||||||
url: response.data.path_document,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
};
|
|
||||||
|
|
||||||
NotifOk({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: 'Solution file uploaded successfully',
|
|
||||||
});
|
|
||||||
return fileData;
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: response?.message || 'Failed to upload solution file',
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading solution file:', error);
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: 'Failed to upload solution file',
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSolutionFileView = (fileData) => {
|
|
||||||
if (fileData && fileData.url) {
|
|
||||||
window.open(fileData.url, '_blank');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
errorCodeForm.resetFields();
|
errorCodeForm.resetFields();
|
||||||
@@ -504,6 +400,7 @@ const AddEditErrorCode = () => {
|
|||||||
errorCodeIcon={errorCodeIcon}
|
errorCodeIcon={errorCodeIcon}
|
||||||
onErrorCodeIconUpload={handleErrorCodeIconUpload}
|
onErrorCodeIconUpload={handleErrorCodeIconUpload}
|
||||||
onErrorCodeIconRemove={handleErrorCodeIconRemove}
|
onErrorCodeIconRemove={handleErrorCodeIconRemove}
|
||||||
|
isEdit={isEdit}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -541,8 +438,11 @@ const AddEditErrorCode = () => {
|
|||||||
onSolutionTypeChange={handleSolutionTypeChange}
|
onSolutionTypeChange={handleSolutionTypeChange}
|
||||||
onSolutionStatusChange={handleSolutionStatusChange}
|
onSolutionStatusChange={handleSolutionStatusChange}
|
||||||
onSolutionFileUpload={handleSolutionFileUpload}
|
onSolutionFileUpload={handleSolutionFileUpload}
|
||||||
onFileView={handleSolutionFileView}
|
onFileView={(fileData) => {
|
||||||
fileList={fileList}
|
if (fileData && fileData.url) {
|
||||||
|
window.open(fileData.url, '_blank');
|
||||||
|
}
|
||||||
|
}}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -570,6 +470,7 @@ const AddEditErrorCode = () => {
|
|||||||
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
loading={confirmLoading}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#23A55A',
|
backgroundColor: '#23A55A',
|
||||||
borderColor: '#23A55A',
|
borderColor: '#23A55A',
|
||||||
|
|||||||
@@ -19,16 +19,13 @@ import TableList from '../../../components/Global/TableList';
|
|||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
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 { getFileUrl } from '../../../api/file-uploads';
|
||||||
import BrandForm from './component/BrandForm';
|
import BrandForm from './component/BrandForm';
|
||||||
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
|
import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm';
|
||||||
import SolutionForm from './component/SolutionForm';
|
import SolutionForm from './component/SolutionForm';
|
||||||
import FormActions from './component/FormActions';
|
import FormActions from './component/FormActions';
|
||||||
import ListErrorCode from './component/ListErrorCode';
|
|
||||||
import { useSolutionLogic } from './hooks/solution';
|
import { useSolutionLogic } from './hooks/solution';
|
||||||
import { useBrandDeviceLogic } from './hooks/useBrandDeviceLogic';
|
|
||||||
import { useBrandForm } from '../../../context/BrandFormContext';
|
|
||||||
import SingleSparepartSelect from './component/SingleSparepartSelect';
|
import SingleSparepartSelect from './component/SingleSparepartSelect';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
@@ -46,19 +43,9 @@ const EditBrandDevice = () => {
|
|||||||
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
||||||
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
|
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
|
||||||
|
|
||||||
// Context integration
|
// Context integration
|
||||||
const {
|
// Use step from query parameter
|
||||||
routeBrandId,
|
|
||||||
setRouteBrandId,
|
|
||||||
initializeFromRoute,
|
|
||||||
navigateToErrorCodes,
|
|
||||||
editErrorCode,
|
|
||||||
isLoading: contextLoading
|
|
||||||
} = useBrandForm();
|
|
||||||
|
|
||||||
// Use step from query parameter or context
|
|
||||||
const tab = searchParams.get('tab');
|
const tab = searchParams.get('tab');
|
||||||
const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0);
|
const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : 0);
|
||||||
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
|
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
|
||||||
@@ -67,22 +54,9 @@ const EditBrandDevice = () => {
|
|||||||
const [apiErrorCodes, setApiErrorCodes] = useState([]);
|
const [apiErrorCodes, setApiErrorCodes] = useState([]);
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [brandInfo, setBrandInfo] = useState({});
|
||||||
const {
|
const [tempErrorCodes, setTempErrorCodes] = useState([]);
|
||||||
brandId,
|
const [existingErrorCodes, setExistingErrorCodes] = useState([]);
|
||||||
brandInfo,
|
|
||||||
setBrandInfo,
|
|
||||||
setBrandId,
|
|
||||||
tempErrorCodes,
|
|
||||||
existingErrorCodes,
|
|
||||||
addErrorCode,
|
|
||||||
updateErrorCode,
|
|
||||||
deleteErrorCode,
|
|
||||||
setExistingErrorCodes,
|
|
||||||
prepareSubmissionData,
|
|
||||||
validateForm,
|
|
||||||
resetForm,
|
|
||||||
} = useBrandForm();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
solutionFields,
|
solutionFields,
|
||||||
@@ -141,6 +115,7 @@ const EditBrandDevice = () => {
|
|||||||
const brandData = response.data;
|
const brandData = response.data;
|
||||||
|
|
||||||
const brandInfoData = {
|
const brandInfoData = {
|
||||||
|
brand_code: brandData.brand_code,
|
||||||
brand_name: brandData.brand_name,
|
brand_name: brandData.brand_name,
|
||||||
brand_type: brandData.brand_type || '',
|
brand_type: brandData.brand_type || '',
|
||||||
brand_manufacture: brandData.brand_manufacture || '',
|
brand_manufacture: brandData.brand_manufacture || '',
|
||||||
@@ -149,12 +124,11 @@ const EditBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setBrandInfo(brandInfoData);
|
setBrandInfo(brandInfoData);
|
||||||
setBrandId(brandData.brand_id);
|
|
||||||
brandForm.setFieldsValue(brandInfoData);
|
brandForm.setFieldsValue(brandInfoData);
|
||||||
|
|
||||||
if (brandData.brand_id) {
|
if (brandData.brand_id) {
|
||||||
try {
|
try {
|
||||||
const errorCodesResponse = await getErrorCodesByBrandId(brandId || brandData.brand_id);
|
const errorCodesResponse = await getErrorCodesByBrandId(id || brandData.brand_id);
|
||||||
if (errorCodesResponse && errorCodesResponse.statusCode === 200) {
|
if (errorCodesResponse && errorCodesResponse.statusCode === 200) {
|
||||||
const apiErrorData = errorCodesResponse.data || [];
|
const apiErrorData = errorCodesResponse.data || [];
|
||||||
const existingCodes = apiErrorData.map(ec => ({
|
const existingCodes = apiErrorData.map(ec => ({
|
||||||
@@ -199,26 +173,59 @@ const EditBrandDevice = () => {
|
|||||||
setCurrentStep(tab === 'brand' ? 0 : 1);
|
setCurrentStep(tab === 'brand' ? 0 : 1);
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
// Initialize context with route parameters
|
|
||||||
useEffect(() => {
|
|
||||||
if (id) {
|
|
||||||
initializeFromRoute(id);
|
|
||||||
setBrandId(id);
|
|
||||||
}
|
|
||||||
}, [id, initializeFromRoute, setBrandId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentStep === 1 && brandId) {
|
if (currentStep === 1 && id) {
|
||||||
setTrigerFilter(prev => !prev);
|
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 () => {
|
const handleNextStep = async () => {
|
||||||
try {
|
try {
|
||||||
await brandForm.validateFields();
|
await brandForm.validateFields();
|
||||||
const currentBrandId = brandId || id;
|
const currentBrandId = id;
|
||||||
if (currentBrandId) {
|
if (currentBrandId) {
|
||||||
navigateToErrorCodes(currentBrandId);
|
|
||||||
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
|
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -234,44 +241,6 @@ const EditBrandDevice = () => {
|
|||||||
navigate('/master/brand-device');
|
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) => {
|
const handleErrorCodeIconUpload = (iconData) => {
|
||||||
setErrorCodeIcon(iconData);
|
setErrorCodeIcon(iconData);
|
||||||
@@ -415,7 +384,7 @@ const EditBrandDevice = () => {
|
|||||||
|
|
||||||
const handleEditErrorCodeNavigate = (record) => {
|
const handleEditErrorCodeNavigate = (record) => {
|
||||||
const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId;
|
const errorCodeId = record.status === 'existing' ? record.error_code_id : record.tempId;
|
||||||
const currentBrandId = brandId || id;
|
const currentBrandId = id;
|
||||||
if (errorCodeId && currentBrandId) {
|
if (errorCodeId && currentBrandId) {
|
||||||
navigate(`/master/brand-device/${currentBrandId}/error-code/edit/${errorCodeId}`);
|
navigate(`/master/brand-device/${currentBrandId}/error-code/edit/${errorCodeId}`);
|
||||||
}
|
}
|
||||||
@@ -473,8 +442,6 @@ const EditBrandDevice = () => {
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
{text}
|
{text}
|
||||||
{record.status === 'new' && <Tag color="green">New</Tag>}
|
|
||||||
{record.status === 'modified' && <Tag color="orange">Modified</Tag>}
|
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -484,24 +451,6 @@ const EditBrandDevice = () => {
|
|||||||
key: 'error_code_name',
|
key: 'error_code_name',
|
||||||
width: '25%',
|
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',
|
title: 'Status',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
@@ -570,7 +519,7 @@ const EditBrandDevice = () => {
|
|||||||
const search = params.get('search') || '';
|
const search = params.get('search') || '';
|
||||||
const page = parseInt(params.get('page')) || 1;
|
const page = parseInt(params.get('page')) || 1;
|
||||||
const limit = parseInt(params.get('limit')) || 10;
|
const limit = parseInt(params.get('limit')) || 10;
|
||||||
const currentBrandId = brandId || id;
|
const currentBrandId = id;
|
||||||
|
|
||||||
if (!currentBrandId) {
|
if (!currentBrandId) {
|
||||||
console.warn('Brand ID is not available');
|
console.warn('Brand ID is not available');
|
||||||
@@ -767,7 +716,7 @@ const EditBrandDevice = () => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => navigate(`/master/brand-device/${brandId || id}/error-code/add`)}
|
onClick={() => navigate(`/master/brand-device/${id}/error-code/add`)}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
Add Error Code
|
Add Error Code
|
||||||
@@ -811,12 +760,9 @@ const EditBrandDevice = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/master/brand-device/edit/${brandId || id}?tab=brand`)}
|
onClick={() => navigate(`/master/brand-device/edit/${id}?tab=brand`)}
|
||||||
style={{ marginLeft: 8 }}
|
style={{ marginLeft: 8 }}
|
||||||
>
|
>
|
||||||
Back to Brand Info
|
Back to Brand Info
|
||||||
@@ -833,20 +779,19 @@ const EditBrandDevice = () => {
|
|||||||
borderColor: '#23A55A',
|
borderColor: '#23A55A',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Next to Error Codes
|
Error Code
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleFinish}
|
onClick={handleCancel}
|
||||||
loading={confirmLoading}
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#23A55A',
|
backgroundColor: '#23A55A',
|
||||||
borderColor: '#23A55A',
|
borderColor: '#23A55A',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Update Brand Device
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ const BrandForm = ({
|
|||||||
|
|
||||||
<Form.Item label="Brand Code" name="brand_code">
|
<Form.Item label="Brand Code" name="brand_code">
|
||||||
<Input
|
<Input
|
||||||
placeholder={'Auto Fill Brand Code'}
|
|
||||||
disabled={true}
|
disabled={true}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Form, Input, Switch, Upload, Button, Typography, message, ConfigProvider } from 'antd';
|
import React from 'react';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { Form, Input, Switch, Typography, ConfigProvider } from 'antd';
|
||||||
import { uploadFile } from '../../../../api/file-uploads';
|
import FileUploadHandler from './FileUploadHandler';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -11,53 +12,10 @@ const ErrorCodeSimpleForm = ({
|
|||||||
onErrorCodeIconUpload,
|
onErrorCodeIconUpload,
|
||||||
onErrorCodeIconRemove,
|
onErrorCodeIconRemove,
|
||||||
onAddErrorCode,
|
onAddErrorCode,
|
||||||
|
isEdit = false, // Add isEdit prop to check if we're in edit mode
|
||||||
}) => {
|
}) => {
|
||||||
const statusValue = Form.useWatch('status', errorCodeForm);
|
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 = () => {
|
const handleIconRemove = () => {
|
||||||
onErrorCodeIconRemove();
|
onErrorCodeIconRemove();
|
||||||
};
|
};
|
||||||
@@ -69,7 +27,6 @@ const ErrorCodeSimpleForm = ({
|
|||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Form.Item name="status" valuePropName="checked" noStyle>
|
<Form.Item name="status" valuePropName="checked" noStyle>
|
||||||
<Switch
|
<Switch
|
||||||
disabled={isErrorCodeFormReadOnly}
|
|
||||||
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
|
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -83,7 +40,10 @@ const ErrorCodeSimpleForm = ({
|
|||||||
name="error_code"
|
name="error_code"
|
||||||
rules={[{ required: true, message: 'Error code wajib diisi!' }]}
|
rules={[{ required: true, message: 'Error code wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Enter error code" disabled={isErrorCodeFormReadOnly} />
|
<Input
|
||||||
|
placeholder="Enter error code"
|
||||||
|
disabled={isErrorCodeFormReadOnly}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/* Error Name */}
|
{/* Error Name */}
|
||||||
@@ -92,7 +52,7 @@ const ErrorCodeSimpleForm = ({
|
|||||||
name="error_code_name"
|
name="error_code_name"
|
||||||
rules={[{ required: true, message: 'Error name wajib diisi!' }]}
|
rules={[{ required: true, message: 'Error name wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Enter error name" disabled={isErrorCodeFormReadOnly} />
|
<Input placeholder="Enter error name" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/* Error Description */}
|
{/* Error Description */}
|
||||||
@@ -100,126 +60,47 @@ const ErrorCodeSimpleForm = ({
|
|||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="Enter error description"
|
placeholder="Enter error description"
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={isErrorCodeFormReadOnly}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/* Color and Icon in same row */}
|
{/* Color and Icon */}
|
||||||
<Form.Item label="Color & Icon">
|
<Form.Item label="Color & Icon">
|
||||||
<Input.Group compact>
|
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start' }}>
|
||||||
<Form.Item name="error_code_color" noStyle>
|
<Form.Item name="error_code_color" noStyle style={{ flex: '0 0 auto' }}>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
disabled={isErrorCodeFormReadOnly}
|
|
||||||
style={{
|
style={{
|
||||||
width: '30%',
|
width: '120px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid #d9d9d9',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
}}
|
}}
|
||||||
defaultValue="#000000"
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item noStyle style={{ width: '70%', paddingLeft: 8, }}>
|
<Form.Item noStyle style={{ flex: '1 1 auto' }}>
|
||||||
{!isErrorCodeFormReadOnly ? (
|
<FileUploadHandler
|
||||||
<Upload
|
type="error_code"
|
||||||
beforeUpload={handleIconUpload}
|
existingFile={errorCodeIcon}
|
||||||
showUploadList={false}
|
accept="image/*"
|
||||||
accept="image/*"
|
onFileUpload={onErrorCodeIconUpload}
|
||||||
style={{ width: '100%' }}
|
onFileRemove={handleIconRemove}
|
||||||
>
|
buttonText="Upload Icon"
|
||||||
<Button
|
buttonStyle={{
|
||||||
icon={<UploadOutlined />}
|
width: '100%',
|
||||||
style={{
|
borderColor: '#23A55A',
|
||||||
width: '100%',
|
color: '#23A55A',
|
||||||
borderColor: '#23A55A',
|
display: 'flex',
|
||||||
color: '#23A55A',
|
alignItems: 'center',
|
||||||
display: 'flex',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
gap: '8px'
|
||||||
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();
|
|
||||||
}}
|
}}
|
||||||
style={{ width: '100%' }}
|
uploadText="Upload error code icon"
|
||||||
>
|
/>
|
||||||
Simpan Error Code
|
</Form.Item>
|
||||||
</Button>
|
</div>
|
||||||
</ConfigProvider>
|
|
||||||
</Form.Item>
|
</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 React, { useState } from 'react';
|
||||||
import { Upload, Modal } from 'antd';
|
import { Upload, Modal, Button, Typography, Space, Image } from 'antd';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined, EyeOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
|
||||||
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
|
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 = ({
|
const FileUploadHandler = ({
|
||||||
solutionFields,
|
type = 'solution',
|
||||||
fileList,
|
maxCount = 1,
|
||||||
|
accept = '.pdf,.jpg,.jpeg,.png,.gif',
|
||||||
|
disabled = false,
|
||||||
|
|
||||||
|
// File management
|
||||||
|
fileList = [],
|
||||||
onFileUpload,
|
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 [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [previewImage, setPreviewImage] = useState('');
|
const [previewImage, setPreviewImage] = useState('');
|
||||||
const [previewTitle, setPreviewTitle] = useState('');
|
const [previewTitle, setPreviewTitle] = useState('');
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadedFile, setUploadedFile] = useState(null);
|
||||||
|
|
||||||
const getBase64 = (file) =>
|
const getBase64 = (file) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
@@ -22,99 +42,389 @@ const FileUploadHandler = ({
|
|||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUploadPreview = async (file) => {
|
const handlePreview = async (file) => {
|
||||||
const preview = await getBase64(file);
|
if (!file.url && !file.preview) {
|
||||||
setPreviewImage(preview);
|
file.preview = await getBase64(file.originFileObj);
|
||||||
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
}
|
||||||
|
setPreviewImage(file.url || file.preview);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
|
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = async (file) => {
|
const validateFile = (file) => {
|
||||||
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type);
|
const isAllowedType = [
|
||||||
|
'application/pdf',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
].includes(file.type);
|
||||||
|
|
||||||
if (!isAllowedType) {
|
if (!isAllowedType) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: '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 {
|
try {
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
||||||
const fileType = isImage ? 'image' : 'pdf';
|
const fileType = isImage ? 'image' : 'pdf';
|
||||||
const folder = getFolderFromFileType(fileType);
|
const folder = getFolderFromFileType(fileType);
|
||||||
|
|
||||||
const uploadResponse = await uploadFile(file, folder);
|
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) {
|
if (actualPath) {
|
||||||
file.uploadPath = actualPath;
|
let fileObject;
|
||||||
file.solution_name = file.name;
|
|
||||||
file.solutionId = solutionFields[0];
|
if (type === 'error_code') {
|
||||||
file.type_solution = fileType;
|
fileObject = {
|
||||||
onFileUpload(file);
|
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({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `${file.name} berhasil diupload!`
|
message: `${file.name} berhasil diupload!`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setIsUploading(false);
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
console.error('Failed to extract file path from upload response:', uploadResponse);
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Gagal',
|
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) {
|
} catch (error) {
|
||||||
console.error('Error uploading file:', error);
|
console.error('Upload error:', error);
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: '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 = {
|
const uploadProps = {
|
||||||
multiple: true,
|
name: 'file',
|
||||||
accept: '.pdf,.jpg,.jpeg,.png,.gif',
|
multiple: false,
|
||||||
onRemove: onFileRemove,
|
accept,
|
||||||
beforeUpload: handleFileUpload,
|
disabled: disabled || isUploading,
|
||||||
fileList,
|
fileList: [],
|
||||||
onPreview: handleUploadPreview,
|
beforeUpload: () => false,
|
||||||
|
onChange: handleFileChange,
|
||||||
|
onPreview: handlePreview,
|
||||||
|
maxCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ ...containerStyle }}>
|
||||||
<Upload.Dragger {...uploadProps}>
|
{!existingFile && (
|
||||||
<p className="ant-upload-drag-icon">
|
<Upload {...uploadProps}>
|
||||||
<UploadOutlined />
|
{type === 'drag' ? (
|
||||||
</p>
|
<Upload.Dragger>
|
||||||
<p className="ant-upload-text">Click or drag file to this area to upload</p>
|
<p className="ant-upload-drag-icon">
|
||||||
<p className="ant-upload-hint">Support for PDF and image files only</p>
|
<UploadOutlined />
|
||||||
</Upload.Dragger>
|
</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
|
{renderExistingFile()}
|
||||||
open={previewOpen}
|
|
||||||
title={previewTitle}
|
{showPreview && (
|
||||||
footer={null}
|
<Modal
|
||||||
onCancel={() => setPreviewOpen(false)}
|
open={previewOpen}
|
||||||
width="80%"
|
title={previewTitle}
|
||||||
style={{ top: 20 }}
|
footer={null}
|
||||||
>
|
onCancel={() => setPreviewOpen(false)}
|
||||||
{previewImage && (
|
width={600}
|
||||||
<img
|
style={{ top: 100 }}
|
||||||
alt={previewTitle}
|
>
|
||||||
style={{ width: '100%' }}
|
{previewImage && (
|
||||||
src={previewImage}
|
<img
|
||||||
/>
|
alt={previewTitle}
|
||||||
)}
|
style={{ width: '100%' }}
|
||||||
</Modal>
|
src={previewImage}
|
||||||
</>
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,26 +26,12 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
key: 'brand_name',
|
key: 'brand_name',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Type',
|
|
||||||
dataIndex: 'brand_type',
|
|
||||||
key: 'brand_type',
|
|
||||||
width: '15%',
|
|
||||||
render: (text) => text || '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Manufacturer',
|
title: 'Manufacturer',
|
||||||
dataIndex: 'brand_manufacture',
|
dataIndex: 'brand_manufacture',
|
||||||
key: 'brand_manufacture',
|
key: 'brand_manufacture',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Model',
|
|
||||||
dataIndex: 'brand_model',
|
|
||||||
key: 'brand_model',
|
|
||||||
width: '15%',
|
|
||||||
render: (text) => text || '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
@@ -139,12 +125,10 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showPreviewModal = (param) => {
|
const showPreviewModal = (param) => {
|
||||||
// Direct navigation without loading, page will handle its own loading
|
|
||||||
navigate(`/master/brand-device/view/${param.brand_id}`);
|
navigate(`/master/brand-device/view/${param.brand_id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditModal = (param = null) => {
|
const showEditModal = (param = null) => {
|
||||||
// Direct navigation without loading, page will handle its own loading
|
|
||||||
if (param) {
|
if (param) {
|
||||||
navigate(`/master/brand-device/edit/${param.brand_id}`);
|
navigate(`/master/brand-device/edit/${param.brand_id}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -202,7 +186,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
// Auto search when clearing by backspace/delete
|
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
setFormDataFilter({ search: '' });
|
setFormDataFilter({ search: '' });
|
||||||
setTrigerFilter((prev) => !prev);
|
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 React, { useState } from 'react';
|
||||||
import { Form, Input, Button, Switch, Radio, Upload, Typography, Space } from 'antd';
|
import { Form, Input, Button, Switch, Radio, Typography, Space } from 'antd';
|
||||||
import { DeleteOutlined, UploadOutlined, EyeOutlined } from '@ant-design/icons';
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
|
import FileUploadHandler from './FileUploadHandler';
|
||||||
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -22,64 +22,43 @@ const SolutionFieldNew = ({
|
|||||||
onFileView,
|
onFileView,
|
||||||
fileList = []
|
fileList = []
|
||||||
}) => {
|
}) => {
|
||||||
const handleFileUpload = async (file) => {
|
const form = Form.useFormInstance();
|
||||||
try {
|
const existingFile = Form.useWatch([`solution_items,${fieldKey}`, 'fileUpload'], form) ||
|
||||||
const isAllowedType = [
|
Form.useWatch([`solution_items,${fieldKey}`, 'file'], form);
|
||||||
'application/pdf',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/gif',
|
|
||||||
].includes(file.type);
|
|
||||||
|
|
||||||
if (!isAllowedType) {
|
// Get form values for debugging and file data extraction
|
||||||
NotifAlert({
|
const allFormValues = form.getFieldsValue(true);
|
||||||
icon: 'error',
|
const solutionData = allFormValues[`solution_items,${fieldKey}`] || {};
|
||||||
title: 'Error',
|
|
||||||
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
// Extract file data from form values for preview
|
||||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
const getFileFromFormValues = () => {
|
||||||
const fileType = isImage ? 'image' : 'pdf';
|
if (solutionData.fileUpload && typeof solutionData.fileUpload === 'object' && Object.keys(solutionData.fileUpload).length > 0) {
|
||||||
const folder = getFolderFromFileType(fileType);
|
return solutionData.fileUpload;
|
||||||
|
|
||||||
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.`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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 = () => {
|
const renderSolutionContent = () => {
|
||||||
if (solutionType === 'text') {
|
if (solutionType === 'text') {
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[fieldName, 'text']}
|
name={[`solution_items,${fieldKey}`, 'text']}
|
||||||
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
|
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
@@ -93,59 +72,44 @@ const SolutionFieldNew = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (solutionType === 'file') {
|
if (solutionType === 'file') {
|
||||||
const currentFiles = fileList.filter(file => file.solutionId === fieldKey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<FileUploadHandler
|
||||||
name={[fieldName, 'file']}
|
type="solution"
|
||||||
rules={[{ required: true, message: 'File solution wajib diupload!' }]}
|
existingFile={displayFile}
|
||||||
>
|
onFileUpload={(fileObject) => {
|
||||||
<Upload
|
const fileWithKey = {
|
||||||
beforeUpload={handleFileUpload}
|
...fileObject,
|
||||||
showUploadList={false}
|
solutionId: fieldKey
|
||||||
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>
|
|
||||||
|
|
||||||
{currentFiles.length > 0 && (
|
if (onFileUpload && typeof onFileUpload === 'function') {
|
||||||
<div style={{ marginTop: 8 }}>
|
onFileUpload(fileWithKey);
|
||||||
{currentFiles.map((file, index) => (
|
}
|
||||||
<div key={index} style={{
|
|
||||||
display: 'flex',
|
form.setFieldValue([`solution_items,${fieldKey}`, 'fileUpload'], fileWithKey);
|
||||||
justifyContent: 'space-between',
|
form.setFieldValue([`solution_items,${fieldKey}`, 'file'], fileWithKey);
|
||||||
alignItems: 'center',
|
form.setFieldValue([`solution_items,${fieldKey}`, 'type'], 'file');
|
||||||
padding: '4px 8px',
|
}}
|
||||||
border: '1px solid #d9d9d9',
|
onFileRemove={() => {
|
||||||
borderRadius: 4,
|
console.log(`🗑️ Removing file from solution ${fieldKey}`);
|
||||||
marginBottom: 4
|
|
||||||
}}>
|
// Clear file form values only, keep type as file and status active
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
form.setFieldValue([`solution_items,${fieldKey}`, 'fileUpload'], null);
|
||||||
<Text style={{ fontSize: 12 }}>{file.name}</Text>
|
form.setFieldValue([`solution_items,${fieldKey}`, 'file'], null);
|
||||||
<Text type="secondary" style={{ fontSize: 10 }}>
|
|
||||||
({(file.size / 1024).toFixed(1)} KB)
|
// Call parent callback if exists
|
||||||
</Text>
|
if (onFileUpload && typeof onFileUpload === 'function') {
|
||||||
</div>
|
onFileUpload(null);
|
||||||
<Button
|
}
|
||||||
type="text"
|
|
||||||
size="small"
|
console.log(`✅ File removed from solution ${fieldKey} - type and status preserved`);
|
||||||
icon={<EyeOutlined />}
|
}}
|
||||||
onClick={() => onFileView(file.uploadPath, file.type_solution)}
|
disabled={isReadOnly}
|
||||||
/>
|
buttonText={displayFile ? 'Replace File' : 'Upload File'}
|
||||||
</div>
|
buttonStyle={{ width: '100%', fontSize: 12 }}
|
||||||
))}
|
uploadText="Upload solution file"
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,15 +140,16 @@ const SolutionFieldNew = ({
|
|||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<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
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
onStatusChange(fieldKey, checked);
|
onStatusChange(fieldKey, checked);
|
||||||
}}
|
}}
|
||||||
|
defaultChecked={solutionStatus !== false}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: solutionStatus ? '#23A55A' : '#bfbfbf'
|
backgroundColor: solutionStatus !== false ? '#23A55A' : '#bfbfbf'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -193,7 +158,7 @@ const SolutionFieldNew = ({
|
|||||||
color: '#666',
|
color: '#666',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}>
|
}}>
|
||||||
{solutionStatus ? 'Active' : 'Inactive'}
|
{solutionStatus !== false ? 'Active' : 'Inactive'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -215,7 +180,7 @@ const SolutionFieldNew = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[fieldName, 'name']}
|
name={[`solution_items,${fieldKey}`, 'name']}
|
||||||
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
|
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
>
|
>
|
||||||
@@ -226,10 +191,11 @@ const SolutionFieldNew = ({
|
|||||||
style={{ fontSize: 13 }}
|
style={{ fontSize: 13 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[fieldName, 'type']}
|
name={[`solution_items,${fieldKey}`, 'type']}
|
||||||
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
|
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
initialValue={solutionType || 'text'}
|
initialValue={solutionType || 'text'}
|
||||||
@@ -238,13 +204,20 @@ const SolutionFieldNew = ({
|
|||||||
onChange={(e) => onTypeChange(fieldKey, e.target.value)}
|
onChange={(e) => onTypeChange(fieldKey, e.target.value)}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
size="small"
|
size="small"
|
||||||
defaultValue={solutionType || 'text'}
|
|
||||||
>
|
>
|
||||||
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
|
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
|
||||||
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
|
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name={[`solution_items,${fieldKey}`, 'status']}
|
||||||
|
initialValue={solutionStatus !== false ? true : false}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
{renderSolutionContent()}
|
{renderSolutionContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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 { PlusOutlined } from '@ant-design/icons';
|
||||||
import SolutionFieldNew from './SolutionField';
|
import SolutionFieldNew from './SolutionField';
|
||||||
|
|
||||||
@@ -21,83 +21,66 @@ const SolutionForm = ({
|
|||||||
fileList,
|
fileList,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Debug props
|
// console.log('SolutionForm props:', {
|
||||||
console.log('🔍 SolutionForm props:', {
|
// solutionFields,
|
||||||
solutionFields,
|
// solutionTypes,
|
||||||
solutionTypes,
|
// solutionStatuses,
|
||||||
solutionStatuses,
|
// firstSolutionValid,
|
||||||
firstSolutionValid,
|
// onAddSolutionField: typeof onAddSolutionField,
|
||||||
onAddSolutionField: typeof onAddSolutionField,
|
// onRemoveSolutionField: typeof onRemoveSolutionField,
|
||||||
onRemoveSolutionField: typeof onRemoveSolutionField,
|
// checkFirstSolutionValid: typeof checkFirstSolutionValid,
|
||||||
checkFirstSolutionValid: typeof checkFirstSolutionValid,
|
// onSolutionFileUpload: typeof onSolutionFileUpload,
|
||||||
onSolutionFileUpload: typeof onSolutionFileUpload,
|
// onFileView: typeof onFileView,
|
||||||
onFileView: typeof onFileView,
|
// fileList: fileList ? fileList.length : 0
|
||||||
fileList: fileList ? fileList.length : 0
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 0 }}>
|
<div style={{ marginBottom: 0 }}>
|
||||||
<Form
|
<Divider orientation="left">Solution Items</Divider>
|
||||||
form={solutionForm}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{
|
|
||||||
solution_items: [{
|
|
||||||
status: true,
|
|
||||||
type: 'text',
|
|
||||||
}]
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
marginBottom: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Divider orientation="left">Solution Items</Divider>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
maxHeight: '400px',
|
maxHeight: '400px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
paddingRight: '8px'
|
paddingRight: '8px'
|
||||||
}}>
|
}}>
|
||||||
{solutionFields.map((field) => (
|
{solutionFields.map((field, displayIndex) => (
|
||||||
<SolutionFieldNew
|
<SolutionFieldNew
|
||||||
key={field}
|
key={field}
|
||||||
fieldKey={field}
|
fieldKey={field}
|
||||||
fieldName={['solution_items', field]}
|
fieldName={['solution_items', field]}
|
||||||
index={field}
|
index={displayIndex}
|
||||||
solutionType={solutionTypes[field]}
|
solutionType={solutionTypes[field]}
|
||||||
solutionStatus={solutionStatuses[field]}
|
solutionStatus={solutionStatuses[field]}
|
||||||
onTypeChange={onSolutionTypeChange}
|
onTypeChange={onSolutionTypeChange}
|
||||||
onStatusChange={onSolutionStatusChange}
|
onStatusChange={onSolutionStatusChange}
|
||||||
onRemove={() => onRemoveSolutionField(field)}
|
onRemove={() => onRemoveSolutionField(field)}
|
||||||
onFileUpload={onSolutionFileUpload}
|
onFileUpload={onSolutionFileUpload}
|
||||||
onFileView={onFileView}
|
onFileView={onFileView}
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
canRemove={solutionFields.length > 1}
|
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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
|
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
|
||||||
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
|
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
|
||||||
|
|
||||||
// Set default values for the new field
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'name'], '');
|
solutionForm.setFieldValue(['solution_items', newKey, 'name'], '');
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
|
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
|
||||||
@@ -27,7 +26,7 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSolutionFields(prev => prev.filter(field => field.key !== key));
|
setSolutionFields(prev => prev.filter(field => field !== key));
|
||||||
|
|
||||||
// Clean up type and status
|
// Clean up type and status
|
||||||
const newTypes = { ...solutionTypes };
|
const newTypes = { ...solutionTypes };
|
||||||
@@ -41,6 +40,26 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
|
|
||||||
const handleSolutionTypeChange = (key, value) => {
|
const handleSolutionTypeChange = (key, value) => {
|
||||||
setSolutionTypes(prev => ({ ...prev, [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) => {
|
const handleSolutionStatusChange = (key, value) => {
|
||||||
@@ -48,7 +67,7 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetSolutionFields = () => {
|
const resetSolutionFields = () => {
|
||||||
setSolutionFields([{ name: ['solution_items', 0], key: 0 }]);
|
setSolutionFields([0]);
|
||||||
setSolutionTypes({ 0: 'text' });
|
setSolutionTypes({ 0: 'text' });
|
||||||
setSolutionStatuses({ 0: true });
|
setSolutionStatuses({ 0: true });
|
||||||
|
|
||||||
@@ -71,8 +90,10 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const solutionKey = firstField.key || firstField;
|
const solutionKey = firstField.key || firstField;
|
||||||
const solutionPath = `solution_items,${solutionKey}`;
|
// Try both notations for compatibility
|
||||||
const firstSolution = values[solutionPath];
|
const commaPath = `solution_items,${solutionKey}`;
|
||||||
|
const dotPath = `solution_items.${solutionKey}`;
|
||||||
|
const firstSolution = values[commaPath] || values[dotPath];
|
||||||
|
|
||||||
if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') {
|
if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') {
|
||||||
return false;
|
return false;
|
||||||
@@ -86,25 +107,73 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSolutionData = () => {
|
const getSolutionData = () => {
|
||||||
const values = solutionForm.getFieldsValue();
|
const values = solutionForm.getFieldsValue(true);
|
||||||
|
|
||||||
const result = solutionFields.map(key => {
|
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;
|
if (!solution) return null;
|
||||||
|
|
||||||
const validSolution = solution.name && solution.name.trim() !== '';
|
const validSolution = solution.name && solution.name.trim() !== '';
|
||||||
|
|
||||||
if (validSolution) {
|
if (!validSolution) return null;
|
||||||
return {
|
|
||||||
solution_name: solution.name || 'Default Solution',
|
let pathSolution = '';
|
||||||
type_solution: solutionTypes[key] || 'text',
|
let fileObject = null;
|
||||||
text_solution: solution.text || '',
|
|
||||||
path_solution: solution.file || '',
|
if (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0) {
|
||||||
is_active: solution.status !== false,
|
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);
|
}).filter(Boolean);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -123,17 +192,44 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
|
|
||||||
solutions.forEach((solution, index) => {
|
solutions.forEach((solution, index) => {
|
||||||
const key = solution.id || 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] = {
|
solutionsValues[key] = {
|
||||||
name: solution.solution_name || '',
|
name: solution.solution_name || '',
|
||||||
type: solution.type_solution || 'text',
|
type: isFileType ? 'file' : 'text',
|
||||||
text: solution.text_solution || '',
|
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;
|
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 = {};
|
const formValues = {};
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
Object.keys(solutionsValues).forEach(key => {
|
||||||
const solution = solutionsValues[key];
|
const solution = solutionsValues[key];
|
||||||
@@ -142,10 +238,12 @@ export const useSolutionLogic = (solutionForm) => {
|
|||||||
type: solution.type,
|
type: solution.type,
|
||||||
text: solution.text,
|
text: solution.text,
|
||||||
file: solution.file,
|
file: solution.file,
|
||||||
status: solution.is_active !== false
|
fileUpload: solution.fileUpload,
|
||||||
|
status: solution.status
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('🔧 Setting form values:', formValues);
|
||||||
form.setFieldsValue(formValues);
|
form.setFieldsValue(formValues);
|
||||||
setSolutionTypes(newTypes);
|
setSolutionTypes(newTypes);
|
||||||
setSolutionStatuses(newStatuses);
|
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