diff --git a/cope2n-ai-fi/configs/sdsap_sbt/configs.py b/cope2n-ai-fi/configs/sdsap_sbt/configs.py index 7120915..288a546 100755 --- a/cope2n-ai-fi/configs/sdsap_sbt/configs.py +++ b/cope2n-ai-fi/configs/sdsap_sbt/configs.py @@ -29,7 +29,7 @@ kvu_model = { "option": "sbt_v2", "model": { "pretrained_model_path": "/workspace/cope2n-ai-fi/weights/layoutxlm-base", - "config": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding_for_sbt-20240503-182151_1/base.yaml", - "checkpoint": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding_for_sbt-20240503-182151_1/checkpoints/best_model.pth" + "config": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding_for_sbt-20240506-175534/base.yaml", + "checkpoint": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding_for_sbt-20240506-175534/checkpoints/best_model.pth" } } \ No newline at end of file diff --git a/cope2n-ai-fi/modules/sdsvkvu b/cope2n-ai-fi/modules/sdsvkvu index ec0d9a0..f5604a9 160000 --- a/cope2n-ai-fi/modules/sdsvkvu +++ b/cope2n-ai-fi/modules/sdsvkvu @@ -1 +1 @@ -Subproject commit ec0d9a0a5a8c17c9a78165d1f78103951a2e138b +Subproject commit f5604a92edcf8fd9021397b047c946a03dbb9e5a diff --git a/cope2n-api/fwd_api/celery_worker/internal_task.py b/cope2n-api/fwd_api/celery_worker/internal_task.py index ec53777..90a7a68 100755 --- a/cope2n-api/fwd_api/celery_worker/internal_task.py +++ b/cope2n-api/fwd_api/celery_worker/internal_task.py @@ -127,7 +127,7 @@ def process_csv_feedback(csv_file_path, feedback_id): print(f"[ERROR] image.doc_type: {image.doc_type} - image.index_in_request: {image.index_in_request} - time_cost: {time_cost} - {e}") if not validate_feedback_file(_feedback_result, _predict_result): status[request_id] = "Missalign imei number between feedback and predict" - continue + # continue if image.doc_type == "invoice": _predict_result["imei_number"] = [] if _feedback_result: @@ -136,7 +136,7 @@ def process_csv_feedback(csv_file_path, feedback_id): else: try: _predict_result = {"retailername": None, "sold_to_party": None, "invoice_no": None, "purchase_date": [], "imei_number": [_predict_result["imei_number"][image.index_in_request]]} - _feedback_result = {"retailername": None, "sold_to_party": None, "invoice_no": None, "purchase_date": None, "imei_number": [_feedback_result["imei_number"][image.index_in_request]]} if _feedback_result else None + _feedback_result = {"retailername": None, "sold_to_party": None, "invoice_no": None, "purchase_date": None, "imei_number": [_feedback_result["imei_number"][image.index_in_request]]} if _feedback_result and len(_feedback_result["imei_number"]) > image.index_in_request else None except Exception as e: print (f"[ERROR]: {request_id} - {e}") image.predict_result = _predict_result diff --git a/cope2n-api/fwd_api/utils/accuracy.py b/cope2n-api/fwd_api/utils/accuracy.py index d5954c4..ceced56 100755 --- a/cope2n-api/fwd_api/utils/accuracy.py +++ b/cope2n-api/fwd_api/utils/accuracy.py @@ -529,6 +529,7 @@ def validate_feedback_file(feedback, predict): num_imei_predict = len(predict.get("imei_number", [])) if num_imei_feedback != num_imei_predict: return False + feedback["imei_number"] = imei_feedback return True def first_of_list(the_list): diff --git a/cope2n-api/fwd_api/utils/sdsvkvu b/cope2n-api/fwd_api/utils/sdsvkvu index ec0d9a0..f5604a9 160000 --- a/cope2n-api/fwd_api/utils/sdsvkvu +++ b/cope2n-api/fwd_api/utils/sdsvkvu @@ -1 +1 @@ -Subproject commit ec0d9a0a5a8c17c9a78165d1f78103951a2e138b +Subproject commit f5604a92edcf8fd9021397b047c946a03dbb9e5a diff --git a/cope2n-fe/Dockerfile b/cope2n-fe/Dockerfile index f0b31ef..6a0f99c 100644 --- a/cope2n-fe/Dockerfile +++ b/cope2n-fe/Dockerfile @@ -2,7 +2,7 @@ FROM node:21-alpine AS build WORKDIR /app/ COPY --chown=node:node package*.json ./ -RUN npm install -g npm@10.4.0 && npm install +RUN yarn COPY --chown=node:node . . RUN npm run build RUN npm cache clean --force diff --git a/cope2n-fe/src/pages/reviews2/FileCard.tsx b/cope2n-fe/src/pages/reviews2/FileCard.tsx index 241989a..1e76926 100644 --- a/cope2n-fe/src/pages/reviews2/FileCard.tsx +++ b/cope2n-fe/src/pages/reviews2/FileCard.tsx @@ -1,75 +1,86 @@ -import { DownloadOutlined } from '@ant-design/icons'; -import { Button } from 'antd'; +import { + CheckCircleOutlined, + DownloadOutlined, + ExclamationCircleOutlined, +} from '@ant-design/icons'; +import { Button, Tag } from 'antd'; const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { const fileName = file['File Name']; - + let status = true; + if (file['Is Required'] && !file['Is Reviewed']) { + status = false; + } return (
-

