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