From 1f4e1fa8744a3a049afe4fcdeee8d46289faf87d Mon Sep 17 00:00:00 2001 From: dx-tan Date: Thu, 7 Mar 2024 09:12:44 +0700 Subject: [PATCH 01/15] Fix: #60 FE --- cope2n-fe/src/models/report.ts | 1 + cope2n-fe/src/pages/reports/index.tsx | 26 ++++++++++++++++++++++++++ cope2n-fe/src/request/report.ts | 3 ++- docker-compose-dev.yml | 16 ++++++++-------- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/cope2n-fe/src/models/report.ts b/cope2n-fe/src/models/report.ts index 23db378..58b8af2 100644 --- a/cope2n-fe/src/models/report.ts +++ b/cope2n-fe/src/models/report.ts @@ -65,6 +65,7 @@ export interface MakeReportParams { start_date: string; end_date: string; subsidiary: string; + report_type: string; } export type CustomUseMutationOptions< diff --git a/cope2n-fe/src/pages/reports/index.tsx b/cope2n-fe/src/pages/reports/index.tsx index ee53149..8418cd9 100644 --- a/cope2n-fe/src/pages/reports/index.tsx +++ b/cope2n-fe/src/pages/reports/index.tsx @@ -10,6 +10,7 @@ import { useState } from 'react'; export interface ReportFormValues { dateRange: [Dayjs, Dayjs]; subsidiary: string; + reportType: string; } const ReportsPage = () => { @@ -30,6 +31,7 @@ const ReportsPage = () => { end_date: values.dateRange[1].format('YYYY-MM-DD'), start_date: values.dateRange[0].format('YYYY-MM-DD'), subsidiary: values.subsidiary, + report_type: values.reportType, }) .then((data) => { if (!!data && data?.report_id) { @@ -51,6 +53,8 @@ const ReportsPage = () => { setIsModalOpen(false); }; + form.setFieldsValue({reportType: "accuracy"}) + return ( <> { ]} /> + + + { message: 'Please select a type', }, ]} + initialValue={'accuracy'} > @@ -137,6 +150,7 @@ const ReportsPage = () => { initialValue={'accuracy'} > + + ) : ( +
+ {children} +
+ ); + } + + return {childNode}; +}; + +// type EditableTableProps = Parameters[0]; + +const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { + const fileName = file["File Name"]; + + return ( +
+
+ {file["Doc Type"].toUpperCase()} + + {fileName ? fileName.substring(0, 25).replace("temp_", "") : fileName} + +
+
+ + +
+
+ ); + +}; + +const fetchRequest = async (id) => { + const token = localStorage.getItem('sbt-token') || ''; + const response = await fetch(`${baseURL}/ctel/request/${id}/`, { + method: 'GET', + headers: { + "Authorization": `${JSON.parse(token)}` + } + }); + return await (await response.json()).subscription_requests[0]; +}; + const InferencePage = () => { const [invoiceFiles, setInvoiceFiles] = useState([]); const [imei1Files, setImei1Files] = useState([]); const [imei2Files, setImei2Files] = useState([]); const [uploading, setUploading] = useState(false); - const [jsonData, setJsonData] = useState({}); + const [responseData, setResponseData] = useState(null); const [finishedProcessing, setFinishedProcessing] = useState(false); const handleUpload = () => { @@ -31,9 +236,7 @@ const InferencePage = () => { } formData.append('is_test_request', 'true'); setUploading(true); - setJsonData({ - "message": "Please wait..." - }) + setResponseData(null); const token = localStorage.getItem('sbt-token') || ''; fetch(`${baseURL}/ctel/images/process_sync/`, { method: 'POST', @@ -42,112 +245,443 @@ const InferencePage = () => { "Authorization": `${JSON.parse(token)}` } }) - .then(async(res) => { + .then(async (res) => { const data = await res.json(); - setJsonData(data); + if (data["status"] != "200") { + setResponseData(null); + return; + } + loadRequestById(data["request_id"]); setFinishedProcessing(true); return data; }) .then(() => { message.success('Upload successfully.'); }) - .catch(() => { + .catch((e) => { + console.log(e); message.error('Upload failed.'); }) .finally(() => { setUploading(false); }); }; + const [loading, setLoading] = useState(false); + const [isReasonModalOpen, setIsReasonModalOpen] = useState(false); + const [selectedFileId, setSelectedFileId] = useState(0); + const [selectedFileData, setSelectedFileData] = useState(null); + const [selectedFileName, setSelectedFileName] = useState(null); + const [currentRequest, setCurrentRequest] = useState(null); + const [totalRequests, setTotalPages] = useState(0); + const [dataSource, setDataSource] = useState([]); + + const setAndLoadSelectedFile = async (requestData, index) => { + setSelectedFileId(index); + if (!requestData["Files"][index]) { + setSelectedFileData("FAILED_TO_LOAD_FILE"); + return; + }; + const fileName = requestData["Files"][index]["File Name"]; + const fileURL = requestData["Files"][index]["File URL"]; + 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"); + } + }; + + const loadRequestById = (requestID) => { + setLoading(true); + const requestData = fetchRequest(requestID); + requestData.then(async (data) => { + if (data) setCurrentRequest(data); + const predicted = (data && data["Reviewed Result"]) ? data["Reviewed 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["revised"] = revised[keys[i]]; + tableRows.push(instance); + } + setDataSource(tableRows); + setLoading(false); + setAndLoadSelectedFile(data, 0); + }).finally(() => { + setLoading(false); + }); + }; + + const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, + }; + + // "Key", "Accuracy", "Revised" + interface DataType { + key: string; + accuracy: number; + revised: string; + }; + + const updateRevisedData = async (newRevisedData: any) => { + const requestID = newRevisedData.request_id; + const token = localStorage.getItem('sbt-token') || ''; + 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); + message.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, + }); + setDataSource(newData); + const newRevisedData = {}; + for (let i = 0; i < newData.length; i++) { + newRevisedData[newData[i].key] = newData[i].revised; + } + updateRevisedData(newRevisedData).then(() => { + // "[Is Reviewed]" => true + setCurrentRequest({ + ...currentRequest, + ["Is Reviewed"]: true, + }) + }) + }; + + const submitRevisedData = async () => { + const newData = [...dataSource]; + const newRevisedData = {}; + for (let i = 0; i < newData.length; i++) { + newRevisedData[newData[i].key] = newData[i].revised; + } + console.log(currentRequest) + updateRevisedData(newRevisedData).then(() => { + // "[Is Reviewed]" => true + setCurrentRequest({ + ...currentRequest, + ["Is Reviewed"]: true, + }) + }) + }; + + + const defaultColumns = [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + width: 200, + }, + { + title: 'Predicted', + dataIndex: 'predicted', + key: 'predicted', + }, + { + title: (
Revised   + {ENABLE_REVIEW && } +
), + dataIndex: 'revised', + key: 'revised', + editable: ENABLE_REVIEW, + }, + ]; + + + const columns = defaultColumns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record: DataType) => ({ + record, + editable: col.key != "request_id" && col.editable, + dataIndex: col.dataIndex, + title: col.title, + handleSave, + }), + }; + }); + const fileExtension = selectedFileName ? selectedFileName.split('.').pop() : ''; return ( <> - -

- {t`Upload files to process. The requests here will not be used in accuracy or payment calculations.`} -

- { - if (finishedProcessing) return; - setInvoiceFiles([]) - }} - beforeUpload={(file) => { - if (finishedProcessing) return; - setInvoiceFiles([file]) - return false; - }} - fileList={invoiceFiles} + +
+
+ { + if (finishedProcessing) return; + setInvoiceFiles([]) + }} + beforeUpload={(file) => { + if (finishedProcessing) return; + setInvoiceFiles([file]) + return false; + }} + fileList={invoiceFiles} + > + Invoice: + +
+
+ { + if (finishedProcessing) return; + setImei1Files([]) + }} + beforeUpload={(file) => { + if (finishedProcessing) return; + setImei1Files([file]) + return false; + }} + fileList={imei1Files} + > + IMEI 1: + +
+
+ { + if (finishedProcessing) return; + setImei2Files([]) + }} + beforeUpload={(file) => { + if (finishedProcessing) return; + setImei2Files([file]) + return false; + }} + fileList={imei2Files} + > + IMEI 2: + +
+
+ {!finishedProcessing && } + {finishedProcessing && } +
+
+
- Invoice File: - -
-
- { - if (finishedProcessing) return; - setImei1Files([]) - }} - beforeUpload={(file) => { - if (finishedProcessing) return; - setImei1Files([file]) - return false; - }} - fileList={imei1Files} + +
+ + + {currentRequest?.Files?.length &&
+

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

+ {currentRequest?.Files.map((file, index) => ( + { + setAndLoadSelectedFile(currentRequest, index); + } + } setIsReasonModalOpen={setIsReasonModalOpen} /> + ))} +
} + {selectedFileData &&
+ {selectedFileData === "FAILED_TO_LOAD_FILE" ?

: (fileExtension === "pdf" ? () :
file
)} +
} +
+ +
+ + + + + +
+ {currentRequest && (currentRequest["Is Reviewed"] ? } color="success" style={{ padding: "4px 16px" }}> + Reviewed + : } color="warning" style={{ padding: "4px 16px" }}> + Not Reviewed + )} +
+
+
+
+ { + + } + } + onCancel={ + () => { + setIsReasonModalOpen(false); + } + } > - IMEI File 1: -
-
-
- { - if (finishedProcessing) return; - setImei2Files([]) - }} - beforeUpload={(file) => { - if (finishedProcessing) return; - setImei2Files([file]) - return false; - }} - fileList={imei2Files} - > - IMEI File 2: - -
- {!finishedProcessing && } - {finishedProcessing && } -
-