+

+ {file['Doc Type'].toUpperCase()} +

+ + {fileName + ? fileName.substring(0, 25).replace('temp_', '') + : fileName} + +
+ : + } + style={{ display: 'inline-block', fontWeight: 'bold' }} + // bordered={false} > - {file['Doc Type'].toUpperCase()} -

- - {fileName ? fileName.substring(0, 25).replace('temp_', '') : fileName} - + {status ? 'Good' : 'Warning'}{' '} +
-
{ + const downloadUrl = file['File URL']; + window.open(downloadUrl, '_blank'); }} > - - -
+ + ); }; diff --git a/cope2n-fe/src/pages/reviews2/api.ts b/cope2n-fe/src/pages/reviews2/api.ts index a8c1885..75def5b 100644 --- a/cope2n-fe/src/pages/reviews2/api.ts +++ b/cope2n-fe/src/pages/reviews2/api.ts @@ -43,7 +43,6 @@ export const fetchAllRequests = async ( export const updateRevisedData = async ( requestID: any, - newRevisedData: any, ) => { // const requestID = ; const token = localStorage.getItem('sbt-token') || ''; @@ -53,9 +52,7 @@ export const updateRevisedData = async ( Authorization: `${JSON.parse(token)}`, 'Content-Type': 'application/json', }, - body: JSON.stringify({ - reviewed_result: newRevisedData, - }), + body: JSON.stringify({"request_file_results": []}), }).catch((error) => { console.log(error); throw error; @@ -65,6 +62,30 @@ export const updateRevisedData = async ( } }; +export const updateRevisedDataByFile = async ( + requestID: any, + fileID: any, + newRevisedData: any, +) => { + // const requestID = ; + const token = localStorage.getItem('sbt-token') || ''; + const result = await fetch(`${baseURL}/ctel/request_image/${requestID}/${fileID}/`, { + method: 'POST', + headers: { + Authorization: `${JSON.parse(token)}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newRevisedData), + }).catch((error) => { + console.log(error); + throw error; + }); + if (result.status != 200) { + throw new Error('Could not update revised data'); + } +}; + + export const fetchRequest = async (id) => { const token = localStorage.getItem('sbt-token') || ''; const response = await fetch(`${baseURL}/ctel/request/${id}/`, { diff --git a/cope2n-fe/src/pages/reviews2/const.ts b/cope2n-fe/src/pages/reviews2/const.ts index e4b1f1d..0122ac5 100644 --- a/cope2n-fe/src/pages/reviews2/const.ts +++ b/cope2n-fe/src/pages/reviews2/const.ts @@ -1,4 +1,4 @@ -import { t } from "@lingui/macro"; +import { t } from '@lingui/macro'; export const counter_measure_map = { invalid_image: 'Remove this image from the evaluation report', @@ -23,9 +23,9 @@ export const REASON_BAD_QUALITY = [ { value: 'wrong_feedback', label: t`Wrong Feedback` }, { value: 'ocr_cannot_extract', label: t`Ocr cannot extract` }, { value: 'other', label: t`Other` }, -] +]; -export const SOLUTION_BAD_QUALITY =[ +export const SOLUTION_BAD_QUALITY = [ { value: 'Remove this image from the evaluation report', label: t`Remove this image from the evaluation report`, @@ -36,7 +36,7 @@ export const SOLUTION_BAD_QUALITY =[ label: t`Update revised result and re-calculate accuracy`, }, { value: 'other', label: t`Other` }, -] +]; export const SUBSIDIARIES = [ { value: 'SEAO', label: 'SEAO' }, @@ -46,4 +46,25 @@ export const SUBSIDIARIES = [ { value: 'SEPCO', label: 'SEPCO' }, { value: 'TSE', label: 'TSE' }, { value: 'SEIN', label: 'SEIN' }, -] \ No newline at end of file +]; + +export const SOURCE_KEYS = [ + 'retailername', + 'sold_to_party', + 'invoice_no', + 'purchase_date', + 'imei_number', +]; + +export const FEEDBACK_RESULT = 'Feedback Result'; +export const FEEDBACK_ACCURACY = 'Feedback Accuracy'; +export const PREDICTED_RESULT = 'Predicted Result'; +export const REVIEWED_ACCURACY = 'Reviewed Accuracy'; +export const REVIEWED_RESULT = 'Reviewed Result'; +export const SOURCE_OBJECT_NAMES = [ + FEEDBACK_RESULT, + FEEDBACK_ACCURACY, + PREDICTED_RESULT, + REVIEWED_ACCURACY, + REVIEWED_RESULT +]; diff --git a/cope2n-fe/src/pages/reviews2/index.tsx b/cope2n-fe/src/pages/reviews2/index.tsx index 46a11bf..fca81ca 100644 --- a/cope2n-fe/src/pages/reviews2/index.tsx +++ b/cope2n-fe/src/pages/reviews2/index.tsx @@ -26,17 +26,27 @@ import { useEffect, useState } from 'react'; import Lightbox from 'react-awesome-lightbox'; import 'react-awesome-lightbox/build/style.css'; import { useHotkeys } from 'react-hotkeys-hook'; -import { baseURL } from 'request/api'; // Import the styles import '@react-pdf-viewer/core/lib/styles/index.css'; import { badQualityReasonSubmit } from 'request'; -import { normalizeData } from 'utils/field-value-process'; -import { fetchAllRequests, fetchRequest } from './api'; +import { getErrorMessage } from 'utils/error-handler'; +import { + fetchAllRequests, + fetchRequest, + updateRevisedData, + updateRevisedDataByFile, +} from './api'; import { counter_measure_map, + FEEDBACK_ACCURACY, + FEEDBACK_RESULT, + PREDICTED_RESULT, REASON_BAD_QUALITY, + REVIEWED_RESULT, SOLUTION_BAD_QUALITY, + SOURCE_KEYS, + SOURCE_OBJECT_NAMES, SUBSIDIARIES, } from './const'; import FileCard from './FileCard'; @@ -48,7 +58,9 @@ const ReviewPage = () => { const [isReasonModalOpen, setIsReasonModalOpen] = useState(false); const [selectedFileId, setSelectedFileId] = useState(0); const [selectedFileData, setSelectedFileData] = useState(null); + const [selectedFileDataSource, setSelectedFileDataSource] = useState({}); const [selectedFileName, setSelectedFileName] = useState(null); + const [disableUpdateFileBtn, setDisableUpdateFileBtn] = useState(false); // Default date range: 1 month ago to today const [filterDateRange, setFilterDateRange] = useState(['', '']); @@ -62,7 +74,6 @@ const ReviewPage = () => { const [currentRequestIndex, setCurrentRequestIndex] = useState(1); const [hasNextRequest, setHasNextRequest] = useState(true); const [totalRequests, setTotalPages] = useState(0); - const [dataSource, setDataSource] = useState([]); const [pageIndexToGoto, setPageIndexToGoto] = useState(1); @@ -78,6 +89,18 @@ const ReviewPage = () => { } }, [reason]); + const updateSelectedFileDataSource = (fileContent) => { + let tempData = {}; + SOURCE_KEYS.forEach((k) => { + tempData[k] = {}; + SOURCE_OBJECT_NAMES.forEach((name) => { + tempData[k][name] = fileContent[name][k]; + }); + }); + + setSelectedFileDataSource(tempData); + }; + const setAndLoadSelectedFile = async (requestData, index) => { setSelectedFileId(index); if (!requestData['Files'][index]) { @@ -87,19 +110,38 @@ const ReviewPage = () => { } const fileName = requestData['Files'][index]['File Name']; const fileURL = requestData['Files'][index]['File URL']; + let fileDataSource = requestData['Files'][index]; + updateSelectedFileDataSource(fileDataSource); + let reason = fileDataSource.Reason; + let solution = fileDataSource['Counter Measures']; + + if (!reason) { + setReason(null); + } else if (REASON_BAD_QUALITY.some((r) => r.value === reason)) { + setReason(reason); + } else { + setReason('other'); + setOtherReason(reason); + } + if (!solution) { + setSolution(null); + } else if (SOLUTION_BAD_QUALITY.some((r) => r.value === solution)) { + setSolution(solution); + } else { + setSolution('other'); + setOtherSolution(solution); + } + + setSelectedFileName(fileName); const response = await fetch(fileURL); if (response.status === 200) { - setSelectedFileName(fileName); setSelectedFileData(fileURL); - console.log('Loading file: ' + fileName); - console.log('URL: ' + fileURL); } else { setSelectedFileData('FAILED_TO_LOAD_FILE'); setImageLoading(false); } }; - console.log(dataSource); const loadCurrentRequest = (requestIndex) => { setLoading(true); setImageLoading(true); @@ -122,25 +164,7 @@ const ReviewPage = () => { ); requestData .then(async (data) => { - console.log('🚀 ~ .then ~ data:', data); if (data) setCurrentRequest(data); - const predicted = - data && data['Predicted Result'] ? data['Predicted Result'] : {}; - const submitted = - data && data['Feedback Result'] ? data['Feedback Result'] : {}; - const revised = - data && data['Reviewed Result'] ? data['Reviewed Result'] : {}; - const keys = Object.keys(predicted); - const tableRows = []; - for (let i = 0; i < keys.length; i++) { - let instance = {}; - instance['key'] = keys[i]; - instance['predicted'] = predicted[keys[i]]; - instance['submitted'] = submitted[keys[i]]; - instance['revised'] = revised[keys[i]]; - tableRows.push(instance); - } - setDataSource(tableRows); setAndLoadSelectedFile(data, 0); }) .finally(() => { @@ -222,80 +246,58 @@ const ReviewPage = () => { }); }, []); - // "Key", "Accuracy", "Submitted", "Revised" - interface DataType { - key: string; - accuracy: number; - submitted: string; - revised: string; - } - - const updateRevisedData = async (newRevisedData: any) => { - const requestID = currentRequest.RequestID; - const token = localStorage.getItem('sbt-token') || ''; - const result = await fetch(`${baseURL}/ctel/request/${requestID}/`, { - method: 'POST', - headers: { - Authorization: `${JSON.parse(token)}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - reviewed_result: newRevisedData, - }), - }).catch((error) => { - console.log(error); - throw error; - }); - if (result.status != 200) { - throw new Error('Could not update revised data'); - } - }; - - const handleSave = (row: DataType) => { - const newData = [...dataSource]; - const index = newData.findIndex((item) => row.key === item.key); - const item = newData[index]; - newData.splice(index, 1, { - ...item, - ...row, - }); - const newRevisedData = {}; - for (let i = 0; i < newData.length; i++) { - newData[i].revised = normalizeData(newData[i].key, newData[i].revised); - newRevisedData[newData[i].key] = newData[i].revised; - } - updateRevisedData(newRevisedData) - .then(() => { - // "[Is Reviewed]" => true + const handleConfirmReview = async () => { + const isConfirmed = window.confirm( + 'Are you sure you want to confirm this request is reviewed?', + ); + if (isConfirmed) { + try { + await updateRevisedData(currentRequest?.RequestID); setCurrentRequest({ ...currentRequest, ['Is Reviewed']: true, }); - }) - .then(() => { - setDataSource(newData); - }) - .catch((error) => { - message.error( - 'Could not update revised data. Please check the format.', - ); - }); + notification.success({ message: 'Update file success' }); + } catch (error) { + notification.error({ + message: getErrorMessage(error), + }); + } + } }; const submitRevisedData = async () => { - const newData = [...dataSource]; - const newRevisedData = {}; - for (let i = 0; i < newData.length; i++) { - newData[i].revised = normalizeData(newData[i].key, newData[i].revised); - newRevisedData[newData[i].key] = newData[i].revised; - } - updateRevisedData(newRevisedData).then(() => { - // "[Is Reviewed]" => true - setCurrentRequest({ - ...currentRequest, - ['Is Reviewed']: true, - }); + const fileId = selectedFileName.split('.')[0]; + + let request_file_result = {}; + SOURCE_KEYS.forEach((k) => { + request_file_result[k] = selectedFileDataSource[k][REVIEWED_RESULT]; }); + let data = { + request_file_result, + reason: reason ? reason : otherReason, + solution: solution ? solution : otherSolution, + }; + try { + await updateRevisedDataByFile(currentRequest?.RequestID, fileId, data); + + let newData = currentRequest; + newData.Files[selectedFileId]['Is Reviewed'] = true; + + setCurrentRequest({ + ...newData, + }); + notification.success({ message: 'Update file success' }); + + const requestData = await fetchRequest( + currentRequest?.RequestID, + ); + setCurrentRequest(requestData) + } catch (error) { + notification.error({ + message: getErrorMessage(error), + }); + } }; // use left/right keys to navigate @@ -308,6 +310,25 @@ const ReviewPage = () => { const [lightBox, setLightBox] = useState(false); + const updateRevised = (fieldName) => { + setSelectedFileDataSource((prevData) => { + prevData[fieldName][REVIEWED_RESULT] = + prevData[fieldName][FEEDBACK_RESULT]; + return { + ...prevData, + }; + }); + }; + + const handleUpdateFileInField = (fieldName, fieldValue) => { + setSelectedFileDataSource((prevData) => { + prevData[fieldName][REVIEWED_RESULT] = fieldValue; + return { + ...prevData, + }; + }); + }; + return (
{ size='large' />
-
-
- - {totalRequests ? ( - <> -    Request ID: {currentRequest?.RequestID} - - ) : ( - '' - )} -
-
- - -
-
+
- {totalRequests > 0 && ( -
-
-

- Files ({currentRequest?.Files?.length}) -

- {currentRequest?.Files.map((file, index) => ( - { - setAndLoadSelectedFile(currentRequest, index); - setImageLoading(true); - }} - setIsReasonModalOpen={setIsReasonModalOpen} - /> - ))} -
- {totalRequests > 0 && ( -
- Request ID - - Redemption - - Created date - - Request time - - Processing time - - Raw accuracy - -
- )} -
- )} -
-
+
+ + {totalRequests && ( +
+
+ Request ID:   + {currentRequest?.RequestID} +
{' '} +
+ Created at:   + {currentRequest?.created_at} +
{' '} +
+ Request time:   + {currentRequest?.['Client Request Time (ms)']} +
{' '} +
+ Redemption ID:   + {currentRequest?.RedemptionID} +
{' '} +
+ Raw accuracy:   + {currentRequest?.raw_accuracy} +
{' '} +
+ Processing time:   + {currentRequest?.['Server Processing Time (ms)']} +
{' '} +
+ )} +
+ {totalRequests > 0 && ( +
+

+ Files ({currentRequest?.Files?.length}) +

+ {currentRequest?.Files.map((file, index) => ( + { + setAndLoadSelectedFile(currentRequest, index); + setImageLoading(true); + }} + setIsReasonModalOpen={setIsReasonModalOpen} + /> + ))} +
+ )}
@@ -685,20 +624,67 @@ const ReviewPage = () => { />
+
-
- {dataSource?.map((data) => { +
+ + +
+
+ {SOURCE_KEYS?.map((data) => { + let shouldRevised = false; + try { + if ( + selectedFileDataSource[data]?.[FEEDBACK_ACCURACY].length > 0 + ) { + shouldRevised = + selectedFileDataSource[data][FEEDBACK_ACCURACY][0] < 1; + } + } catch (error) {} return (
{ margin: '0 0 4px', }} > -

{data.key}

+

{data}

handleUpdateFileInField(data, e.target.value)} />
); })} + {t`Bad image reason:`} +
+ { + setOtherReason(e.target.value); + }} + /> + )} +
+ {t`Solution:`} +
+ { + setOtherSolution(e.target.value); + }} + /> + )} +
+
+
+
-