From 59b8036949ceec3f79b7230709aca4f2dd202390 Mon Sep 17 00:00:00 2001 From: Vu Khanh Du Date: Mon, 18 Mar 2024 14:30:34 +0700 Subject: [PATCH] fix dashboard & submit bad reason review --- .../report-detail/report-overview-table.tsx | 57 +- cope2n-fe/src/pages/reviews/index.tsx | 945 +++++++++++------- cope2n-fe/src/request/index.ts | 1 + cope2n-fe/src/request/reviews.ts | 32 + 4 files changed, 655 insertions(+), 380 deletions(-) create mode 100644 cope2n-fe/src/request/reviews.ts 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 dd39d00..f70f92a 100644 --- a/cope2n-fe/src/components/report-detail/report-overview-table.tsx +++ b/cope2n-fe/src/components/report-detail/report-overview-table.tsx @@ -1,7 +1,12 @@ import type { TableColumnsType } from 'antd'; import { Table } from 'antd'; import React from 'react'; -import { ensureMax, ensureMin, formatPercent, formatNumber } from 'utils/metric-format'; +import { + ensureMax, + ensureMin, + formatNumber, + formatPercent, +} from 'utils/metric-format'; interface DataType { key: React.Key; @@ -18,8 +23,7 @@ interface DataType { purchaseDateAAR: number; retailerNameAAR: number; invoiceNumberAAR: number; - snImeiAPT: number; // APT: Average Processing Time - invoiceAPT: number; + avgAPT: number; // APT: Average Processing Time snImeiTC: number; // TC: transaction count invoiceTC: number; reviewProgress: number; @@ -230,34 +234,16 @@ const columns: TableColumnsType = [ }, { title: 'Average Processing Time Per Image (Seconds)', - children: [ - { - title: 'SN/IMEI', - dataIndex: 'snImeiAPT', - key: 'snImeiAPT', - render: (_, record) => { - const isAbnormal = ensureMax(record.snImeiAPT, 2); - return ( - - {formatNumber(record?.snImeiAPT)} - - ); - }, - }, - { - title: 'Invoice', - dataIndex: 'invoiceAPT', - key: 'invoiceAPT', - render: (_, record) => { - const isAbnormal = ensureMax(record.invoiceAPT, 2); - return ( - - {formatNumber(record?.invoiceAPT)} - - ); - }, - }, - ], + dataIndex: 'avgAPT', + key: 'avgAPT', + render: (_, record) => { + const isAbnormal = ensureMax(record.avgAPT, 2); + return ( + + {formatNumber(record?.avgAPT)} + + ); + }, }, { title: 'Review Progress', @@ -267,7 +253,9 @@ const columns: TableColumnsType = [ render: (_, record) => { return ( - {formatPercent(record.reviewProgress) === '-' ? 0 : formatPercent(record.reviewProgress)} + {formatPercent(record.reviewProgress) === '-' + ? 0 + : formatPercent(record.reviewProgress)} ); }, @@ -301,11 +289,10 @@ const ReportOverViewTable: React.FC = ({ purchaseDateAAR: item.average_accuracy_rate.purchase_date, retailerNameAAR: item.average_accuracy_rate.retailer_name, invoiceNumberAAR: item.average_accuracy_rate.invoice_no, - snImeiAPT: item.average_processing_time.imei, - invoiceAPT: item.average_processing_time.invoice, + avgAPT: item.average_processing_time.avg, snImeiTC: item.usage.imei, invoiceTC: item.usage.invoice, - reviewProgress:item.review_progress, + reviewProgress: item.review_progress, }; }, ); diff --git a/cope2n-fe/src/pages/reviews/index.tsx b/cope2n-fe/src/pages/reviews/index.tsx index 5dd649c..f3fc856 100644 --- a/cope2n-fe/src/pages/reviews/index.tsx +++ b/cope2n-fe/src/pages/reviews/index.tsx @@ -1,39 +1,52 @@ -import { t } from '@lingui/macro'; -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, + CheckCircleOutlined, ClockCircleFilled, + DownloadOutlined, + FullscreenExitOutlined, + FullscreenOutlined, } from '@ant-design/icons'; +import { t } from '@lingui/macro'; +import type { GetRef } from 'antd'; +import { + Button, + DatePicker, + Form, + Input, + Layout, + message, + Modal, + notification, + Select, + Space, + Spin, + Table, + Tag, +} from 'antd'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { baseURL } from 'request/api'; import styled from 'styled-components'; -const { Sider, Content } = Layout; -import { baseURL } from "request/api"; -import { useHotkeys } from "react-hotkeys-hook"; // Import the main component import { Viewer } from '@react-pdf-viewer/core'; // Import the styles import '@react-pdf-viewer/core/lib/styles/index.css'; +import { badQualityReasonSubmit } from 'request'; import { normalizeData } from 'utils/field-value-process'; - +const { Sider, Content } = Layout; const siderStyle: React.CSSProperties = { backgroundColor: '#fafafa', padding: 10, width: 200, }; - const StyledTable = styled(Table)` & .sbt-table-cell { - padding: 4px!important; + padding: 4px !important; } `; @@ -74,7 +87,6 @@ interface EditableCellProps { handleSave: (record: Item) => void; } - const EditableCell: React.FC = ({ title, editable, @@ -114,14 +126,15 @@ const EditableCell: React.FC = ({ if (editable) { childNode = editing ? ( - + ) : ( -
+
{children}
); @@ -133,74 +146,97 @@ const EditableCell: React.FC = ({ // type EditableTableProps = Parameters[0]; const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { - const fileName = file["File Name"]; + const fileName = file['File Name']; return ( -
+
- {file["Doc Type"].toUpperCase()} - - {fileName ? fileName.substring(0, 25).replace("temp_", "") : fileName} + + {file['Doc Type'].toUpperCase()} + + + {fileName ? fileName.substring(0, 25).replace('temp_', '') : fileName}
-
- -
); - }; -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 = ""; +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}&`; @@ -218,13 +254,12 @@ const fetchAllRequests = async (filterDateRange, filterSubsidiaries, filterRevie 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; - }); + Authorization: `${JSON.parse(token)}`, + }, + }).then(async (res) => { + const data = await res.json(); + return data; + }); return data; }; @@ -233,10 +268,12 @@ const fetchRequest = async (id) => { const response = await fetch(`${baseURL}/ctel/request/${id}/`, { method: 'GET', headers: { - "Authorization": `${JSON.parse(token)}` - } + Authorization: `${JSON.parse(token)}`, + }, }); - return await (await response.json()).subscription_requests[0]; + return await ( + await response.json() + ).subscription_requests[0]; }; const ReviewPage = () => { @@ -249,9 +286,7 @@ const ReviewPage = () => { const [selectedFileName, setSelectedFileName] = useState(null); // Default date range: 1 month ago to today - const [filterDateRange, setFilterDateRange] = useState([ - "", "" - ]); + const [filterDateRange, setFilterDateRange] = useState(['', '']); const [filterSubsidiaries, setFilterSubsidiaries] = useState('SEAO'); const [filterReviewState, setFilterReviewState] = useState('all'); @@ -265,56 +300,74 @@ const ReviewPage = () => { const [pageIndexToGoto, setPageIndexToGoto] = useState(1); + const [reason, setReason] = useState(''); + const setAndLoadSelectedFile = async (requestData, index) => { setSelectedFileId(index); - if (!requestData["Files"][index]) { - setSelectedFileData("FAILED_TO_LOAD_FILE"); + 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 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); + console.log('Loading file: ' + fileName); + console.log('URL: ' + fileURL); } else { - setSelectedFileData("FAILED_TO_LOAD_FILE"); + setSelectedFileData('FAILED_TO_LOAD_FILE'); } }; const loadCurrentRequest = (requestIndex) => { setLoading(true); - fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, requestIndex, 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["Predicted Result"]) ? data["Predicted 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(() => { + fetchAllRequests( + filterDateRange, + filterSubsidiaries, + filterReviewState, + filterIncludeTests, + requestIndex, + 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['Predicted Result'] ? data['Predicted 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); }); - }).finally(() => { - setLoading(false); - }); }; const gotoNextRequest = () => { @@ -337,11 +390,20 @@ const ReviewPage = () => { const reloadFilters = () => { setCurrentRequestIndex(1); - fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, 1, 2).then((data) => { + fetchAllRequests( + filterDateRange, + filterSubsidiaries, + filterReviewState, + filterIncludeTests, + 1, + 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); + const firstRequest = fetchRequest( + data?.subscription_requests[0].RequestID, + ); firstRequest.then(async (data) => { if (data) setCurrentRequest(data); setAndLoadSelectedFile(data, 0); @@ -350,16 +412,24 @@ const ReviewPage = () => { }, 500); }); }); - }; useEffect(() => { setCurrentRequestIndex(1); - fetchAllRequests(filterDateRange, filterSubsidiaries, filterReviewState, filterIncludeTests, 1, 2).then((data) => { + fetchAllRequests( + filterDateRange, + filterSubsidiaries, + filterReviewState, + filterIncludeTests, + 1, + 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); + const firstRequest = fetchRequest( + data?.subscription_requests[0].RequestID, + ); firstRequest.then(async (data) => { if (data) setCurrentRequest(data); setAndLoadSelectedFile(data, 0); @@ -380,7 +450,7 @@ const ReviewPage = () => { accuracy: number; submitted: string; revised: string; - }; + } const updateRevisedData = async (newRevisedData: any) => { const requestID = currentRequest.RequestID; @@ -388,18 +458,18 @@ const ReviewPage = () => { const result = await fetch(`${baseURL}/ctel/request/${requestID}/`, { method: 'POST', headers: { - "Authorization": `${JSON.parse(token)}`, - "Content-Type": "application/json", + Authorization: `${JSON.parse(token)}`, + 'Content-Type': 'application/json', }, body: JSON.stringify({ - "reviewed_result": newRevisedData + reviewed_result: newRevisedData, }), }).catch((error) => { console.log(error); throw error; }); if (result.status != 200) { - throw new Error("Could not update revised data"); + throw new Error('Could not update revised data'); } }; @@ -416,17 +486,22 @@ const ReviewPage = () => { newData[i].revised = normalizeData(newData[i].key, newData[i].revised); newRevisedData[newData[i].key] = newData[i].revised; } - updateRevisedData(newRevisedData).then(() => { - // "[Is Reviewed]" => true - setCurrentRequest({ - ...currentRequest, - ["Is Reviewed"]: true, + updateRevisedData(newRevisedData) + .then(() => { + // "[Is Reviewed]" => true + setCurrentRequest({ + ...currentRequest, + ['Is Reviewed']: true, + }); }) - }).then(() => { - setDataSource(newData); - }).catch((error) => { - message.error("Could not update revised data. Please check the format."); - }); + .then(() => { + setDataSource(newData); + }) + .catch((error) => { + message.error( + 'Could not update revised data. Please check the format.', + ); + }); }; const submitRevisedData = async () => { @@ -440,13 +515,11 @@ const ReviewPage = () => { // "[Is Reviewed]" => true setCurrentRequest({ ...currentRequest, - ["Is Reviewed"]: true, - }) - }) + ['Is Reviewed']: true, + }); + }); }; - - const defaultColumns = [ { title: 'Key', @@ -459,21 +532,25 @@ const ReviewPage = () => { dataIndex: 'predicted', key: 'predicted', render: (text) => { - if (!text) return {""}; + if (!text) return {''}; const displayedContent = text; - if (typeof(displayedContent) === "string") { + if (typeof displayedContent === 'string') { return {displayedContent}; - } else if (typeof(displayedContent) === "object") { + } else if (typeof displayedContent === 'object') { if (displayedContent.length === 0) { - return {""}; + return {''}; } // Set all empty values to "" for (const key in displayedContent) { if (!displayedContent[key]) { - displayedContent[key] = ""; + displayedContent[key] = ''; } } - return {displayedContent.join(", ")}; + return ( + + {displayedContent.join(', ')} + + ); } return {displayedContent}; }, @@ -483,87 +560,103 @@ const ReviewPage = () => { dataIndex: 'submitted', key: 'submitted', render: (text) => { - if (!text) return {""}; + if (!text) return {''}; const displayedContent = text; - if (typeof(displayedContent) === "string") { + if (typeof displayedContent === 'string') { return {displayedContent}; - } else if (typeof(displayedContent) === "object") { + } else if (typeof displayedContent === 'object') { if (displayedContent.length === 0) { - return {""}; + return {''}; } // Set all empty values to "" for (const key in displayedContent) { if (!displayedContent[key]) { - displayedContent[key] = ""; + displayedContent[key] = ''; } } - return {displayedContent.join(", ")}; + return ( + + {displayedContent.join(', ')} + + ); } return {displayedContent}; }, }, { - title: (
Revised   - - -
), + Revised   + + +
+ ), dataIndex: 'revised', key: 'revised', editable: true, render: (text) => { - if (!text) return {""}; + if (!text) return {''}; const displayedContent = text; - if (typeof(displayedContent) === "string") { + if (typeof displayedContent === 'string') { return {displayedContent}; - } else if (typeof(displayedContent) === "object") { + } else if (typeof displayedContent === 'object') { if (displayedContent.length === 0) { - return {""}; + return {''}; } // Set all empty values to "" for (const key in displayedContent) { if (!displayedContent[key]) { - displayedContent[key] = ""; + displayedContent[key] = ''; } } - return {displayedContent.join(", ")}; + return ( + + {displayedContent.join(', ')} + + ); } return {displayedContent}; }, }, ]; - const columns = defaultColumns.map((col) => { if (!col.editable) { return col; @@ -572,7 +665,7 @@ const ReviewPage = () => { ...col, onCell: (record: DataType) => ({ record, - editable: col.key != "request_id" && col.editable, + editable: col.key != 'request_id' && col.editable, dataIndex: col.dataIndex, title: col.title, handleSave, @@ -581,107 +674,166 @@ const ReviewPage = () => { }); // use left/right keys to navigate - useHotkeys("left", gotoPreviousRequest); - useHotkeys("right", gotoNextRequest); + useHotkeys('left', gotoPreviousRequest); + useHotkeys('right', gotoNextRequest); - const fileExtension = selectedFileName ? selectedFileName.split('.').pop() : ''; + const fileExtension = selectedFileName + ? selectedFileName.split('.').pop() + : ''; return ( -
+
- + top: 0, + left: 0, + width: '100%', + background: '#00000033', + zIndex: 1000, + display: loading ? 'block' : 'none', + }} + > +
- - {totalRequests ? <>   Request ID: {currentRequest?.RequestID} : ""} + {totalRequests ? ( + <> +    Request ID: {currentRequest?.RequestID} + + ) : ( + '' + )}
- - - {totalRequests > 0 &&
-

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
)} + display: 'flex', + flexDirection: 'row', + }} + > + {totalRequests > 0 && ( +
+

+ 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 - - } + { + 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 => { + onChange={(e) => { setPageIndexToGoto(parseInt(e.target.value)); }} />
-

{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 - )} +

+ {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); - } - } + onOk={() => { + setIsModalOpen(false); + reloadFilters(); + }} + onCancel={() => { + setIsModalOpen(false); + }} >
{ onChange={setFilterSubsidiaries} /> -
+
{ title={t`Review`} open={isReasonModalOpen} width={700} - onOk={ - () => { + onOk={async () => { + // call submit api + if (!reason) { + notification.warning({ message: 'Please select a reason' }); + } else { + const params = { + request_id: currentRequest?.RequestID, + request_image_id: selectedFileName.replace(/\.[^/.]+$/, ''), + }; + const res = await badQualityReasonSubmit(params, reason); + if (res.message) { + notification.success({ message: 'Update reason success' }); + setIsReasonModalOpen(false); + } } - } - onCancel={ - () => { - setIsReasonModalOpen(false); - } - } + }} + onCancel={() => { + setIsReasonModalOpen(false); + }} > { { value: 'handwritten', label: t`Handwritten` }, { value: 'recheck', label: t`Recheck` }, ]} + onChange={setReason} /> - {totalRequests > 0 &&
- 'editable-row'} - bordered dataSource={dataSource} columns={columns} - /> -
} + {totalRequests > 0 && ( +
+ 'editable-row'} + bordered + dataSource={dataSource} + columns={columns} + /> +
+ )}
); }; diff --git a/cope2n-fe/src/request/index.ts b/cope2n-fe/src/request/index.ts index e5abc85..148240a 100644 --- a/cope2n-fe/src/request/index.ts +++ b/cope2n-fe/src/request/index.ts @@ -1 +1,2 @@ +export * from './reviews'; export * from './user'; diff --git a/cope2n-fe/src/request/reviews.ts b/cope2n-fe/src/request/reviews.ts new file mode 100644 index 0000000..00edb00 --- /dev/null +++ b/cope2n-fe/src/request/reviews.ts @@ -0,0 +1,32 @@ +import { notification } from 'antd'; +import { API } from './api'; + +type badQualityReasonSubmitParams = { + request_id: string; + request_image_id: string; +}; + +export async function badQualityReasonSubmit( + params: badQualityReasonSubmitParams, + reason: string, +) { + const data = { reason }; + + try { + const response = await API.post<{ message: string }>( + `/ctel/request_image/${params.request_id}/${params.request_image_id}/`, + data, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + return response.data; + } catch (error) { + notification.error({ + message: `${error?.message}`, + }); + console.log(error); + } +}