From 5fed071439af0252763a7dbca1b0aed11b9dbacc Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Tue, 20 Feb 2024 15:01:06 +0700 Subject: [PATCH 1/8] 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 2/8] 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 efd2da3a8282c69dded89227624a3011ffb785d6 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Wed, 21 Feb 2024 19:01:19 +0700 Subject: [PATCH 3/8] 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 d89cc70ec566c171a95a936a6787b12b749df10f Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 09:03:15 +0700 Subject: [PATCH 4/8] 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 7a3ad4c78cf82b00bff6ef4f6088c747009ae873 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 09:29:49 +0700 Subject: [PATCH 5/8] 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 21a805334a39c9cdbf3bb93834f1079bd16e20e0 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 10:43:14 +0700 Subject: [PATCH 6/8] 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 4a483e1519bb8cd926ec30e01a76abea5f4d51ab Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Thu, 22 Feb 2024 16:36:03 +0700 Subject: [PATCH 7/8] 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}