diff --git a/cope2n-api/fwd_api/api/accuracy_view.py b/cope2n-api/fwd_api/api/accuracy_view.py index ea08187..e68cb4f 100644 --- a/cope2n-api/fwd_api/api/accuracy_view.py +++ b/cope2n-api/fwd_api/api/accuracy_view.py @@ -16,12 +16,14 @@ 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, dict2xlsx, save_report_to_S3 +from ..utils.file import download_from_S3, dict2xlsx, save_report_to_S3, build_S3_url 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 ..utils.report import aggregate_overview +from fwd_api.utils.accuracy import predict_result_to_ready +import copy redis_client = RedisUtils() @@ -112,6 +114,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 +122,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 +131,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) @@ -341,7 +342,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', @@ -628,8 +629,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={ @@ -644,7 +643,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): @@ -657,6 +656,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 = [] @@ -668,26 +675,48 @@ 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, '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, '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, @@ -696,9 +725,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, @@ -729,19 +758,29 @@ class RequestViewSet(viewsets.ViewSet): subscription_request_files = SubscriptionRequestFile.objects.filter(request=subscription_request.id) - reviewed_result = json.loads(data["reviewed_result"]) + if "reviewed_result" not in data: + 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}') + reviewed_result['request_id'] = request_id for subscription_request_file in subscription_request_files: if subscription_request_file.doc_type == 'invoice': 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() - 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 @@ -750,3 +789,50 @@ class RequestViewSet(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/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 diff --git a/cope2n-api/fwd_api/utils/accuracy.py b/cope2n-api/fwd_api/utils/accuracy.py index 8ad333f..136cd5d 100644 --- a/cope2n-api/fwd_api/utils/accuracy.py +++ b/cope2n-api/fwd_api/utils/accuracy.py @@ -624,6 +624,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", []) 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" 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/components/report-detail/report-overview-table.tsx b/cope2n-fe/src/components/report-detail/report-overview-table.tsx index ab5fd25..f6aab1f 100644 --- a/cope2n-fe/src/components/report-detail/report-overview-table.tsx +++ b/cope2n-fe/src/components/report-detail/report-overview-table.tsx @@ -34,7 +34,7 @@ const columns: TableColumnsType = [ return String(record.subSidiaries).toUpperCase(); }, filters: [ - { text: 'ALL', value: 'ALL' }, + { text: 'SEAO', value: 'SEAO' }, { text: 'SEAU', value: 'SEAU' }, { text: 'SESP', value: 'SESP' }, { text: 'SME', value: 'SME' }, 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/dashboard/index.tsx b/cope2n-fe/src/pages/dashboard/index.tsx index 6d998cf..5c3dbe2 100644 --- a/cope2n-fe/src/pages/dashboard/index.tsx +++ b/cope2n-fe/src/pages/dashboard/index.tsx @@ -16,7 +16,7 @@ export interface ReportFormValues { const Dashboard = () => { const navigate = useNavigate(); const [duration, setDuration] = useState('30d'); - const [subsidiary, setSubsidiary] = useState('ALL'); + const [subsidiary, setSubsidiary] = useState('SEAO'); const [form] = Form.useForm(); const { isLoading, data } = useOverViewReport({ duration: duration, @@ -93,6 +93,7 @@ const Dashboard = () => { style={{ width: 200 }} options={[ { value: 'ALL', label: 'ALL' }, + { value: 'SEAO', label: 'SEAO' }, { value: 'SEAU', label: 'SEAU' }, { value: 'SESP', label: 'SESP' }, { value: 'SME', label: 'SME' }, @@ -100,7 +101,7 @@ const Dashboard = () => { { value: 'TSE', label: 'TSE' }, { value: 'SEIN', label: 'SEIN' }, ]} - defaultValue='ALL' + defaultValue='SEAO' value={subsidiary} onChange={(value) => { setSubsidiary(value); diff --git a/cope2n-fe/src/pages/reports/index.tsx b/cope2n-fe/src/pages/reports/index.tsx index 309753c..ee53149 100644 --- a/cope2n-fe/src/pages/reports/index.tsx +++ b/cope2n-fe/src/pages/reports/index.tsx @@ -112,7 +112,7 @@ const ReportsPage = () => { placeholder='Select a subsidiary' style={{ width: 200 }} options={[ - { value: 'ALL', label: 'ALL' }, + { value: 'SEAO', label: 'SEAO' }, { value: 'SEAU', label: 'SEAU' }, { value: 'SESP', label: 'SESP' }, { value: 'SME', label: 'SME' }, diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 616fb15..5bda53f 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -1,10 +1,23 @@ import { t } from '@lingui/macro'; -import { Button, message, Upload, Input, Table } from 'antd'; -import { SbtPageHeader } from 'components/page-header'; -import { useState } 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 { + DownloadOutlined, CheckCircleOutlined, + ArrowLeftOutlined, + 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 = { backgroundColor: '#fafafa', @@ -13,61 +26,142 @@ const siderStyle: React.CSSProperties = { }; -const fileList = [ - { - name: "invoice.pdf", - url: "/dummpy.pdf", - type: "invoice", - isBadQuality: false, - }, - { - name: "invoice.pdf", - url: "/dummpy.pdf", - type: "imei", - isBadQuality: true, +const StyledTable = styled(Table)` + & .sbt-table-cell { + padding: 4px!important; } -] +`; -const dataSource = [ - { - key: '1', - value: 'Mike', - }, - { - key: '2', - value: 'Mike', - }, - { - key: '3', - value: 'Mike', - }, -]; +type InputRef = GetRef; +type FormInstance = GetRef>; -const columns = [ +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 {childNode}; +}; + +// type EditableTableProps = Parameters[0]; + +const defaultColumns = [ { title: 'Key', dataIndex: 'key', key: 'key', + width: 200, }, { 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', + editable: true, }, ]; -const FileCard = ({ file, isSelected, onClick }) => { +const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { + const fileName = file["File Name"]; + return (
{ backgroundColor: isSelected ? '#d4ecff' : '#fff', padding: '4px 8px', marginRight: '4px', - marginTop: '4px', + marginTop: '2px', + position: 'relative', + height: '100px', + overflow: 'hidden', }} onClick={onClick}>
{ color: '#333', fontWeight: 'bold', padding: '4px 8px', - }}>{file.type.toUpperCase()} + cursor: 'default', + }}>{file["Doc Type"].toUpperCase()} {file.name} + cursor: 'default', + maxWidth: '50px', + overflow: 'hidden', + textOverflow: 'ellipsis', + }}> + {fileName ? fileName.substring(0, 25).replace("temp_", "") : fileName} + +
+
+ +
); }; -const InferencePage = () => { +const fetchAllRequests = async (filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, page = 1, page_size = 20) => { + const startDate = (filterDateRange && filterDateRange[0]) ? filterDateRange[0] : ''; + const endDate = (filterDateRange && filterDateRange[1]) ? filterDateRange[1] : ''; + let filterStr = ""; + filterStr += `page=${page}&page_size=${page_size}&`; + if (filterSubsidiaries) { + filterStr += `subsidiary=${filterSubsidiaries}&`; + } + if (filterReviewState) { + filterStr += `is_reviewed=${filterReviewState}&`; + } + if (filterIncludeTests) { + filterStr += `includes_test=${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 [loading, setLoading] = useState(false); + const [fullscreen, setFullscreen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isReasonModalOpen, setIsReasonModalOpen] = useState(false); const [selectedFileId, setSelectedFileId] = useState(0); - const selectFileByIndex = (index) => { + const [selectedFileData, setSelectedFileData] = useState(null); + const [selectedFileName, setSelectedFileName] = useState(null); + + // Default date range: 1 month ago to today + const [filterDateRange, setFilterDateRange] = useState([ + "", "" + ]); + + const [filterSubsidiaries, setFilterSubsidiaries] = useState('SEAO'); + 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 [totalRequests, setTotalPages] = useState(0); + const [dataSource, setDataSource] = useState([]); + + const [pageIndexToGoto, setPageIndexToGoto] = useState(1); + + const setAndLoadSelectedFile = async (requestData, index) => { setSelectedFileId(index); + if (!requestData["Files"][index]) { + setSelectedFileData("FAILED_TO_LOAD_FILE"); + return; + }; + const fileName = requestData["Files"][index]["File Name"]; + const fileURL = requestData["Files"][index]["File URL"]; + const response = await fetch(fileURL); + if (response.status === 200) { + setSelectedFileName(fileName); + setSelectedFileData(fileURL); + console.log("Loading file: " + fileName); + console.log("URL: " + fileURL); + } else { + setSelectedFileData("FAILED_TO_LOAD_FILE"); + } }; + const 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_requests); + 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); + setAndLoadSelectedFile(data, 0); + }).finally(() => { + setLoading(false); + }); + }).finally(() => { + setLoading(false); + }); + }; + + const gotoNextRequest = () => { + if (currentRequestIndex >= totalRequests) { + return; + } + const nextRequestIndex = currentRequestIndex + 1; + setCurrentRequestIndex(nextRequestIndex); + loadCurrentRequest(nextRequestIndex); + }; + + const gotoPreviousRequest = () => { + if (currentRequestIndex === 1) { + return; + } + const previousRequestIndex = currentRequestIndex - 1; + setCurrentRequestIndex(previousRequestIndex); + loadCurrentRequest(previousRequestIndex); + }; + + const reloadFilters = () => { + setCurrentRequestIndex(1); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, currentRequestIndex, 2).then((data) => { + setTotalPages(data?.page?.total_requests); + setRequests(data?.subscription_requests); + setHasNextRequest(data?.subscription_requests.length > 1); + const firstRequest = fetchRequest(data?.subscription_requests[0].RequestID); + firstRequest.then(async (data) => { + if (data) setCurrentRequest(data); + setAndLoadSelectedFile(data, 0); + }); + }); + + }; + + useEffect(() => { + setCurrentRequestIndex(1); + fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, currentRequestIndex, 2).then((data) => { + setTotalPages(data?.page?.total_requests); + setRequests(data?.subscription_requests); + setHasNextRequest(data?.subscription_requests.length > 1); + const firstRequest = fetchRequest(data?.subscription_requests[0].RequestID); + firstRequest.then(async (data) => { + if (data) setCurrentRequest(data); + setAndLoadSelectedFile(data, 0); + }); + }); + }, []); + + const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, + }; + + // "Key", "Accuracy", "Submitted", "Revised" + interface DataType { + key: string; + accuracy: number; + submitted: string; + revised: string; + }; + + const updateRevisedData = async (newRevisedData: any) => { + const requestID = newRevisedData.request_id; + const token = localStorage.getItem('sbt-token') || ''; + await fetch(`${baseURL}/ctel/request/${requestID}/`, { + method: 'POST', + headers: { + "Authorization": `${JSON.parse(token)}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + "reviewed_result": newRevisedData + }), + }).catch((error) => { + console.log(error); + message.error("Could not update revised data"); + }); + }; + + const handleSave = (row: DataType) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.key === item.key); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + ...row, + }); + setDataSource(newData); + const newRevisedData = {}; + for (let i = 0; i < newData.length; i++) { + newRevisedData[newData[i].key] = newData[i].revised; + } + updateRevisedData(newRevisedData).then(() => { + // "[Is Reviewed]" => true + setCurrentRequest({ + ...currentRequest, + ["Is Reviewed"]: true, + }) + }) + }; + + const columns = defaultColumns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record: DataType) => ({ + record, + editable: col.key != "request_id" && col.editable, + dataIndex: col.dataIndex, + title: col.title, + handleSave, + }), + }; + }); + + // use left/right keys to navigate + useHotkeys("left", gotoPreviousRequest); + useHotkeys("right", gotoNextRequest); + + const fileExtension = selectedFileName ? selectedFileName.split('.').pop() : ''; + return ( - <> - {/* */} +
+
+ +
+
+ + {totalRequests ? <>   Request ID: {currentRequest?.RequestID} : ""} +
- -
+ {totalRequests > 0 &&
- -
-
- {fileList.map((file, index) => ( - { - setSelectedFileId(index); + color: "#333", + padding: 10, + fontWeight: 'bold' + }} + >Files ({currentRequest?.Files?.length}) + {currentRequest?.Files.map((file, index) => ( + { + setAndLoadSelectedFile(currentRequest, index); + } + } setIsReasonModalOpen={setIsReasonModalOpen} /> + ))} +
} +
+ {selectedFileData === "FAILED_TO_LOAD_FILE" ?

Failed to load file.

: ( fileExtension === "pdf" ? () :
file
)} +
+ + + + + + +
+
+ + + { + if (pageIndexToGoto > totalRequests) { + message.error("RequestID is out of range."); + return; + } + if (pageIndexToGoto < 1) { + message.error("RequestID is out of range."); + return; + } + setCurrentRequestIndex(pageIndexToGoto); + loadCurrentRequest(pageIndexToGoto); + }}> + Go to + + } + value={pageIndexToGoto} + onChange={e => { + setPageIndexToGoto(parseInt(e.target.value)); + }} + />
- - -

Overview

- - - - - +
+

{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 + )}
- - - - + } + + + { + setIsModalOpen(false); + reloadFilters(); + } + } + onCancel={ + () => { + setIsModalOpen(false); + } + } + > +
+ + { + setFilterDateRange(dateString); + }} + style={{ width: 200 }} + /> + + + + + + + + +
+ {totalRequests > 0 &&
+ 'editable-row'} + bordered dataSource={dataSource} columns={columns} + /> +
} + ); }; -export default InferencePage; +export default ReviewPage; 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, diff --git a/cope2n-fe/src/request/report.ts b/cope2n-fe/src/request/report.ts index 585e7e7..2c1f55a 100644 --- a/cope2n-fe/src/request/report.ts +++ b/cope2n-fe/src/request/report.ts @@ -123,7 +123,7 @@ export async function downloadReport(report_id: string, downloadFinishedCallback } -export async function downloadDashboardReport(duration='30d', subsidiary='ALL') { +export async function downloadDashboardReport(duration='30d', subsidiary='SEAO') { try { const response = await API.get(`/ctel/overview_download_file/?duration=${duration}&subsidiary=${subsidiary}`, { responseType: 'blob', // Important