Result:

- +
+ + @@ -333,7 +327,7 @@ const InferencePage = () => { }; const updateRevisedData = async (newRevisedData: any) => { - const requestID = newRevisedData.request_id; + const requestID = currentRequest.RequestID; const token = localStorage.getItem('sbt-token') || ''; await fetch(`${baseURL}/ctel/request/${requestID}/`, { method: 'POST', @@ -376,9 +370,32 @@ const InferencePage = () => { const newData = [...dataSource]; const newRevisedData = {}; for (let i = 0; i < newData.length; i++) { + if (newData[i].revised === "") { + newData[i].revised = null; + } + if (typeof(newData[i].revised) === "string") { + newData[i].revised = newData[i].revised.trim(); + } + if (newData[i].revised === "" || newData[i].revised === null || newData[i].revised === undefined) { + newData[i].revised = null; + } + if ((newData[i].key === "imei_number" || newData[i].key === "purchase_date") && typeof(newData[i].revised) === "string") { + // Convert to list + newData[i].revised = new Array(newData[i].revised.split(",")); + } + if (Array.isArray(newData[i].revised)) { + // Trim all empty strings + for (let j = 0; j < newData[i].revised.length; j++) { + if (typeof(newData[i].revised[j]) === "string") { + newData[i].revised[j] = newData[i].revised[j].trim(); + } + if (newData[i].revised[j] === "") { + newData[i].revised[j] = null; + } + } + } newRevisedData[newData[i].key] = newData[i].revised; } - console.log(currentRequest) updateRevisedData(newRevisedData).then(() => { // "[Is Reviewed]" => true setCurrentRequest({ @@ -400,6 +417,25 @@ const InferencePage = () => { title: 'Predicted', dataIndex: 'predicted', key: 'predicted', + render: (text) => { + if (!text) return {""}; + const displayedContent = text; + if (typeof(displayedContent) === "string") { + return {displayedContent}; + } else if (typeof(displayedContent) === "object") { + if (displayedContent.length === 0) { + return {""}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ""; + } + } + return {displayedContent.join(", ")}; + } + return {displayedContent}; + }, }, { title: (
{ dataIndex: 'revised', key: 'revised', editable: ENABLE_REVIEW, + render: (text) => { + if (!text) return {""}; + const displayedContent = text; + if (typeof(displayedContent) === "string") { + return {displayedContent}; + } else if (typeof(displayedContent) === "object") { + if (displayedContent.length === 0) { + return {""}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ""; + } + } + return {displayedContent.join(", ")}; + } + return {displayedContent}; + }, }, ]; diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index d158104..6f71652 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -430,9 +430,32 @@ const ReviewPage = () => { const newData = [...dataSource]; const newRevisedData = {}; for (let i = 0; i < newData.length; i++) { + if (newData[i].revised === "") { + newData[i].revised = null; + } + if (typeof(newData[i].revised) === "string") { + newData[i].revised = newData[i].revised.trim(); + } + if (newData[i].revised === "" || newData[i].revised === null || newData[i].revised === undefined) { + newData[i].revised = null; + } + if ((newData[i].key === "imei_number" || newData[i].key === "purchase_date") && typeof(newData[i].revised) === "string") { + // Convert to list + newData[i].revised = new Array(newData[i].revised.split(",")); + } + if (Array.isArray(newData[i].revised)) { + // Trim all empty strings + for (let j = 0; j < newData[i].revised.length; j++) { + if (typeof(newData[i].revised[j]) === "string") { + newData[i].revised[j] = newData[i].revised[j].trim(); + } + if (newData[i].revised[j] === "") { + newData[i].revised[j] = null; + } + } + } newRevisedData[newData[i].key] = newData[i].revised; } - console.log(currentRequest) updateRevisedData(newRevisedData).then(() => { // "[Is Reviewed]" => true setCurrentRequest({ @@ -443,6 +466,7 @@ const ReviewPage = () => { }; + const defaultColumns = [ { title: 'Key', @@ -454,11 +478,49 @@ const ReviewPage = () => { title: 'Predicted', dataIndex: 'predicted', key: 'predicted', + render: (text) => { + if (!text) return {""}; + const displayedContent = text; + if (typeof(displayedContent) === "string") { + return {displayedContent}; + } else if (typeof(displayedContent) === "object") { + if (displayedContent.length === 0) { + return {""}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ""; + } + } + return {displayedContent.join(", ")}; + } + return {displayedContent}; + }, }, { title: 'Submitted', dataIndex: 'submitted', key: 'submitted', + render: (text) => { + if (!text) return {""}; + const displayedContent = text; + if (typeof(displayedContent) === "string") { + return {displayedContent}; + } else if (typeof(displayedContent) === "object") { + if (displayedContent.length === 0) { + return {""}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ""; + } + } + return {displayedContent.join(", ")}; + } + return {displayedContent}; + }, }, { title: (
{ dataIndex: 'revised', key: 'revised', editable: true, - render: (text, record) => { - return
{text}
; + render: (text) => { + if (!text) return {""}; + const displayedContent = text; + if (typeof(displayedContent) === "string") { + return {displayedContent}; + } else if (typeof(displayedContent) === "object") { + if (displayedContent.length === 0) { + return {""}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ""; + } + } + return {displayedContent.join(", ")}; + } + return {displayedContent}; }, }, ]; From 79b2de181877376990dbfa136685e6bc5c0f25a5 Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Tue, 12 Mar 2024 17:26:33 +0700 Subject: [PATCH 15/15] UPDATE: add invoice_number infomation --- cope2n-api/fwd_api/utils/file.py | 45 ++++++++++++++++++------------- cope2n-api/report.xlsx | Bin 6989 -> 7016 bytes cope2n-api/report_detail.xlsx | Bin 6662 -> 6742 bytes 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/cope2n-api/fwd_api/utils/file.py b/cope2n-api/fwd_api/utils/file.py index 2df0d41..fea47c0 100755 --- a/cope2n-api/fwd_api/utils/file.py +++ b/cope2n-api/fwd_api/utils/file.py @@ -487,9 +487,10 @@ def dict2xlsx(input: json, _type='report'): 'M': 'average_accuracy_rate.imei', 'N': 'average_accuracy_rate.purchase_date', 'O': 'average_accuracy_rate.retailer_name', - 'P': 'average_processing_time.imei', - 'Q': 'average_processing_time.invoice', - 'R': 'review_progress' + 'P': 'average_accuracy_rate.invoice_number', + 'Q': 'average_processing_time.imei', + 'R': 'average_processing_time.invoice', + 'S': 'review_progress' } start_index = 5 @@ -503,21 +504,29 @@ def dict2xlsx(input: json, _type='report'): 'D': 'image_type', 'E': 'imei_user_submitted', 'F': "imei_ocr_retrieved", - 'G': "imei1_accuracy", - 'H': "invoice_purchase_date_consumer", - 'I': "invoice_purchase_date_ocr", - 'J': "invoice_purchase_date_accuracy", - 'K': "invoice_retailer_consumer", - 'L': "invoice_retailer_ocr", - 'M': "invoice_retailer_accuracy", - 'N': "ocr_image_accuracy", - 'O': "ocr_image_speed_(seconds)", - 'P': "is_reviewed", - 'Q': "bad_image_reasons", - 'R': "countermeasures", - 'S': 'imei_revised_accuracy', - 'T': 'purchase_date_revised_accuracy', - 'U': 'retailer_revised_accuracy', + 'G': "imei_revised", + 'H': "imei1_accuracy", + 'I': "invoice_number_user", + 'J': "invoice_number_ocr", + 'K': "invoice_number_revised", + 'L': "invoice_number_accuracy", + 'M': "invoice_purchase_date_consumer", + 'N': "invoice_purchase_date_ocr", + 'O': "invoice_purchase_date_revised", + 'P': "invoice_purchase_date_accuracy", + 'Q': "invoice_retailer_consumer", + 'R': "invoice_retailer_ocr", + 'S': 'invoice_retailer_revised', + 'T': "invoice_retailer_accuracy", + 'U': "ocr_image_accuracy", + 'V': "ocr_image_speed_(seconds)", + 'W': "is_reviewed", + 'X': "bad_image_reasons", + 'Y': "countermeasures", + 'Z': "imei_revised_accuracy", + 'AA': "invoice_number_revised_accuracy", + 'AB': 'purchase_date_revised_accuracy', + 'AC': 'retailer_revised_accuracy', } start_index = 4 diff --git a/cope2n-api/report.xlsx b/cope2n-api/report.xlsx index 3319daed4d48b3452c27bf4223c890a1c8f99cbf..92da45f2608946c38042e19ca90cb26f641a4de9 100644 GIT binary patch delta 3034 zcmZuz2T&8(77a0AR5}3?V8KugH9?Rd#V8^WTt#~59Rf%R5(R{S2u7NO(4|OkDor{{ z3`IaXiX|XL1*A6x5#fXTXV<^;-^`nP=gyfq@7?p}zSDbZ_7t0e{vi+q00x5r$_Ad5 zY~oCZ_G7gYlf++%6%YmdRfz}L|CMa8Xn+nXRHb8br1xk@$hhs~s#Z1;Rg0f44~dsRqNdFWo_1tQ-Lqz|~H_zZkaufKsAvkukDibpPp$@ko= z6OTGs>$)tec`qm2N4dJlZt6I$>wC|8m@UTYf@9+}ZwgBZe#WLYX5?;mf6unE=b>MwZC;3piVP{nPWqd*C^E{@6AgeQo}1)jEj2gvIkfD@`*eIS z6wxd%G^37GVpJIg6A8lGuSD*elNIIUcfz#?6hkS!rsn7lk1Qj?Mm=4VsQNAHhl?6% z>6k=W$^>OwHSEqCw;4bDEdw1~>e*QGb-2k(P z1yp*PwBc5z2?=mKpBwiQ>VuOlMrdaE>hSRz z+4kDDbszImGX&c}HPYxN)gbuAj523dVfQDtIs}!eSP+XX`^JtHOdA@C!y9L8 zg1tuyT&j26d@`;Zq<<~JjTDo<>v@ZBTC64CMbG&|HQX+CNtRM&R^FcezIe1pH=jfp zlVk=ryH=MItc7QNJFe}{WSQr;*>9`L^SUD$D%MKWZ0J(6cYS}S` zNq|h5X`Fg5Pr3&COv|RKf;ZsEP#PO3lCwU71_oZ|tiMAu1INITq%ax;coN(LuLz}a z17+sVD7x;6`_(NkmT7R!_7CO{e%RDFE__alcV4rit0&D$ao60s?Jtee z70+;d3`eTZR9sf|noQn5PM_SPOv0!ucYrd+jx|gV`07}GAL5G=we#g6pdVPaPj9{` zU%%ejf5}R0oM1@8)MWF<%PpskTb@hHc;LO(N6It552^d_O1$kC&^i7|k>#dM23p!B1!HN!?3pS(!y8|plfl+>@qCo6b8_^*&FRmHJ_*gA4^k=Vj2@^(?M2_*7< z(LQbG(U@>j*Z8|dUzlbkV_os4eO-$4R`;bMqYf1N(@Sl-ovxI0ad*p@Pv8x7=6fM2 zBTn38b8KBA#Y}+kY16k8CDUxYg!w zrst=k3SQ*hS+S39O?o;63Wa?+8!q;N9%LVTQ)Dq2)NmfTZ)qCA%IX6nbcr71&z&NTtG zPddpX)uG>!#LVA^Sl; zy5op}W`lY3Yb=H^t&@8nQP~Xnv%Rq7Qi_%DJ?ZgGTq+%tiyw}Yf!)W(Y40I+0;`z< zW0*{O*lu;`YS%xy_x3qCWRuviRRPy_IUan}#UncC7WLKon@W7u&l6-@Jv7rZiG_>m zd~nTa*gdGRw6h8+jZ9%v;DNazS!8Ksd6K)`&7=K?hEs9e+Aj>ZB3c^EuOid zM*h{fWL-BzYIA5`e>7$j@yWzanj|k6<<_X~d#9FlRSAgj=~_Cm6W+N~sYGHrkV|mo zX<9TB0FVs&w^~phagz1ra3myn(=7g(7NQ3at~lSDW#v#MCot|F^rF-ty;hLXQb)5h zO4%azYZT3dC};jrI>ZP+m75F`Jxf-Pe>i=P4`xw5y{oYnQ;;xRl^mZV2330LZCJ={ z>TzSR26&Yj+7HvCB9n3+)kr^oTOnus3UmqnM?;Cf_M`Ad8Y!wu!2%FR_lc6JNHo&- z^_VRiB78LN$j7BHKC}Z_Y04t z*HasJkAzsND>PjRhBHi1n1DccaeIMc<|Wned<7L``Rd1E_qeqaF_Iy=J7_Bk`p9yE z&vw?7?XqPnN%OJ)hbq?z2lhyO&ci1A%e%U0D?{AEDBUl9H}IWahjCDMfU){NHl_<<4%@<9N%eI-VLe=EWAm-nL5Ag_PUL*ap2exLvz3pWNpc}Z*#1%A$qzHHKh8KEaPE}JT!{{t8I(T<6fC-bD~KO8Jp-& z#M7|x1uZ1tU_||B(cZZNo}wR9g=e;V9G@KisC=1rqM_iazSU+oz++DG*qa2+pz@>% zL=b&sPL1{>;79;nF6(1ptts&|t8FyVZKi#U7N5PvMA5bEwUI1v#u*Vf)F>+a)wrMJ zGNGBU7GVh$+jj~FZ3#**p4mR^=CdC5) zIC(o75WIc-6db(?&hj^|dK&060U?0DowNDm2p}|NAH2H1_wm22J_xlR!b`0eV6{Hr t`TeS#zrg^2Lf+qa?0tOx9}S+PN0PALByE;|(|n-91tnQ1`|}t8_zwjGPPqU8 delta 2905 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 Qy^*Xfeso`e$ZwPX0ECe-wEzGB diff --git a/cope2n-api/report_detail.xlsx b/cope2n-api/report_detail.xlsx index 82e12dd17c594fedab37b94c75f88f9685118495..480d827cd5fdc37cf12c12785fbedf5e76a44b71 100644 GIT binary patch delta 2696 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