From 5fed071439af0252763a7dbca1b0aed11b9dbacc Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Tue, 20 Feb 2024 15:01:06 +0700 Subject: [PATCH 01/17] Reflow UI --- cope2n-fe/src/components/left-menu/index.tsx | 2 +- cope2n-fe/src/pages/reviews/index.tsx | 390 +++++++++++++++---- 2 files changed, 322 insertions(+), 70 deletions(-) diff --git a/cope2n-fe/src/components/left-menu/index.tsx b/cope2n-fe/src/components/left-menu/index.tsx index d0390dc..4c9206e 100644 --- a/cope2n-fe/src/components/left-menu/index.tsx +++ b/cope2n-fe/src/components/left-menu/index.tsx @@ -35,7 +35,7 @@ function LeftMenu() { const generalSubItems = [ getItem(t`Dashboard`, '/dashboard', ), getItem(t`Reports`, '/reports', ), - // getItem(t`Review`, '/reviews', ), + getItem(t`Review`, '/reviews', ), getItem(t`Inference`, '/inference', ), // getItem(t`Users`, '/users', ), ]; diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 616fb15..ab34816 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -1,9 +1,17 @@ import { t } from '@lingui/macro'; -import { Button, message, Upload, Input, Table } from 'antd'; -import { SbtPageHeader } from 'components/page-header'; +import { Button, Input, Table, Tag, DatePicker, Form, Modal, Select, Space, Checkbox } from 'antd'; import { useState } from 'react'; import { Layout } from 'antd'; +import { + EditOutlined, DownloadOutlined, CheckCircleOutlined, + ClockCircleOutlined, + ArrowLeftOutlined, + ArrowRightOutlined, + FullscreenOutlined, + FullscreenExitOutlined, +} from '@ant-design/icons'; import FileViewer from '@cyntler/react-doc-viewer'; +import styled from 'styled-components'; const { Sider, Content } = Layout; const siderStyle: React.CSSProperties = { @@ -13,6 +21,24 @@ const siderStyle: React.CSSProperties = { }; +const StyledTable = styled(Table)` + & .sbt-table-cell { + padding: 4px!important; + } +`; + + +const StyledEditOutlined = styled(EditOutlined)` + & { + color: #6666ff; + margin-left: 8px; + } + &:hover { + color: #0000ff; + } +`; + + const fileList = [ { name: "invoice.pdf", @@ -30,7 +56,7 @@ const fileList = [ const dataSource = [ { - key: '1', + key: 'retailer_name', value: 'Mike', }, { @@ -41,6 +67,34 @@ const dataSource = [ key: '3', value: 'Mike', }, + { + key: '3', + value: 'Mike', + }, + { + key: '3', + value: 'Mike', + }, + { + key: '3', + value: 'Mike', + }, + { + key: '3', + value: 'Mike', + }, + { + key: '3', + value: 'Mike', + }, + { + key: '3', + value: 'Mike', + }, + { + key: '3', + value: 'Mike', + }, ]; const columns = [ @@ -49,6 +103,14 @@ const columns = [ dataIndex: 'key', key: 'key', }, + { + title: 'Accuracy', + dataIndex: 'acc', + key: 'acc', + render: (text, record) => { + return
100%
; + }, + }, { title: 'Predicted', dataIndex: 'value', @@ -63,6 +125,18 @@ const columns = [ title: 'Revised', dataIndex: 'value', key: 'value', + render: (text, record) => { + return ( +
+ {text} + +
+ ) + }, + }, ]; @@ -75,7 +149,9 @@ const FileCard = ({ file, isSelected, onClick }) => { backgroundColor: isSelected ? '#d4ecff' : '#fff', padding: '4px 8px', marginRight: '4px', - marginTop: '4px', + marginTop: '2px', + position: 'relative', + height: '100px', }} onClick={onClick}>
{ color: '#333', fontWeight: 'bold', padding: '4px 8px', + cursor: 'default', }}>{file.type.toUpperCase()} {file.name}
+
+ + +
); }; -const InferencePage = () => { +const ReviewPage = () => { + const [fullscreen, setFullscreen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const [selectedFileId, setSelectedFileId] = useState(0); const selectFileByIndex = (index) => { setSelectedFileId(index); }; + const [filterDateRange, setFilterDateRange] = useState(null); + const [filterSubsidiaries, setFilterSubsidiaries] = useState(''); + const [filterReviewState, setFilterReviewState] = useState(''); return ( - <> - {/* */} +
+ - -
+
- -
-
- {fileList.map((file, index) => ( - { - setSelectedFileId(index); - } - } /> - ))} + color: "#333", + padding: 10, + fontWeight: 'bold' + }} + >Files ({fileList.length}) + {fileList.map((file, index) => ( + { + setSelectedFileId(index); + } + } /> + ))} +
+
+ +
+ + + + + + +
+
+ + +
- - -

Overview

- - - - - -
- -
- - - - + +

Request Review

+ + + + + +
+ + {/* } color="warning" style={{ padding: "4px 16px", marginLeft: 8 }}> + Not Reviewed + */} + } color="success" style={{ padding: "4px 16px", marginLeft: 8 }}> + Reviewed + +
+ + + { + setIsModalOpen(false); + } + } + onCancel={ + () => { + setIsModalOpen(false); + } + } + > +
+ + { + console.log(value); + setFilterDateRange(value); + }} + style={{ width: 200 }} + /> + +
+ + + +
+ +
+
+ +
+ ); }; -export default InferencePage; +export default ReviewPage; From 15a550ae360e9430ebac5646fdd3e97b525961bb Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Wed, 21 Feb 2024 18:11:02 +0700 Subject: [PATCH 02/17] Integrate APIs --- cope2n-fe/src/pages/reviews/index.tsx | 301 +++++++++++++++++++------- cope2n-fe/src/request/api.ts | 2 +- 2 files changed, 229 insertions(+), 74 deletions(-) diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index ab34816..491d5a3 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -1,6 +1,6 @@ import { t } from '@lingui/macro'; import { Button, Input, Table, Tag, DatePicker, Form, Modal, Select, Space, Checkbox } from 'antd'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Layout } from 'antd'; import { EditOutlined, DownloadOutlined, CheckCircleOutlined, @@ -13,6 +13,8 @@ import { import FileViewer from '@cyntler/react-doc-viewer'; import styled from 'styled-components'; const { Sider, Content } = Layout; +import { baseURL } from "request/api"; +import moment from 'moment'; const siderStyle: React.CSSProperties = { backgroundColor: '#fafafa', @@ -38,22 +40,6 @@ const StyledEditOutlined = styled(EditOutlined)` } `; - -const fileList = [ - { - name: "invoice.pdf", - url: "/dummpy.pdf", - type: "invoice", - isBadQuality: false, - }, - { - name: "invoice.pdf", - url: "/dummpy.pdf", - type: "imei", - isBadQuality: true, - } -] - const dataSource = [ { key: 'retailer_name', @@ -102,6 +88,7 @@ const columns = [ title: 'Key', dataIndex: 'key', key: 'key', + width: 200, }, { title: 'Accuracy', @@ -110,6 +97,7 @@ const columns = [ render: (text, record) => { return
100%
; }, + width: 150, }, { title: 'Predicted', @@ -142,6 +130,8 @@ const columns = [ const FileCard = ({ file, isSelected, onClick }) => { + const fileName = file["File Name"]; + return (
{ fontWeight: 'bold', padding: '4px 8px', cursor: 'default', - }}>{file.type.toUpperCase()} + }}>{file["Doc Type"].toUpperCase()} {file.name} + maxWidth: '50px', + overflow: 'hidden', + textOverflow: 'ellipsis', + }}> + {fileName? fileName.substring(0, 10) : fileName } +
{ }; +const fetchAllRequests = async (filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, page=1, page_size=20) => { + const startDate = (filterDateRange && filterDateRange[0]) ? filterDateRange[0].format('YYYY-MM-DD'): ''; + const endDate = (filterDateRange && filterDateRange[1]) ? filterDateRange[1].format('YYYY-MM-DD'): ''; + let filterStr = ""; + filterStr += `page=${page}&page_size=${page_size}&`; + if (filterSubsidiaries) { + filterStr += `subsidiary=${filterSubsidiaries}&`; + } + if (filterReviewState) { + filterStr += `is_reviewed=${filterReviewState}&`; + } + if (filterIncludeTests) { + filterStr += `includes_tests=${filterIncludeTests}&`; + } + if (startDate && endDate) { + filterStr += `start_date=${startDate}&end_date=${endDate}&`; + } + const token = localStorage.getItem('sbt-token') || ''; + const data = await fetch(`${baseURL}/ctel/request_list/?${filterStr}`, { + method: 'GET', + headers: { + "Authorization": `${JSON.parse(token)}` + } + }) + .then(async (res) => { + const data = await res.json(); + return data; + }); + return data; +}; + +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 ReviewPage = () => { const [fullscreen, setFullscreen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); @@ -198,9 +235,86 @@ const ReviewPage = () => { const selectFileByIndex = (index) => { setSelectedFileId(index); }; - const [filterDateRange, setFilterDateRange] = useState(null); - const [filterSubsidiaries, setFilterSubsidiaries] = useState(''); - const [filterReviewState, setFilterReviewState] = useState(''); + + // Default date range: 1 month ago to today + const [filterDateRange, setFilterDateRange] = useState([ + moment().subtract(1,'month'), + moment(), + ]); + + const [filterSubsidiaries, setFilterSubsidiaries] = useState('ALL'); + const [filterReviewState, setFilterReviewState] = useState('all'); + const [filterIncludeTests, setFilterIncludesTests] = useState('true'); + const [requests, setRequests] = useState([]); + const [currentRequest, setCurrentRequest] = useState(null); + const [currentRequestIndex, setCurrentRequestIndex] = useState(1); + const [hasNextRequest, setHasNextRequest] = useState(true); + const [totalPages, setTotalPages] = useState(0); + + const gotoNextRequest = () => { + const nextRequestIndex = currentRequestIndex + 1; + setCurrentRequestIndex(nextRequestIndex); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, nextRequestIndex, 2).then((data) => { + setRequests(data?.subscription_requests); + setHasNextRequest(data?.subscription_requests.length > 1); + setTotalPages(data?.page?.total_pages); + const requestData = fetchRequest(data?.subscription_requests[0].RequestID); + requestData.then(async (data) => { + console.log(data) + if (data) setCurrentRequest(data); + }); + }); + }; + + const gotoPreviousRequest = () => { + if (currentRequestIndex === 1) { + return; + } + const previousRequestIndex = currentRequestIndex - 1; + setCurrentRequestIndex(previousRequestIndex); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, previousRequestIndex, 2).then((data) => { + setRequests(data?.subscription_requests); + setHasNextRequest(data?.subscription_requests.length > 1); + setTotalPages(data?.page?.total_pages); + const requestData = fetchRequest(data?.subscription_requests[0].RequestID); + requestData.then(async (data) => { + console.log(data) + if (data) setCurrentRequest(data); + }); + }); + }; + + + const reloadFilters = () => { + setCurrentRequestIndex(1); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, currentRequestIndex, 2).then((data) => { + setTotalPages(data?.page?.total_pages); + setRequests(data?.subscription_requests); + setHasNextRequest(data?.subscription_requests.length > 1); + const firstRequest = fetchRequest(data?.subscription_requests[0].RequestID); + firstRequest.then(async (data) => { + console.log(firstRequest) + if (data) setCurrentRequest(data); + }); + }); + + }; + + useEffect(() => { + setCurrentRequestIndex(1); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, currentRequestIndex, 2).then((data) => { + setTotalPages(data?.page?.total_pages); + setRequests(data?.subscription_requests); + setHasNextRequest(data?.subscription_requests.length > 1); + const firstRequest = fetchRequest(data?.subscription_requests[0].RequestID); + firstRequest.then(async (data) => { + console.log(firstRequest) + if (data) setCurrentRequest(data); + }); + }); + }, []); + + const fileURL = currentRequest ? baseURL + currentRequest["Files"][selectedFileId]["File URL"].replace("http://be-ctel-sbt:9000/api", "") : "dummy.pdf"; return (
{ width: '100%', height: '100%', maxWidth: '100%', - minHeight: '60%', - maxHeight: '60%', + minHeight: '70%', + maxHeight: '70%', display: 'flex', padding: '8px', }}> @@ -249,10 +363,10 @@ const ReviewPage = () => { style={{ color: "#333", padding: 10, - fontWeight: 'bold' + fontWeight: 'bold' }} - >Files ({fileList.length}) - {fileList.map((file, index) => ( + >Files ({currentRequest?.Files?.length}) + {currentRequest?.Files.map((file, index) => ( { setSelectedFileId(index); @@ -267,7 +381,7 @@ const ReviewPage = () => { }}> { }} />
- + - + - @@ -306,12 +435,12 @@ const ReviewPage = () => {
-

Request Review

- - - - - +

{totalPages ? ("Request: " + currentRequestIndex + "/" + totalPages) : "No Request. Adjust your search criteria to see more results."}

+ + "} /> + + +
{/* } color="warning" style={{ padding: "4px 16px", marginLeft: 8 }}> @@ -330,6 +459,7 @@ const ReviewPage = () => { onOk={ () => { setIsModalOpen(false); + reloadFilters(); } } onCancel={ @@ -359,40 +489,42 @@ const ReviewPage = () => { { - console.log(value); setFilterDateRange(value); }} style={{ width: 200 }} /> -
- + - + value={filterSubsidiaries} + defaultValue={filterSubsidiaries} + onChange={setFilterSubsidiaries} + /> + +
{ message: 'Please select review status', }, ]} + > +
-
-
diff --git a/cope2n-fe/src/request/api.ts b/cope2n-fe/src/request/api.ts index a0945df..6a54c3e 100644 --- a/cope2n-fe/src/request/api.ts +++ b/cope2n-fe/src/request/api.ts @@ -11,7 +11,7 @@ const environment = process.env.NODE_ENV; const AXIOS_TIMEOUT_MS = 30 * 60 * 1000; // This config sastified long-live upload file request const EXPIRED_PASSWORD_SIGNAL = 'expired_password'; -export const baseURL = environment === 'development' ? 'http://42.96.42.13:9000/api' : '/api'; +export const baseURL = environment === 'development' ? 'http://42.96.42.13:9881/api' : '/api'; export const API = axios.create({ timeout: AXIOS_TIMEOUT_MS, From 532905ca783856818506e6b9b056f1a85a9fac8c Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Wed, 21 Feb 2024 18:20:35 +0700 Subject: [PATCH 03/17] UPDATE: Generate S3 url --- cope2n-api/fwd_api/api/accuracy_view.py | 6 +++--- cope2n-api/fwd_api/utils/file.py | 2 ++ cope2n-api/fwd_api/utils/s3.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index db6f0cd..c75b771 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -15,7 +15,7 @@ import json from ..exception.exceptions import InvalidException, RequiredFieldException, NotFoundException from ..models import SubscriptionRequest, Report, ReportFile, SubscriptionRequestFile from ..utils.accuracy import shadow_report, MonthReportAccumulate, first_of_list, extract_report_detail_list, IterAvg -from ..utils.file import download_from_S3, convert_date_string, build_media_url_v2, build_url +from ..utils.file import download_from_S3, convert_date_string, build_media_url_v2, build_url, build_S3_url from ..utils.redis import RedisUtils from ..utils.process import string_to_boolean from ..request.ReportCreationSerializer import ReportCreationSerializer @@ -588,8 +588,8 @@ class RequestViewSet(viewsets.ViewSet): 'File Name': subscription_request_file.file_name, 'File Path': subscription_request_file.file_path, 'File Category': subscription_request_file.file_category, - 'File URL': build_media_url_v2(subscription_request_file.file_name.split('.')[0], user_id, sub_id, sync_id), - 'Origin_Name': subscription_request_file.origin_name, + 'File URL': build_S3_url("sbt_invoice/" + subscription_request.request_id + "/" + subscription_request_file.file_name, 600), + 'Original Name': subscription_request_file.origin_name, 'Is Bad Image Quality': subscription_request_file.is_bad_image_quality, 'Doc Type': subscription_request_file.doc_type, 'Processing Time (ms)': subscription_request_file.processing_time, diff --git a/cope2n-api/fwd_api/utils/file.py b/cope2n-api/fwd_api/utils/file.py index ff613fa..7a0fa11 100644 --- a/cope2n-api/fwd_api/utils/file.py +++ b/cope2n-api/fwd_api/utils/file.py @@ -429,6 +429,8 @@ def build_media_url_v2(media_id: str, user_id: int, sub_id: int, u_sync_id: str) token = image_authenticator.generate_img_token_v2(user_id, sub_id, u_sync_id) return f'{settings.BASE_URL}/api/ctel/v2/media/request/{media_id}/?token={token}' +def build_S3_url(s3_key, exp_time): + return s3_client.create_url_with_expiration(s3_key, exp_time) def get_value(_dict, keys): keys = keys.split('.') diff --git a/cope2n-api/fwd_api/utils/s3.py b/cope2n-api/fwd_api/utils/s3.py index 60d8380..c1ee84f 100644 --- a/cope2n-api/fwd_api/utils/s3.py +++ b/cope2n-api/fwd_api/utils/s3.py @@ -51,6 +51,21 @@ class MinioS3Client: return res except Exception as e: print(f"Error downloading file from S3: {str(e)}") + + def create_url_with_expiration(self, s3_key, expiration_time): + try: + res = self.s3_client.generate_presigned_url( + ClientMethod="get_object", ExpiresIn=expiration_time, + Params={ + "Bucket": self.bucket_name, + "Key": s3_key, + }, + ) + # print(f"URL for file '{s3_key}' expires in {expiration_time} seconds") + return res + except Exception as e: + print(f"Error generating URL for file '{s3_key}': {str(e)}") + if __name__=="__main__": FILE = "/app/media/users/1/subscriptions/33/requests/sbt_invoice/SAP00c6c229c2954e498b119968a318d366/temp_SAP00c6c229c2954e498b119968a318d366.jpg" From efd2da3a8282c69dded89227624a3011ffb785d6 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Wed, 21 Feb 2024 19:01:19 +0700 Subject: [PATCH 04/17] Show data fields --- cope2n-fe/src/pages/reviews/index.tsx | 131 +++++++++++++------------- 1 file changed, 63 insertions(+), 68 deletions(-) diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 491d5a3..74d1315 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -40,49 +40,6 @@ const StyledEditOutlined = styled(EditOutlined)` } `; -const dataSource = [ - { - key: 'retailer_name', - value: 'Mike', - }, - { - key: '2', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, -]; - const columns = [ { title: 'Key', @@ -101,18 +58,18 @@ const columns = [ }, { title: 'Predicted', - dataIndex: 'value', - key: 'value', + dataIndex: 'predicted', + key: 'predicted', }, { title: 'Submitted', - dataIndex: 'value', - key: 'value', + dataIndex: 'submitted', + key: 'submitted', }, { title: 'Revised', - dataIndex: 'value', - key: 'value', + dataIndex: 'revised', + key: 'revised', render: (text, record) => { return (
{text} -
) }, - + }, + { + title: 'Action', + key: 'operation', + fixed: 'right', + width: 100, + render: () => , }, ]; @@ -161,7 +123,7 @@ const FileCard = ({ file, isSelected, onClick }) => { overflow: 'hidden', textOverflow: 'ellipsis', }}> - {fileName? fileName.substring(0, 10) : fileName } + {fileName ? fileName.substring(0, 25).replace("temp_", "") : fileName}
{ @@ -186,9 +151,9 @@ const FileCard = ({ file, isSelected, onClick }) => { }; -const fetchAllRequests = async (filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, page=1, page_size=20) => { - const startDate = (filterDateRange && filterDateRange[0]) ? filterDateRange[0].format('YYYY-MM-DD'): ''; - const endDate = (filterDateRange && filterDateRange[1]) ? filterDateRange[1].format('YYYY-MM-DD'): ''; +const fetchAllRequests = async (filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, page = 1, page_size = 20) => { + const startDate = (filterDateRange && filterDateRange[0]) ? filterDateRange[0].format('YYYY-MM-DD') : ''; + const endDate = (filterDateRange && filterDateRange[1]) ? filterDateRange[1].format('YYYY-MM-DD') : ''; let filterStr = ""; filterStr += `page=${page}&page_size=${page_size}&`; if (filterSubsidiaries) { @@ -238,7 +203,7 @@ const ReviewPage = () => { // Default date range: 1 month ago to today const [filterDateRange, setFilterDateRange] = useState([ - moment().subtract(1,'month'), + moment().subtract(1, 'month'), moment(), ]); @@ -251,6 +216,31 @@ const ReviewPage = () => { const [hasNextRequest, setHasNextRequest] = useState(true); const [totalPages, setTotalPages] = useState(0); + // purchase_date: "2024-01-20", + // retailername: "Test Retailer", + // sold_to_party: "Test Party", + const dataSource = [ + // { + // key: "imei_number", + // predicted: "352271450941944", + // submitted: "352271450941944", + // revised: "352271450941944", + // }, + ]; + + const predicted = (currentRequest && currentRequest["Reviewed Result"]) ? currentRequest["Reviewed Result"] : {}; + const submitted = (currentRequest && currentRequest["Feedback Result"]) ? currentRequest["Feedback Result"] : {}; + const revised = (currentRequest && currentRequest["Reviewed Result"]) ? currentRequest["Reviewed Result"] : {}; + const keys = Object.keys(predicted); + 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]]; + dataSource.push(instance); + } + const gotoNextRequest = () => { const nextRequestIndex = currentRequestIndex + 1; setCurrentRequestIndex(nextRequestIndex); @@ -328,12 +318,15 @@ const ReviewPage = () => { } : { height: '100%', }}> - +
+ +    Request ID: {currentRequest?.RequestID} +
{
- - - + + Go to + + } />

{totalPages ? ("Request: " + currentRequestIndex + "/" + totalPages) : "No Request. Adjust your search criteria to see more results."}

- "} /> + From 2f7505c7979c4676c3b6a0c12b751e2a7bebdc38 Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 08:36:57 +0700 Subject: [PATCH 05/17] UPDATE: update request detail api --- cope2n-api/fwd_api/api/accuracy_view.py | 44 +++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index c75b771..8ce1aff 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -20,6 +20,8 @@ from ..utils.redis import RedisUtils from ..utils.process import string_to_boolean from ..request.ReportCreationSerializer import ReportCreationSerializer from ..utils.subsidiary import map_subsidiary_long_to_short, map_subsidiary_short_to_long +from fwd_api.utils.accuracy import predict_result_to_ready +import copy redis_client = RedisUtils() @@ -573,6 +575,14 @@ class RequestViewSet(viewsets.ViewSet): raise NotFoundException(excArgs=request_id) subscription_request = subscription_request.first() + + sample_result = { + "request_id": subscription_request.request_id, + "retailername": None, + "sold_to_party": None, + "purchase_date": None, + "imei_number": [] + } data = [] files = [] @@ -584,6 +594,17 @@ class RequestViewSet(viewsets.ViewSet): user_id = sub.user.id sync_id = sub.user.sync_id sub_id = sub.id + reviewed_result = subscription_request_file.reviewed_result + feedback_result = subscription_request_file.feedback_result + predicted_result = subscription_request_file.predict_result + + if not reviewed_result: + reviewed_result = copy.deepcopy(sample_result) + if not feedback_result: + feedback_result = copy.deepcopy(sample_result) + if not predicted_result: + predicted_result = copy.deepcopy(sample_result) + files.append({ 'File Name': subscription_request_file.file_name, 'File Path': subscription_request_file.file_path, @@ -595,15 +616,26 @@ class RequestViewSet(viewsets.ViewSet): 'Processing Time (ms)': subscription_request_file.processing_time, 'Reason': subscription_request_file.reason, 'Counter Measures': subscription_request_file.counter_measures, - 'Predicted Result': subscription_request_file.predict_result, - 'Feedback Result': subscription_request_file.feedback_result, - 'Reviewed Result': subscription_request_file.reviewed_result, + 'Predicted Result': predicted_result, + 'Feedback Result': feedback_result, + 'Reviewed Result': reviewed_result, 'Feedback Accuracy': subscription_request_file.feedback_accuracy, 'Reviewed Accuracy': subscription_request_file.reviewed_accuracy, 'Created At': subscription_request_file.created_at.isoformat(), 'Updated At': subscription_request_file.updated_at.isoformat() }) + reviewed_result = subscription_request.reviewed_result + feedback_result = subscription_request.feedback_result + predicted_result = predict_result_to_ready(subscription_request.predict_result) + + if not reviewed_result: + reviewed_result = copy.deepcopy(sample_result) + if not feedback_result: + feedback_result = copy.deepcopy(sample_result) + if not predicted_result: + predicted_result = copy.deepcopy(sample_result) + data.append({ 'Document Type': subscription_request.doc_type, 'RequestID': subscription_request.request_id, @@ -612,9 +644,9 @@ class RequestViewSet(viewsets.ViewSet): 'Provider Code': subscription_request.provider_code, 'Status': subscription_request.status, 'Files': files, - 'Reviewed Result': subscription_request.reviewed_result, - 'Feedback Result': subscription_request.feedback_result, - 'Predicted Result': subscription_request.predict_result, + 'Reviewed Result': reviewed_result, + 'Feedback Result': feedback_result, + 'Predicted Result': predicted_result, 'Is Test Request': subscription_request.is_test_request, 'Client Request Time (ms)': subscription_request.client_request_time, 'Server Processing Time (ms)': subscription_request.preprocessing_time + subscription_request.ai_inference_time, From aefebbb984bb8abecfc0a823dee6e6575b144caa Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 08:43:09 +0700 Subject: [PATCH 06/17] REFACTOR: refactor update review request detail --- cope2n-api/fwd_api/api/accuracy_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index 8ce1aff..8f7c798 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -678,6 +678,9 @@ class RequestViewSet(viewsets.ViewSet): subscription_request_files = SubscriptionRequestFile.objects.filter(request=subscription_request.id) reviewed_result = json.loads(data["reviewed_result"]) + for field in ['retailername', 'sold_to_party', 'purchase_date', 'imei_number']: + if not field in reviewed_result.keys(): + raise RequiredFieldException(excArgs=f'reviewed_result.{field}') for subscription_request_file in subscription_request_files: if subscription_request_file.doc_type == 'invoice': @@ -687,9 +690,6 @@ class RequestViewSet(viewsets.ViewSet): subscription_request_file.reviewed_result = {"retailername": None, "sold_to_party": None, "purchase_date": [], "imei_number": [reviewed_result["imei_number"][subscription_request_file.index_in_request]]} subscription_request_file.save() - for field in ['retailername', 'sold_to_party', 'purchase_date', 'imei_number']: - if not field in reviewed_result.keys(): - raise RequiredFieldException(excArgs=f'reviewed_result.{field}') subscription_request.reviewed_result = reviewed_result subscription_request.reviewed_result['request_id'] = request_id subscription_request.is_reviewed = True From d89cc70ec566c171a95a936a6787b12b749df10f Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 09:03:15 +0700 Subject: [PATCH 07/17] Implement editable row --- cope2n-fe/src/pages/reviews/index.tsx | 265 +++++++++++++++++++++----- 1 file changed, 214 insertions(+), 51 deletions(-) diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 74d1315..312aa5c 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -1,10 +1,10 @@ import { t } from '@lingui/macro'; -import { Button, Input, Table, Tag, DatePicker, Form, Modal, Select, Space, Checkbox } from 'antd'; -import { useState, useEffect } from 'react'; +import { Button, Input, Table, Tag, DatePicker, Form, Modal, Select, Space, Spin, message } from 'antd'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import type { GetRef } from 'antd'; import { Layout } from 'antd'; import { EditOutlined, DownloadOutlined, CheckCircleOutlined, - ClockCircleOutlined, ArrowLeftOutlined, ArrowRightOutlined, FullscreenOutlined, @@ -16,6 +16,7 @@ const { Sider, Content } = Layout; import { baseURL } from "request/api"; import moment from 'moment'; + const siderStyle: React.CSSProperties = { backgroundColor: '#fafafa', padding: 10, @@ -29,6 +30,110 @@ const StyledTable = styled(Table)` } `; +type InputRef = GetRef; +type FormInstance = GetRef>; + +const EditableContext = React.createContext | null>(null); + +interface Item { + key: string; + accuracy: number; + submitted: string; + revised: string; + action: string; +} + +interface EditableRowProps { + index: number; +} + +const EditableRow: React.FC = ({ index, ...props }) => { + const [form] = Form.useForm(); + return ( +
+ +
+ + + ); +}; + +interface EditableCellProps { + title: React.ReactNode; + editable: boolean; + children: React.ReactNode; + dataIndex: keyof Item; + record: Item; + handleSave: (record: Item) => void; +} + + +const EditableCell: React.FC = ({ + title, + editable, + children, + dataIndex, + record, + handleSave, + ...restProps +}) => { + const [editing, setEditing] = useState(false); + const inputRef = useRef(null); + const form = useContext(EditableContext)!; + + useEffect(() => { + if (editing) { + inputRef.current!.focus(); + } + }, [editing]); + + const toggleEdit = () => { + setEditing(!editing); + form.setFieldsValue({ [dataIndex]: record[dataIndex] }); + }; + + const save = async () => { + try { + const values = await form.validateFields(); + + toggleEdit(); + handleSave({ ...record, ...values }); + } catch (errInfo) { + console.log('Save failed:', errInfo); + } + }; + + let childNode = children; + + if (editable) { + childNode = editing ? ( + + + + ) : ( +
+ {children} +
+ ); + } + + return
; +}; + +type EditableTableProps = Parameters[0]; + + +type ColumnTypes = Exclude; + const StyledEditOutlined = styled(EditOutlined)` & { @@ -40,7 +145,7 @@ const StyledEditOutlined = styled(EditOutlined)` } `; -const columns = [ +const defaultColumns = [ { title: 'Key', dataIndex: 'key', @@ -70,23 +175,7 @@ const columns = [ title: 'Revised', dataIndex: 'revised', key: 'revised', - render: (text, record) => { - return ( -
- {text} -
- ) - }, - }, - { - title: 'Action', - key: 'operation', - fixed: 'right', - width: 100, - render: () => , + editable: true, }, ]; @@ -194,6 +283,7 @@ const fetchRequest = async (id) => { }; const ReviewPage = () => { + const [loading, setLoading] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedFileId, setSelectedFileId] = useState(0); @@ -219,18 +309,13 @@ const ReviewPage = () => { // purchase_date: "2024-01-20", // retailername: "Test Retailer", // sold_to_party: "Test Party", - const dataSource = [ - // { - // key: "imei_number", - // predicted: "352271450941944", - // submitted: "352271450941944", - // revised: "352271450941944", - // }, - ]; + const dataSource = []; const predicted = (currentRequest && currentRequest["Reviewed Result"]) ? currentRequest["Reviewed Result"] : {}; const submitted = (currentRequest && currentRequest["Feedback Result"]) ? currentRequest["Feedback Result"] : {}; const revised = (currentRequest && currentRequest["Reviewed Result"]) ? currentRequest["Reviewed Result"] : {}; + + const keys = Object.keys(predicted); for (let i = 0; i < keys.length; i++) { let instance = {}; @@ -241,37 +326,38 @@ const ReviewPage = () => { dataSource.push(instance); } - const gotoNextRequest = () => { - const nextRequestIndex = currentRequestIndex + 1; - setCurrentRequestIndex(nextRequestIndex); - fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, nextRequestIndex, 2).then((data) => { + const [pageIndexToGoto, setPageIndexToGoto] = useState(1); + + const loadCurrentRequest = (requestID) => { + setLoading(true); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, requestID, 2).then((data) => { setRequests(data?.subscription_requests); setHasNextRequest(data?.subscription_requests.length > 1); setTotalPages(data?.page?.total_pages); const requestData = fetchRequest(data?.subscription_requests[0].RequestID); requestData.then(async (data) => { - console.log(data) if (data) setCurrentRequest(data); + }).finally(() => { + setLoading(false); }); + }).finally(() => { + setLoading(false); }); }; + const gotoNextRequest = () => { + const nextRequestIndex = currentRequestIndex + 1; + setCurrentRequestIndex(nextRequestIndex); + loadCurrentRequest(nextRequestIndex); + }; + const gotoPreviousRequest = () => { if (currentRequestIndex === 1) { return; } const previousRequestIndex = currentRequestIndex - 1; setCurrentRequestIndex(previousRequestIndex); - fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, previousRequestIndex, 2).then((data) => { - setRequests(data?.subscription_requests); - setHasNextRequest(data?.subscription_requests.length > 1); - setTotalPages(data?.page?.total_pages); - const requestData = fetchRequest(data?.subscription_requests[0].RequestID); - requestData.then(async (data) => { - console.log(data) - if (data) setCurrentRequest(data); - }); - }); + loadCurrentRequest(previousRequestIndex); }; @@ -304,11 +390,54 @@ const ReviewPage = () => { }); }, []); - const fileURL = currentRequest ? baseURL + currentRequest["Files"][selectedFileId]["File URL"].replace("http://be-ctel-sbt:9000/api", "") : "dummy.pdf"; + const fileURL = (currentRequest && currentRequest["Files"][selectedFileId]) ? baseURL + currentRequest["Files"][selectedFileId]["File URL"].replace("http://be-ctel-sbt:9000/api", "") : "dummy.pdf"; + + const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, + }; + + // "Key", "Accuracy", "Submitted", "Revised" + interface DataType { + key: string; + accuracy: number; + submitted: string; + revised: string; + }; + + const handleSave = (row: DataType) => { + const newData = [...dataSource]; + console.log(row); + // const index = newData.findIndex((item) => row.key === item.key); + // const item = newData[index]; + // newData.splice(index, 1, { + // ...item, + // ...row, + // }); + // setDataSource(newData); + }; + + const columns = defaultColumns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record: DataType) => ({ + record, + editable: col.editable, + dataIndex: col.dataIndex, + title: col.title, + handleSave, + }), + }; + }); return (
{ zIndex: 1000, } : { height: '100%', + position: 'relative', }}> +
+ +
- + - } /> + } + value={pageIndexToGoto} + onChange={e => { + setPageIndexToGoto(parseInt(e.target.value)); + }} + />

{totalPages ? ("Request: " + currentRequestIndex + "/" + totalPages) : "No Request. Adjust your search criteria to see more results."}

@@ -573,7 +734,9 @@ const ReviewPage = () => { overflowY: 'auto', }} > - 'editable-row'} + bordered dataSource={dataSource} columns={columns} /> From 63aa41cd9f866d97b3550c2e9bfbc8e398dd51a0 Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 09:06:41 +0700 Subject: [PATCH 08/17] refactor code --- cope2n-api/fwd_api/api/accuracy_view.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index 8f7c798..d769f5a 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -341,7 +341,7 @@ class AccuracyViewSet(viewsets.ViewSet): location=OpenApiParameter.QUERY, description='Start date (YYYY-mm-DDTHH:MM:SSZ)', type=OpenApiTypes.DATE, - default='2023-01-02T00:00:00+0700', + default='2024-01-02T00:00:00+0700', ), OpenApiParameter( name='end_date', @@ -546,8 +546,6 @@ class AccuracyViewSet(viewsets.ViewSet): return response return JsonResponse({'error': 'Invalid request method.'}, status=405) -class RequestViewSet(viewsets.ViewSet): - lookup_field = "username" @extend_schema( request={ @@ -562,7 +560,7 @@ class RequestViewSet(viewsets.ViewSet): }, }, responses=None, - tags=['Request'] + tags=['Accuracy'] ) @action(detail=False, url_path=r"request/(?P[\w\-]+)", methods=["GET", "POST"]) def get_subscription_request(self, request, request_id=None): From 1bd7f6c81e33927688f1ac15f607067f05ab73ac Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 09:18:13 +0700 Subject: [PATCH 09/17] UPDATE: add request id field to review object --- cope2n-api/fwd_api/api/accuracy_view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index d769f5a..890c515 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -679,6 +679,7 @@ class AccuracyViewSet(viewsets.ViewSet): for field in ['retailername', 'sold_to_party', 'purchase_date', 'imei_number']: if not field in reviewed_result.keys(): raise RequiredFieldException(excArgs=f'reviewed_result.{field}') + reviewed_result['request_id'] = request_id for subscription_request_file in subscription_request_files: if subscription_request_file.doc_type == 'invoice': From 7a3ad4c78cf82b00bff6ef4f6088c747009ae873 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 09:29:49 +0700 Subject: [PATCH 10/17] Implement save method for revised data --- cope2n-fe/src/locales/en/messages.json | 6 +- cope2n-fe/src/locales/vi/messages.json | 6 +- cope2n-fe/src/pages/reviews/index.tsx | 76 +++++++++++++++----------- 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/cope2n-fe/src/locales/en/messages.json b/cope2n-fe/src/locales/en/messages.json index 0cfbc92..e21a284 100644 --- a/cope2n-fe/src/locales/en/messages.json +++ b/cope2n-fe/src/locales/en/messages.json @@ -4,7 +4,7 @@ "Back to Dashboard": "Back to Dashboard", "Create New Report": "Create New Report", "Dashboard": "Dashboard", - "Date": "Date", + "Date (GMT+8)": "Date (GMT+8)", "Download": "Download", "Download Report": "Download Report", "Duration": "Duration", @@ -12,6 +12,7 @@ "English": "English", "Go to Reports": "Go to Reports", "Inference": "Inference", + "Is Test": "Is Test", "Language": "Language", "Login": "Login", "Logout": "Logout", @@ -28,8 +29,11 @@ "Please specify a password": "Please specify a password", "Please specify a username": "Please specify a username", "Report Details": "Report Details", + "Report Filters": "Report Filters", "Reports": "Reports", "Retry": "Retry", + "Review": "Review", + "Reviewed": "Reviewed", "Service temporarily unavailable.": "Service temporarily unavailable.", "Something went wrong.": "Something went wrong.", "Sorry, something went wrong.": "Sorry, something went wrong.", diff --git a/cope2n-fe/src/locales/vi/messages.json b/cope2n-fe/src/locales/vi/messages.json index e277afd..950c2bb 100644 --- a/cope2n-fe/src/locales/vi/messages.json +++ b/cope2n-fe/src/locales/vi/messages.json @@ -4,7 +4,7 @@ "Back to Dashboard": "", "Create New Report": "", "Dashboard": "", - "Date": "", + "Date (GMT+8)": "", "Download": "", "Download Report": "", "Duration": "", @@ -12,6 +12,7 @@ "English": "Tiếng Anh", "Go to Reports": "", "Inference": "", + "Is Test": "", "Language": "Ngôn ngữ", "Login": "Đăng nhập", "Logout": "Đăng xuất", @@ -28,8 +29,11 @@ "Please specify a password": "Vui lòng nhập một mật khẩu", "Please specify a username": "Vui lòng nhập một tên tài khoản", "Report Details": "", + "Report Filters": "", "Reports": "", "Retry": "Thử lại", + "Review": "", + "Reviewed": "", "Service temporarily unavailable.": "Dịch vụ máy chủ hiện tại không sẵn sàng.", "Something went wrong.": "Có lỗi xảy ra", "Sorry, something went wrong.": "Hệ thống gặp lỗi", diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 312aa5c..5468049 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -305,26 +305,7 @@ const ReviewPage = () => { const [currentRequestIndex, setCurrentRequestIndex] = useState(1); const [hasNextRequest, setHasNextRequest] = useState(true); const [totalPages, setTotalPages] = useState(0); - - // purchase_date: "2024-01-20", - // retailername: "Test Retailer", - // sold_to_party: "Test Party", - const dataSource = []; - - const predicted = (currentRequest && currentRequest["Reviewed Result"]) ? currentRequest["Reviewed Result"] : {}; - const submitted = (currentRequest && currentRequest["Feedback Result"]) ? currentRequest["Feedback Result"] : {}; - const revised = (currentRequest && currentRequest["Reviewed Result"]) ? currentRequest["Reviewed Result"] : {}; - - - const keys = Object.keys(predicted); - 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]]; - dataSource.push(instance); - } + const [dataSource, setDataSource] = useState([]); const [pageIndexToGoto, setPageIndexToGoto] = useState(1); @@ -337,6 +318,21 @@ const ReviewPage = () => { const requestData = fetchRequest(data?.subscription_requests[0].RequestID); requestData.then(async (data) => { if (data) setCurrentRequest(data); + const predicted = (data && data["Reviewed Result"]) ? data["Reviewed 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); + setLoading(false); }).finally(() => { setLoading(false); }); @@ -407,16 +403,38 @@ const ReviewPage = () => { revised: string; }; + const updateRevisedData = async (newRevisedData:any) => { + const requestID = newRevisedData.request_id; + const token = localStorage.getItem('sbt-token') || ''; + console.log(newRevisedData) + await fetch(`${baseURL}/ctel/request/${requestID}/`, { + method: 'POST', + headers: { + "Authorization": `${JSON.parse(token)}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(newRevisedData), + }).catch((error) => { + console.log(error); + message.error("Could not update revised data"); + }); + }; + const handleSave = (row: DataType) => { const newData = [...dataSource]; console.log(row); - // const index = newData.findIndex((item) => row.key === item.key); - // const item = newData[index]; - // newData.splice(index, 1, { - // ...item, - // ...row, - // }); - // setDataSource(newData); + 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); }; const columns = defaultColumns.map((col) => { @@ -643,10 +661,6 @@ const ReviewPage = () => { }} > { - setFilterDateRange(value); - }} style={{ width: 200 }} /> From 3749cf83a8a54b591dcb7894aa33144e698470fc Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 09:36:45 +0700 Subject: [PATCH 11/17] FIX: check the imei number array length --- cope2n-api/fwd_api/api/accuracy_view.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index 890c515..fe119f4 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -686,7 +686,13 @@ class AccuracyViewSet(viewsets.ViewSet): subscription_request_file.reviewed_result = reviewed_result subscription_request_file.reviewed_result['imei_number'] = [] elif subscription_request_file.doc_type == 'imei': - subscription_request_file.reviewed_result = {"retailername": None, "sold_to_party": None, "purchase_date": [], "imei_number": [reviewed_result["imei_number"][subscription_request_file.index_in_request]]} + subscription_request_file.reviewed_result = { + "retailername": None, + "sold_to_party": None, + "purchase_date": [], + "imei_number": []} + if len(reviewed_result["imei_number"]) - 1 >= subscription_request_file.index_in_request: + subscription_request_file.reviewed_result["imei_number"] = reviewed_result["imei_number"][subscription_request_file.index_in_request] subscription_request_file.save() subscription_request.reviewed_result = reviewed_result From f88afa54445d420e0107083698b28ff2b16f521e Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 09:42:27 +0700 Subject: [PATCH 12/17] FIX: remove view --- cope2n-api/fwd_api/api_router.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cope2n-api/fwd_api/api_router.py b/cope2n-api/fwd_api/api_router.py index 322cc20..9a466dc 100755 --- a/cope2n-api/fwd_api/api_router.py +++ b/cope2n-api/fwd_api/api_router.py @@ -2,7 +2,7 @@ from django.conf import settings from rest_framework.routers import DefaultRouter, SimpleRouter from fwd_api.api.ctel_view import CtelViewSet -from fwd_api.api.accuracy_view import AccuracyViewSet, RequestViewSet +from fwd_api.api.accuracy_view import AccuracyViewSet from fwd_api.api.ctel_user_view import CtelUserViewSet @@ -16,7 +16,6 @@ else: router.register("ctel", CtelViewSet, basename="CtelAPI") router.register("ctel", CtelUserViewSet, basename="CtelUserAPI") router.register("ctel", AccuracyViewSet, basename="AccuracyAPI") -router.register("ctel", RequestViewSet, basename="RequestAPI") app_name = "api" urlpatterns = router.urls From d156c1d937b2d84384ac28edf768b8ce015f7f04 Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 10:42:04 +0700 Subject: [PATCH 13/17] FIX: check invalid --- cope2n-api/fwd_api/api/accuracy_view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index fe119f4..4da0c0b 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -675,7 +675,10 @@ class AccuracyViewSet(viewsets.ViewSet): subscription_request_files = SubscriptionRequestFile.objects.filter(request=subscription_request.id) - reviewed_result = json.loads(data["reviewed_result"]) + if not data["reviewed_result"]: + raise InvalidException(excArgs=f'reviewed_result') + + reviewed_result = data["reviewed_result"] for field in ['retailername', 'sold_to_party', 'purchase_date', 'imei_number']: if not field in reviewed_result.keys(): raise RequiredFieldException(excArgs=f'reviewed_result.{field}') From 21a805334a39c9cdbf3bb93834f1079bd16e20e0 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 10:43:14 +0700 Subject: [PATCH 14/17] Handle changes for revised data --- cope2n-fe/src/pages/reviews/index.tsx | 76 +++++++++++++-------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 5468049..66f74d8 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -4,7 +4,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import type { GetRef } from 'antd'; import { Layout } from 'antd'; import { - EditOutlined, DownloadOutlined, CheckCircleOutlined, + DownloadOutlined, CheckCircleOutlined, ArrowLeftOutlined, ArrowRightOutlined, FullscreenOutlined, @@ -129,21 +129,7 @@ const EditableCell: React.FC = ({ return
; }; -type EditableTableProps = Parameters[0]; - - -type ColumnTypes = Exclude; - - -const StyledEditOutlined = styled(EditOutlined)` - & { - color: #6666ff; - margin-left: 8px; - } - &:hover { - color: #0000ff; - } -`; +// type EditableTableProps = Parameters[0]; const defaultColumns = [ { @@ -152,15 +138,15 @@ const defaultColumns = [ key: 'key', width: 200, }, - { - title: 'Accuracy', - dataIndex: 'acc', - key: 'acc', - render: (text, record) => { - return
100%
; - }, - width: 150, - }, + // { + // title: 'Accuracy', + // dataIndex: 'acc', + // key: 'acc', + // render: (text, record) => { + // return
100%
; + // }, + // width: 150, + // }, { title: 'Predicted', dataIndex: 'predicted', @@ -287,9 +273,7 @@ const ReviewPage = () => { const [fullscreen, setFullscreen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedFileId, setSelectedFileId] = useState(0); - const selectFileByIndex = (index) => { - setSelectedFileId(index); - }; + const [selectedFileData, setSelectedFileData] = useState(null); // Default date range: 1 month ago to today const [filterDateRange, setFilterDateRange] = useState([ @@ -356,6 +340,19 @@ const ReviewPage = () => { loadCurrentRequest(previousRequestIndex); }; + const setAndLoadSelectedFile = (index) => { + setCurrentRequestIndex(index); + const fileURL = currentRequest["Files"][index]["File URL"]; + fetch(fileURL) + .then(response => response.blob()) + .then(blob => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = () => { + setSelectedFileData(reader.result); + }; + }); + }; const reloadFilters = () => { setCurrentRequestIndex(1); @@ -365,8 +362,10 @@ const ReviewPage = () => { setHasNextRequest(data?.subscription_requests.length > 1); const firstRequest = fetchRequest(data?.subscription_requests[0].RequestID); firstRequest.then(async (data) => { - console.log(firstRequest) if (data) setCurrentRequest(data); + setTimeout(() => { + setAndLoadSelectedFile(0); + }, 300); }); }); @@ -386,8 +385,6 @@ const ReviewPage = () => { }); }, []); - const fileURL = (currentRequest && currentRequest["Files"][selectedFileId]) ? baseURL + currentRequest["Files"][selectedFileId]["File URL"].replace("http://be-ctel-sbt:9000/api", "") : "dummy.pdf"; - const components = { body: { row: EditableRow, @@ -413,7 +410,9 @@ const ReviewPage = () => { "Authorization": `${JSON.parse(token)}`, "Content-Type": "application/json", }, - body: JSON.stringify(newRevisedData), + body: JSON.stringify({ + "reviewed_result": newRevisedData + }), }).catch((error) => { console.log(error); message.error("Could not update revised data"); @@ -422,7 +421,6 @@ const ReviewPage = () => { const handleSave = (row: DataType) => { const newData = [...dataSource]; - console.log(row); const index = newData.findIndex((item) => row.key === item.key); const item = newData[index]; newData.splice(index, 1, { @@ -524,7 +522,7 @@ const ReviewPage = () => { {currentRequest?.Files.map((file, index) => ( { - setSelectedFileId(index); + setAndLoadSelectedFile(index); } } /> ))} @@ -534,11 +532,9 @@ const ReviewPage = () => { flexGrow: 1, height: '100%', }}> - { }, csvDelimiter: ",", // "," as default, pdfVerticalScrollByDefault: true, // false as default - }} /> + }} />} From 683ad1bec566c232bd7ca0d0bdb7be607a16643a Mon Sep 17 00:00:00 2001 From: daovietanh99 Date: Thu, 22 Feb 2024 14:53:33 +0700 Subject: [PATCH 15/17] FIX: fix request api --- cope2n-api/fwd_api/api/accuracy_view.py | 68 +++++++++++++++++++++---- cope2n-api/fwd_api/utils/accuracy.py | 2 + 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index 4da0c0b..f0498cd 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -112,6 +112,7 @@ class AccuracyViewSet(viewsets.ViewSet): try: start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d') # We care only about day precision only end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d') + end_date = end_date + timezone.timedelta(days=1) # Round: # end_date_str to the beginning of the next day # start_date_str to the start of the date @@ -119,7 +120,7 @@ class AccuracyViewSet(viewsets.ViewSet): end_date = timezone.make_aware(end_date) start_date_str = start_date.strftime('%Y-%m-%dT%H:%M:%S%z') # inside logic will include second precision with timezone for calculation - end_date_str = (end_date + timezone.timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S%z') + end_date_str = end_date.strftime('%Y-%m-%dT%H:%M:%S%z') base_query &= Q(created_at__range=(start_date, end_date)) except Exception as e: raise InvalidException(excArgs="Date format") @@ -128,15 +129,13 @@ class AccuracyViewSet(viewsets.ViewSet): base_query &= Q(request_id=request_id) if redemption_id: base_query &= Q(redemption_id=redemption_id) - base_query &= Q(is_test_request=False) if isinstance(include_test, str): include_test = True if include_test=="true" else False - if include_test: - # base_query = ~base_query - base_query.children = base_query.children[:-1] + if not include_test: + base_query &= Q(is_test_request=False) elif isinstance(include_test, bool): - if include_test: - base_query = ~base_query + if not include_test: + base_query &= Q(is_test_request=False) if isinstance(is_reviewed, str): if is_reviewed == "reviewed": base_query &= Q(is_reviewed=True) @@ -675,7 +674,7 @@ class AccuracyViewSet(viewsets.ViewSet): subscription_request_files = SubscriptionRequestFile.objects.filter(request=subscription_request.id) - if not data["reviewed_result"]: + if "reviewed_result" not in data: raise InvalidException(excArgs=f'reviewed_result') reviewed_result = data["reviewed_result"] @@ -690,9 +689,9 @@ class AccuracyViewSet(viewsets.ViewSet): subscription_request_file.reviewed_result['imei_number'] = [] elif subscription_request_file.doc_type == 'imei': subscription_request_file.reviewed_result = { - "retailername": None, - "sold_to_party": None, - "purchase_date": [], + "retailername": None, + "sold_to_party": None, + "purchase_date": [], "imei_number": []} if len(reviewed_result["imei_number"]) - 1 >= subscription_request_file.index_in_request: subscription_request_file.reviewed_result["imei_number"] = reviewed_result["imei_number"][subscription_request_file.index_in_request] @@ -706,3 +705,50 @@ class AccuracyViewSet(viewsets.ViewSet): return JsonResponse({'message': 'success.'}, status=200) else: return JsonResponse({'error': 'Invalid request method.'}, status=405) + + @extend_schema( + request={ + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'reason': { + 'type': 'string', + 'default': '''"Sample reason"''', + }, + }, + }, + }, + responses=None, + tags=['Accuracy'] + ) + @action(detail=False, url_path=r"request_image/(?P[\w\-]+)/(?P[\w\-]+)", methods=["POST"]) + def request_image(self, request, request_id=None, request_image_id=None): + if request.method == 'POST': + data = request.data + + base_query = Q(request_id=request_id) + + subscription_request = SubscriptionRequest.objects.filter(base_query) + + if subscription_request.count() == 0: + raise NotFoundException(excArgs=request_id) + + subscription_request = subscription_request.first() + + subscription_request_files = SubscriptionRequestFile.objects.filter(request=subscription_request.id) + + if "reason" not in data: + raise InvalidException(excArgs=f'reason') + + reason = data["reason"] + + is_available = False + for subscription_request_file in subscription_request_files: + if subscription_request_file.file_name.split(".")[0] == request_image_id: + subscription_request_file.reason = reason + subscription_request_file.save() + is_available = True + if not is_available: + raise NotFoundException(excArgs=request_id + "/" + request_image_id) + else: + return JsonResponse({'error': 'Invalid request method.'}, status=405) diff --git a/cope2n-api/fwd_api/utils/accuracy.py b/cope2n-api/fwd_api/utils/accuracy.py index 36a1e27..1e8016d 100644 --- a/cope2n-api/fwd_api/utils/accuracy.py +++ b/cope2n-api/fwd_api/utils/accuracy.py @@ -615,6 +615,8 @@ def predict_result_to_ready(result): "sold_to_party": "", "purchase_date": [], "imei_number": [],} + if not result: + return dict_result dict_result["retailername"] = result.get("content", {}).get("document", [{}])[0].get("content", [{}])[0].get("value", None) dict_result["sold_to_party"] = result.get("content", {}).get("document", [{}])[0].get("content", [{}, {}])[1].get("value", None) dict_result["purchase_date"] = result.get("content", {}).get("document", [{}])[0].get("content", [{}, {}, {}])[2].get("value", []) From 4a483e1519bb8cd926ec30e01a76abea5f4d51ab Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 16:36:03 +0700 Subject: [PATCH 16/17] View images on review UI --- cope2n-fe/package-lock.json | 10 ++ cope2n-fe/package.json | 43 +++--- cope2n-fe/src/pages/reviews/index.tsx | 209 +++++++++++++++++--------- 3 files changed, 170 insertions(+), 92 deletions(-) diff --git a/cope2n-fe/package-lock.json b/cope2n-fe/package-lock.json index d24b175..e5db921 100644 --- a/cope2n-fe/package-lock.json +++ b/cope2n-fe/package-lock.json @@ -25,6 +25,7 @@ "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", + "react-hotkeys-hook": "^4.5.0", "react-json-view-lite": "^1.2.1", "react-office-viewer": "^1.0.4", "react-router-dom": "^6.6.1", @@ -11407,6 +11408,15 @@ "react": "^18.2.0" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz", + "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-i18next": { "version": "11.18.6", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", diff --git a/cope2n-fe/package.json b/cope2n-fe/package.json index c7e2006..769f0c1 100644 --- a/cope2n-fe/package.json +++ b/cope2n-fe/package.json @@ -27,27 +27,28 @@ }, "dependencies": { "@ant-design/colors": "^6.0.0", - "@ant-design/icons": "^4.8.0", - "@ant-design/plots": "^1.2.3", - "@ant-design/pro-layout": "^7.10.3", - "@babel/core": "^7.13.10", - "@cyntler/react-doc-viewer": "^1.14.1", - "@tanstack/react-query": "^4.20.4", - "antd": "^5.4.0", - "axios": "^1.2.2", - "chart.js": "^4.4.1", - "history": "^5.3.0", - "lodash-es": "^4.17.21", - "mousetrap": "^1.6.5", - "process": "^0.11.10", - "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", - "react-dom": "^18.2.0", - "react-json-view-lite": "^1.2.1", - "react-office-viewer": "^1.0.4", - "react-router-dom": "^6.6.1", - "styled-components": "^5.3.6", - "uuid": "^9.0.0" + "@ant-design/icons": "^4.8.0", + "@ant-design/plots": "^1.2.3", + "@ant-design/pro-layout": "^7.10.3", + "@babel/core": "^7.13.10", + "@cyntler/react-doc-viewer": "^1.14.1", + "@tanstack/react-query": "^4.20.4", + "antd": "^5.4.0", + "axios": "^1.2.2", + "chart.js": "^4.4.1", + "history": "^5.3.0", + "lodash-es": "^4.17.21", + "mousetrap": "^1.6.5", + "process": "^0.11.10", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "react-hotkeys-hook": "^4.5.0", + "react-json-view-lite": "^1.2.1", + "react-office-viewer": "^1.0.4", + "react-router-dom": "^6.6.1", + "styled-components": "^5.3.6", + "uuid": "^9.0.0" }, "devDependencies": { "@babel/plugin-syntax-jsx": "^7.12.13", diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 66f74d8..028e48a 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -9,12 +9,14 @@ import { ArrowRightOutlined, FullscreenOutlined, FullscreenExitOutlined, + ClockCircleFilled, } from '@ant-design/icons'; import FileViewer from '@cyntler/react-doc-viewer'; import styled from 'styled-components'; const { Sider, Content } = Layout; import { baseURL } from "request/api"; import moment from 'moment'; +import { useHotkeys } from "react-hotkeys-hook"; const siderStyle: React.CSSProperties = { @@ -138,15 +140,6 @@ const defaultColumns = [ key: 'key', width: 200, }, - // { - // title: 'Accuracy', - // dataIndex: 'acc', - // key: 'acc', - // render: (text, record) => { - // return
100%
; - // }, - // width: 150, - // }, { title: 'Predicted', dataIndex: 'predicted', @@ -166,7 +159,7 @@ const defaultColumns = [ ]; -const FileCard = ({ file, isSelected, onClick }) => { +const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { const fileName = file["File Name"]; return ( @@ -179,6 +172,7 @@ const FileCard = ({ file, isSelected, onClick }) => { marginTop: '2px', position: 'relative', height: '100px', + overflow: 'hidden', }} onClick={onClick}>
{ }}> -    Request ID: {currentRequest?.RequestID} + {totalRequests ? <>   Request ID: {currentRequest?.RequestID} : ""}
{ display: 'flex', flexDirection: 'row', }}> -
0 &&
{ {currentRequest?.Files.map((file, index) => ( { - setAndLoadSelectedFile(index); + setAndLoadSelectedFile(currentRequest, index); } - } /> + } setIsReasonModalOpen={setIsReasonModalOpen} /> ))} -
+
}
- {selectedFileData && Failed to load file.

: ( fileExtension === "pdf" ? ( { }, csvDelimiter: ",", // "," as default, pdfVerticalScrollByDefault: true, // false as default - }} />} + }} />) :
file
)}
- {/* } color="warning" style={{ padding: "4px 16px", marginLeft: 8 }}> - Not Reviewed - */} - } color="success" style={{ padding: "4px 16px", marginLeft: 8 }}> - Reviewed - - +

{totalRequests ? ("Request: " + currentRequestIndex + "/" + totalRequests) : "No Request. Adjust your search criteria to see more results."}

+ {totalRequests > 0 &&
+ + + + + +
+ {currentRequest && (currentRequest["Is Reviewed"] ? } color="success" style={{ padding: "4px 16px" }}> + Reviewed + : } color="warning" style={{ padding: "4px 16px" }}> + Not Reviewed + )} +
+
}
{ }} > { + setFilterDateRange(dateString); + }} style={{ width: 200 }} /> @@ -738,7 +762,50 @@ const ReviewPage = () => { -
{ + + } + } + onCancel={ + () => { + setIsReasonModalOpen(false); + } + } + > + {currentRequest && JSON.stringify(currentRequest["Files"][selectedFileId])} +
+ +
{childNode}{childNode}