Implement dashboard, report page
This commit is contained in:
parent
40578d8b6a
commit
8ce84a940d
5669
cope2n-fe/package-lock.json
generated
5669
cope2n-fe/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
"name": "sbt-ui",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"start": "npm run extract && npm run compile && vite",
|
||||
"start": "npm run extract && npm run compile && vite --host",
|
||||
"build": "npm run extract && npm run compile && tsc && vite build",
|
||||
"serve": "vite preview",
|
||||
"extract": "lingui extract --clean",
|
||||
@ -31,16 +31,10 @@
|
||||
"@ant-design/plots": "^1.2.3",
|
||||
"@ant-design/pro-layout": "^7.10.3",
|
||||
"@babel/core": "^7.13.10",
|
||||
"@casl/ability": "^6.3.3",
|
||||
"@casl/react": "^3.1.0",
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@heartexlabs/label-studio": "1.4.0",
|
||||
"@tanstack/react-query": "^4.20.4",
|
||||
"antd": "^5.4.0",
|
||||
"axios": "^1.2.2",
|
||||
"chart.js": "^4.4.1",
|
||||
"dagre": "^0.8.5",
|
||||
"faker": "^6.6.6",
|
||||
"history": "^5.3.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
@ -48,7 +42,6 @@
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.6.1",
|
||||
"reactflow": "^11.4.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
|
@ -1,44 +0,0 @@
|
||||
import {
|
||||
AbilityBuilder,
|
||||
AbilityClass,
|
||||
MatchConditions,
|
||||
PureAbility,
|
||||
} from '@casl/ability';
|
||||
import { Project, User } from 'models';
|
||||
import { AppAbility } from './types';
|
||||
|
||||
const AppAbilityClass = PureAbility as AbilityClass<AppAbility>;
|
||||
|
||||
export const appAbilitiy = new AppAbilityClass([], {
|
||||
conditionsMatcher: (matchConditions: MatchConditions) => matchConditions,
|
||||
});
|
||||
|
||||
export function updateAbility(ability: AppAbility, user: User) {
|
||||
const { can, cannot, rules } = new AbilityBuilder(AppAbilityClass);
|
||||
|
||||
if (user.is_superuser) {
|
||||
can('manage', 'all');
|
||||
}
|
||||
|
||||
if (user.is_superuser === false) {
|
||||
can('read', 'Project');
|
||||
// @ts-ignore
|
||||
can<Project>(
|
||||
['manage', 'update', 'delete', 'labeling:confirm', 'training:terminate'],
|
||||
'Project',
|
||||
(project: Project) => {
|
||||
if (Array.isArray(project.admins)) {
|
||||
if (project.admins.find((item) => item.id === user.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
cannot('create', 'Project');
|
||||
}
|
||||
|
||||
// update ability
|
||||
ability.update(rules);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { createContextualCan, useAbility } from '@casl/react';
|
||||
import * as React from 'react';
|
||||
import { appAbilitiy } from './ability';
|
||||
|
||||
export const AbilityContext = React.createContext(appAbilitiy);
|
||||
export const Can = createContextualCan(AbilityContext.Consumer);
|
||||
export const useAppAbility = () => useAbility(AbilityContext);
|
@ -1,3 +0,0 @@
|
||||
export * from './ability';
|
||||
export * from './context';
|
||||
export * from './types';
|
@ -1,36 +0,0 @@
|
||||
import { MatchConditions, PureAbility } from '@casl/ability';
|
||||
import {
|
||||
Category,
|
||||
DataPointMetadata,
|
||||
Deployment,
|
||||
Device,
|
||||
LogRecord,
|
||||
Project,
|
||||
RegisteredModel,
|
||||
RegisteredProgram,
|
||||
TrainingJob,
|
||||
User,
|
||||
} from 'models';
|
||||
|
||||
type InferSubjectAs<T, V extends string> = T | V;
|
||||
export type AppSubjects =
|
||||
| InferSubjectAs<Category, 'Category'>
|
||||
| InferSubjectAs<DataPointMetadata, 'ImageMetadata'>
|
||||
| InferSubjectAs<Deployment, 'Deployment'>
|
||||
| InferSubjectAs<Device, 'Device'>
|
||||
| InferSubjectAs<Project, 'Project'>
|
||||
| InferSubjectAs<RegisteredModel, 'RegisteredModel'>
|
||||
| InferSubjectAs<RegisteredProgram, 'RegisteredProgram'>
|
||||
| InferSubjectAs<TrainingJob, 'TrainingJob'>
|
||||
| InferSubjectAs<User, 'User'>
|
||||
| InferSubjectAs<LogRecord, 'DeviceResult'>
|
||||
| 'all';
|
||||
|
||||
export type CRUD = 'create' | 'read' | 'update' | 'delete';
|
||||
export type ConfirmLabel = 'labeling:confirm';
|
||||
export type TerminateJob = 'training:terminate';
|
||||
|
||||
export type AppActions = CRUD | ConfirmLabel | TerminateJob | 'manage';
|
||||
export type Abilities = [AppActions, AppSubjects];
|
||||
|
||||
export type AppAbility = PureAbility<Abilities, MatchConditions>;
|
@ -3,7 +3,7 @@ import { t } from '@lingui/macro';
|
||||
import { Modal } from 'antd';
|
||||
import { ANT_PREFIX_CLASS } from 'consts';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
|
||||
function FormBlocker({ block }) {
|
||||
const [isBlocking, setIsBlocking] = useState(false);
|
||||
|
@ -1,11 +1,4 @@
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BarChartOutlined,
|
||||
FileSearchOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RotateRightOutlined,
|
||||
UsergroupAddOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { AppstoreOutlined, BarChartOutlined } from '@ant-design/icons';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Menu, MenuProps } from 'antd';
|
||||
import React from 'react';
|
||||
@ -41,10 +34,10 @@ function LeftMenu() {
|
||||
|
||||
const generalSubItems = [
|
||||
getItem(t`Dashboard`, '/dashboard', <AppstoreOutlined />),
|
||||
getItem(t`Inference`, '/inference', <RotateRightOutlined />),
|
||||
getItem(t`Reviews`, '/reviews', <FileSearchOutlined />),
|
||||
// getItem(t`Inference`, '/inference', <RotateRightOutlined />),
|
||||
// getItem(t`Reviews`, '/reviews', <FileSearchOutlined />),
|
||||
getItem(t`Reports`, '/reports', <BarChartOutlined />),
|
||||
getItem(t`Users`, '/users', <UsergroupAddOutlined />),
|
||||
// getItem(t`Users`, '/users', <UsergroupAddOutlined />),
|
||||
];
|
||||
|
||||
return (
|
||||
@ -62,7 +55,7 @@ function LeftMenu() {
|
||||
style={{ borderRight: 'none' }}
|
||||
items={generalSubItems}
|
||||
/>
|
||||
<div
|
||||
{/* <div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@ -74,7 +67,7 @@ function LeftMenu() {
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
<span>Help</span>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useGlobalState, useLocalStorage } from 'utils/hooks';
|
||||
|
||||
const UserHeader = () => {
|
||||
const { user, logOut } = useAuth();
|
||||
const { logOut } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [, setLocaleStorage] = useLocalStorage('sbt-locale', DEFAULT_LOCALE);
|
||||
const { data: locale, setData: setLocale } = useGlobalState(
|
||||
@ -19,10 +19,6 @@ const UserHeader = () => {
|
||||
DEFAULT_LOCALE,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userMenu = (
|
||||
<Menu className='sbt-header-menu' selectedKeys={[locale]}>
|
||||
<Menu.SubMenu
|
||||
|
@ -1,4 +1,4 @@
|
||||
export { default as ReportDetailTable } from './report-detail-table';
|
||||
export { default as ReportInformation } from './report-information';
|
||||
export { default as ReportMultiTypeChart } from './report-multitype-chart';
|
||||
export { default as ReportOverViewTable } from './report-overview-table';
|
||||
export * from './report-pie-chart';
|
||||
|
@ -1,319 +0,0 @@
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { Table } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface DataType {
|
||||
key: React.Key;
|
||||
subSidiaries: string;
|
||||
extractionDate: string | Date;
|
||||
snOrImeiNumber: number;
|
||||
invoiceNumber: number;
|
||||
totalImages: number;
|
||||
successfulNumber: number;
|
||||
successfulPercentage: number;
|
||||
badNumber: number;
|
||||
badPercentage: number;
|
||||
snImeiAAR: number; // AAR: Average Accuracy Rate
|
||||
purchaseDateAAR: number;
|
||||
retailerNameAAR: number;
|
||||
snImeiAPT: number; // APT: Average Processing Time
|
||||
invoiceAPT: number;
|
||||
snImeiTC: number; // TC: transaction count
|
||||
invoiceTC: number;
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Subsidiaries',
|
||||
dataIndex: 'subSidiaries',
|
||||
key: 'subSidiaries',
|
||||
fixed: 'left',
|
||||
width: '100px',
|
||||
},
|
||||
{
|
||||
title: 'OCR extraction date',
|
||||
dataIndex: 'extractionDate',
|
||||
key: 'extractionDate',
|
||||
fixed: 'left',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'OCR Images',
|
||||
children: [
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snOrImeiNumber',
|
||||
key: 'snOrImeiNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceNumber',
|
||||
key: 'invoiceNumber',
|
||||
width: '50px',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Total Images',
|
||||
dataIndex: 'totalImages',
|
||||
key: 'totalImages',
|
||||
fixed: 'left',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Image Quality',
|
||||
children: [
|
||||
{
|
||||
title: 'Successful',
|
||||
dataIndex: 'successfulNumber',
|
||||
key: 'successfulNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: '% Successful',
|
||||
dataIndex: 'successfulPercentage',
|
||||
key: 'successfulPercentage',
|
||||
width: '120px',
|
||||
},
|
||||
{
|
||||
title: 'Bad',
|
||||
dataIndex: 'badNumber',
|
||||
key: 'badNumber',
|
||||
width: '30px',
|
||||
},
|
||||
{
|
||||
title: '% Bad',
|
||||
dataIndex: 'badPercentage',
|
||||
key: 'badPercentage',
|
||||
width: '60px',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Average accuracy rate (%) \n(※ character-based)',
|
||||
children: [
|
||||
{
|
||||
title: 'IMEI / Serial no.',
|
||||
dataIndex: 'snImeiAAR',
|
||||
key: 'snImeiAAR',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Purchase date',
|
||||
dataIndex: 'purchaseDateAAR',
|
||||
key: 'purchaseDateAAR',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Retailer name',
|
||||
dataIndex: 'retailerNameAAR',
|
||||
key: 'retailerNameAAR',
|
||||
width: '130px',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Average Processing Time',
|
||||
children: [
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiAPT',
|
||||
key: 'snImeiAPT',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceAPT',
|
||||
key: 'invoiceAPT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Usage (Transaction Count)',
|
||||
children: [
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiTC',
|
||||
key: 'snImeiTC',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceTC',
|
||||
key: 'invoiceTC',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const data: DataType[] = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
data.push({
|
||||
key: i,
|
||||
subSidiaries: '',
|
||||
extractionDate: 'SubTotal (Jan)',
|
||||
snOrImeiNumber: null,
|
||||
invoiceNumber: null,
|
||||
totalImages: Math.floor(Math.random() * 100),
|
||||
successfulNumber: Math.floor(Math.random() * 100),
|
||||
successfulPercentage: Math.floor(Math.random() * 100),
|
||||
badNumber: Math.floor(Math.random() * 100),
|
||||
badPercentage: Math.floor(Math.random() * 100),
|
||||
snImeiAAR: null,
|
||||
purchaseDateAAR: null,
|
||||
retailerNameAAR: null,
|
||||
snImeiAPT: null,
|
||||
invoiceAPT: null,
|
||||
snImeiTC: Math.floor(Math.random() * 100),
|
||||
invoiceTC: Math.floor(Math.random() * 100),
|
||||
});
|
||||
}
|
||||
|
||||
const expandedRowRender = () => {
|
||||
const subData = [];
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
subData.push({
|
||||
key: i,
|
||||
subSidiaries: 'SESP',
|
||||
extractionDate: 'Jan',
|
||||
snOrImeiNumber: Math.floor(Math.random() * 100),
|
||||
invoiceNumber: Math.floor(Math.random() * 100),
|
||||
totalImages: Math.floor(Math.random() * 100),
|
||||
successfulNumber: Math.floor(Math.random() * 100),
|
||||
successfulPercentage: Math.floor(Math.random() * 100),
|
||||
badNumber: Math.floor(Math.random() * 100),
|
||||
badPercentage: Math.floor(Math.random() * 100),
|
||||
snImeiAAR: Math.floor(Math.random() * 100),
|
||||
purchaseDateAAR: Math.floor(Math.random() * 100),
|
||||
retailerNameAAR: Math.floor(Math.random() * 100),
|
||||
snImeiAPT: Math.floor(Math.random() * 100),
|
||||
invoiceAPT: Math.floor(Math.random() * 100),
|
||||
snImeiTC: Math.floor(Math.random() * 100),
|
||||
invoiceTC: Math.floor(Math.random() * 100),
|
||||
});
|
||||
}
|
||||
|
||||
const subColumns: TableColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Subsidiaries',
|
||||
dataIndex: 'subSidiaries',
|
||||
key: 'subSidiaries',
|
||||
fixed: 'left',
|
||||
width: '100px',
|
||||
},
|
||||
{
|
||||
title: 'OCR extraction date',
|
||||
dataIndex: 'extractionDate',
|
||||
key: 'extractionDate',
|
||||
fixed: 'left',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snOrImeiNumber',
|
||||
key: 'snOrImeiNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceNumber',
|
||||
key: 'invoiceNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: 'Total Images',
|
||||
dataIndex: 'totalImages',
|
||||
key: 'totalImages',
|
||||
fixed: 'left',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Successful',
|
||||
dataIndex: 'successfulNumber',
|
||||
key: 'successfulNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: '% Successful',
|
||||
dataIndex: 'successfulPercentage',
|
||||
key: 'successfulPercentage',
|
||||
width: '120px',
|
||||
},
|
||||
{
|
||||
title: 'Bad',
|
||||
dataIndex: 'badNumber',
|
||||
key: 'badNumber',
|
||||
width: '30px',
|
||||
},
|
||||
{
|
||||
title: '% Bad',
|
||||
dataIndex: 'badPercentage',
|
||||
key: 'badPercentage',
|
||||
width: '60px',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'IMEI / Serial no.',
|
||||
dataIndex: 'snImeiAAR',
|
||||
key: 'snImeiAAR',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Purchase date',
|
||||
dataIndex: 'purchaseDateAAR',
|
||||
key: 'purchaseDateAAR',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Retailer name',
|
||||
dataIndex: 'retailerNameAAR',
|
||||
key: 'retailerNameAAR',
|
||||
width: '130px',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiAPT',
|
||||
key: 'snImeiAPT',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceAPT',
|
||||
key: 'invoiceAPT',
|
||||
},
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiTC',
|
||||
key: 'snImeiTC',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceTC',
|
||||
key: 'invoiceTC',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
columns={subColumns}
|
||||
dataSource={subData}
|
||||
pagination={false}
|
||||
bordered
|
||||
// showHeader={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ReportDetailTable: React.FC = () => (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
bordered
|
||||
size='small'
|
||||
expandable={{ expandedRowRender, defaultExpandedRowKeys: [0, 1] }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ReportDetailTable;
|
465
cope2n-fe/src/components/report-detail/report-overview-table.tsx
Normal file
465
cope2n-fe/src/components/report-detail/report-overview-table.tsx
Normal file
@ -0,0 +1,465 @@
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { Table } from 'antd';
|
||||
import { useOverViewReport } from 'queries/report';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface DataType {
|
||||
key: React.Key;
|
||||
subSidiaries: string;
|
||||
extractionDate: string | Date;
|
||||
snOrImeiNumber: number;
|
||||
invoiceNumber: number;
|
||||
totalImages: number;
|
||||
successfulNumber: number;
|
||||
successfulPercentage: number;
|
||||
badNumber: number;
|
||||
badPercentage: number;
|
||||
snImeiAAR: number; // AAR: Average Accuracy Rate
|
||||
purchaseDateAAR: number;
|
||||
retailerNameAAR: number;
|
||||
snImeiAPT: number; // APT: Average Processing Time
|
||||
invoiceAPT: number;
|
||||
snImeiTC: number; // TC: transaction count
|
||||
invoiceTC: number;
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Subs',
|
||||
dataIndex: 'subSidiaries',
|
||||
key: 'subSidiaries',
|
||||
width: '100px',
|
||||
},
|
||||
{
|
||||
title: 'OCR extraction date',
|
||||
dataIndex: 'extractionDate',
|
||||
key: 'extractionDate',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'OCR Images',
|
||||
children: [
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snOrImeiNumber',
|
||||
key: 'snOrImeiNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceNumber',
|
||||
key: 'invoiceNumber',
|
||||
width: '50px',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Total Images',
|
||||
dataIndex: 'totalImages',
|
||||
key: 'totalImages',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Image Quality',
|
||||
children: [
|
||||
{
|
||||
title: 'Successful',
|
||||
dataIndex: 'successfulNumber',
|
||||
key: 'successfulNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: '% Successful',
|
||||
dataIndex: 'successfulPercentage',
|
||||
key: 'successfulPercentage',
|
||||
width: '120px',
|
||||
render: (_, record) => {
|
||||
return <span>{(record.successfulPercentage * 100).toFixed(2)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Bad',
|
||||
dataIndex: 'badNumber',
|
||||
key: 'badNumber',
|
||||
width: '30px',
|
||||
},
|
||||
{
|
||||
title: '% Bad',
|
||||
dataIndex: 'badPercentage',
|
||||
key: 'badPercentage',
|
||||
width: '60px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.badPercentage * 100 > 10;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.badPercentage * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Average accuracy rate (%) \n(※ character-based)',
|
||||
children: [
|
||||
{
|
||||
title: 'IMEI / Serial no.',
|
||||
dataIndex: 'snImeiAAR',
|
||||
key: 'snImeiAAR',
|
||||
width: '130px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.snImeiAAR * 100 < 98;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.snImeiAAR * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Purchase date',
|
||||
dataIndex: 'purchaseDateAAR',
|
||||
key: 'purchaseDateAAR',
|
||||
width: '130px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.purchaseDateAAR * 100 < 98;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.purchaseDateAAR * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Retailer name',
|
||||
dataIndex: 'retailerNameAAR',
|
||||
key: 'retailerNameAAR',
|
||||
width: '130px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.retailerNameAAR * 100 < 98;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.retailerNameAAR * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Average Processing Time',
|
||||
children: [
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiAPT',
|
||||
key: 'snImeiAPT',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.snImeiAPT > 2;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{record.snImeiAPT.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceAPT',
|
||||
key: 'invoiceAPT',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.invoiceAPT > 2;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{record.invoiceAPT.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Usage (Transaction Count)',
|
||||
children: [
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiTC',
|
||||
key: 'snImeiTC',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceTC',
|
||||
key: 'invoiceTC',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface ReportOverViewTableProps {
|
||||
pagination: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
setPagination: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
page: number;
|
||||
page_size: number;
|
||||
}>
|
||||
>;
|
||||
isLoading: boolean;
|
||||
data: any;
|
||||
}
|
||||
|
||||
const ReportOverViewTable: React.FC = () => {
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
});
|
||||
const { isLoading, data } = useOverViewReport({
|
||||
page: pagination.page,
|
||||
});
|
||||
const overviewDataResponse = data as any;
|
||||
const dataSubsRows = overviewDataResponse?.overview_data
|
||||
.map((item, index) => {
|
||||
if (item.subs.includes('+')) {
|
||||
return {
|
||||
key: index,
|
||||
subSidiaries: '',
|
||||
extractionDate: item.extraction_date,
|
||||
snOrImeiNumber: '',
|
||||
invoiceNumber: '',
|
||||
totalImages: item.total_images,
|
||||
successfulNumber: item.images_quality.successful,
|
||||
successfulPercentage: item.images_quality.successful_percent,
|
||||
badNumber: item.images_quality.bad,
|
||||
badPercentage: item.images_quality.bad_percent,
|
||||
snImeiAAR: item.average_accuracy_rate.imei,
|
||||
purchaseDateAAR: item.average_accuracy_rate.purchase_date,
|
||||
retailerNameAAR: item.average_accuracy_rate.retailer_name,
|
||||
snImeiAPT: item.average_processing_time.imei,
|
||||
invoiceAPT: item.average_processing_time.invoice,
|
||||
snImeiTC: item.usage.imei,
|
||||
invoiceTC: item.usage.invoice,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((item) => item);
|
||||
|
||||
const expandedRowRender = () => {
|
||||
const subData = overviewDataResponse?.overview_data
|
||||
.map((item, index) => {
|
||||
if (!item.subs.includes('+')) {
|
||||
return {
|
||||
key: index,
|
||||
subSidiaries: item.subs,
|
||||
extractionDate: item.extraction_date,
|
||||
snOrImeiNumber: item.num_imei,
|
||||
invoiceNumber: item.num_invoice,
|
||||
totalImages: item.total_images,
|
||||
successfulNumber: item.images_quality.successful,
|
||||
successfulPercentage: item.images_quality.successful_percent,
|
||||
badNumber: item.images_quality.bad,
|
||||
badPercentage: item.images_quality.bad_percent,
|
||||
snImeiAAR: item.average_accuracy_rate.imei,
|
||||
purchaseDateAAR: item.average_accuracy_rate.purchase_date,
|
||||
retailerNameAAR: item.average_accuracy_rate.retailer_name,
|
||||
snImeiAPT: item.average_processing_time.imei,
|
||||
invoiceAPT: item.average_processing_time.invoice,
|
||||
snImeiTC: item.usage.imei,
|
||||
invoiceTC: item.usage.invoice,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((item) => item);
|
||||
|
||||
const subColumns: TableColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Subs',
|
||||
dataIndex: 'subSidiaries',
|
||||
key: 'subSidiaries',
|
||||
width: '100px',
|
||||
},
|
||||
{
|
||||
title: 'OCR extraction date',
|
||||
dataIndex: 'extractionDate',
|
||||
key: 'extractionDate',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snOrImeiNumber',
|
||||
key: 'snOrImeiNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceNumber',
|
||||
key: 'invoiceNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: 'Total Images',
|
||||
dataIndex: 'totalImages',
|
||||
key: 'totalImages',
|
||||
width: '130px',
|
||||
},
|
||||
{
|
||||
title: 'Successful',
|
||||
dataIndex: 'successfulNumber',
|
||||
key: 'successfulNumber',
|
||||
width: '50px',
|
||||
},
|
||||
{
|
||||
title: '% Successful',
|
||||
dataIndex: 'successfulPercentage',
|
||||
key: 'successfulPercentage',
|
||||
width: '120px',
|
||||
render: (_, record) => {
|
||||
return <span>{(record.successfulPercentage * 100).toFixed(2)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Bad',
|
||||
dataIndex: 'badNumber',
|
||||
key: 'badNumber',
|
||||
width: '30px',
|
||||
},
|
||||
{
|
||||
title: '% Bad',
|
||||
dataIndex: 'badPercentage',
|
||||
key: 'badPercentage',
|
||||
width: '60px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.badPercentage * 100 > 10;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.badPercentage * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: 'IMEI / Serial no.',
|
||||
dataIndex: 'snImeiAAR',
|
||||
key: 'snImeiAAR',
|
||||
width: '130px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.snImeiAAR * 100 < 98;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.snImeiAAR * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Purchase date',
|
||||
dataIndex: 'purchaseDateAAR',
|
||||
key: 'purchaseDateAAR',
|
||||
width: '130px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.purchaseDateAAR * 100 < 98;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.purchaseDateAAR * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Retailer name',
|
||||
dataIndex: 'retailerNameAAR',
|
||||
key: 'retailerNameAAR',
|
||||
width: '130px',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.retailerNameAAR * 100 < 98;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{(record.retailerNameAAR * 100).toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiAPT',
|
||||
key: 'snImeiAPT',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.snImeiAPT > 2;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{record.snImeiAPT.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceAPT',
|
||||
key: 'invoiceAPT',
|
||||
render: (_, record) => {
|
||||
const isAbnormal = record.invoiceAPT > 2;
|
||||
return (
|
||||
<span style={{ color: isAbnormal ? 'red' : '' }}>
|
||||
{record.invoiceAPT.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'SN/IMEI',
|
||||
dataIndex: 'snImeiTC',
|
||||
key: 'snImeiTC',
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
dataIndex: 'invoiceTC',
|
||||
key: 'invoiceTC',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
columns={subColumns}
|
||||
dataSource={subData}
|
||||
pagination={false}
|
||||
bordered
|
||||
// showHeader={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
dataSource={dataSubsRows}
|
||||
bordered
|
||||
size='small'
|
||||
expandable={{ expandedRowRender, defaultExpandedRowKeys: [0, 1] }}
|
||||
scroll={{ x: 2000 }}
|
||||
pagination={{
|
||||
current: pagination.page,
|
||||
pageSize: pagination.page_size,
|
||||
total: overviewDataResponse?.page.count,
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination({
|
||||
page,
|
||||
page_size: pageSize || 10,
|
||||
});
|
||||
},
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportOverViewTable;
|
@ -13,7 +13,7 @@ const ReportTable: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [pagination, setPagination] = useState(() => ({
|
||||
page: reportData?.page.total_pages || 1,
|
||||
page: report_data?.page.total_pages || 1,
|
||||
page_size: 10,
|
||||
}));
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { AbilityContext, appAbilitiy } from 'acl';
|
||||
import Internationalization from 'components/internaltionalization';
|
||||
import { GlobalSpin } from 'components/spin';
|
||||
import { queryClient } from 'queries';
|
||||
@ -16,9 +15,9 @@ function App() {
|
||||
<Suspense fallback={<GlobalSpin />}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Internationalization>
|
||||
<AbilityContext.Provider value={appAbilitiy}>
|
||||
<AppRoutes />
|
||||
</AbilityContext.Provider>
|
||||
{/* <AbilityContext.Provider value={appAbilitiy}> */}
|
||||
<AppRoutes />
|
||||
{/* </AbilityContext.Provider> */}
|
||||
</Internationalization>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
|
@ -25,10 +25,10 @@ export const MainLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
? JSON.parse(localStorage.getItem('user-name'))
|
||||
: '';
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'user_management',
|
||||
label: <>User Management</>,
|
||||
},
|
||||
// {
|
||||
// key: 'user_management',
|
||||
// label: <>User Management</>,
|
||||
// },
|
||||
{
|
||||
key: 'logout',
|
||||
label: <>Logout</>,
|
||||
|
@ -8,7 +8,6 @@
|
||||
"Email format is not correct": "Email format is not correct",
|
||||
"English": "English",
|
||||
"Go to Report page": "Go to Report page",
|
||||
"Inference": "Inference",
|
||||
"Language": "Language",
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
@ -24,10 +23,9 @@
|
||||
"Please enter a valid domain": "Please enter a valid domain",
|
||||
"Please specify a password": "Please specify a password",
|
||||
"Please specify a username": "Please specify a username",
|
||||
"Report": "Report",
|
||||
"Report {0}...": "Report {0}...",
|
||||
"Reports": "Reports",
|
||||
"Retry": "Retry",
|
||||
"Reviews": "Reviews",
|
||||
"Service temporarily unavailable.": "Service temporarily unavailable.",
|
||||
"Something went wrong.": "Something went wrong.",
|
||||
"Sorry, something went wrong.": "Sorry, something went wrong.",
|
||||
@ -40,12 +38,12 @@
|
||||
"User log in successfully": "User log in successfully",
|
||||
"Username": "Username",
|
||||
"Username must not have more than {MAX_USERNAME_LENGTH} characters": "Username must not have more than {MAX_USERNAME_LENGTH} characters",
|
||||
"Users": "Users",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"You are not allowed to upload file bigger than {0}MB.": "You are not allowed to upload file bigger than {0}MB.",
|
||||
"You are not allowed to upload image bigger than {0}MB.": "You are not allowed to upload image bigger than {0}MB.",
|
||||
"You are only allowed to upload .zip file.": "You are only allowed to upload .zip file.",
|
||||
"You are only allowed to upload image or .zip files.": "You are only allowed to upload image or .zip files.",
|
||||
"You are only allowed to upload {0} file.": "You are only allowed to upload {0} file.",
|
||||
"You have unsaved changes!": "You have unsaved changes!"
|
||||
"You have unsaved changes!": "You have unsaved changes!",
|
||||
"Your current password has expired. Please change your password to continue.": "Your current password has expired. Please change your password to continue."
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
"Email format is not correct": "Định dạng email không hợp lệ",
|
||||
"English": "Tiếng Anh",
|
||||
"Go to Report page": "",
|
||||
"Inference": "",
|
||||
"Language": "Ngôn ngữ",
|
||||
"Login": "Đăng nhập",
|
||||
"Logout": "Đăng xuất",
|
||||
@ -24,10 +23,9 @@
|
||||
"Please enter a valid domain": "Vui lòng nhập một tên miền hợp lệ",
|
||||
"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": "",
|
||||
"Report {0}...": "",
|
||||
"Reports": "",
|
||||
"Retry": "Thử lại",
|
||||
"Reviews": "",
|
||||
"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",
|
||||
@ -40,12 +38,12 @@
|
||||
"User log in successfully": "Đăng nhập thành công",
|
||||
"Username": "Tên tài khoản",
|
||||
"Username must not have more than {MAX_USERNAME_LENGTH} characters": "Tên tài khoản không được chứa nhiều hơn {MAX_USERNAME_LENGTH} kí tự",
|
||||
"Users": "Người dùng",
|
||||
"Vietnamese": "Tiếng Việt",
|
||||
"You are not allowed to upload file bigger than {0}MB.": "Bạn không được phép tải lên tệp lớn hơn {0} MB.",
|
||||
"You are not allowed to upload image bigger than {0}MB.": "Bạn không được phép tải lên hình ảnh lớn hơn {0} MB.",
|
||||
"You are only allowed to upload .zip file.": "Bạn chỉ được phép tải lên tập tin .zip.",
|
||||
"You are only allowed to upload image or .zip files.": "Bạn chỉ được phép tải lên hình ảnh hoặc .zip.",
|
||||
"You are only allowed to upload {0} file.": "Bạn chỉ được phép tải lên {0}.",
|
||||
"You have unsaved changes!": "Bạn có những thay đổi chưa được lưu!"
|
||||
"You have unsaved changes!": "Bạn có những thay đổi chưa được lưu!",
|
||||
"Your current password has expired. Please change your password to continue.": ""
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ export type ReportListParams = {
|
||||
page_size?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
subsidiary?: string;
|
||||
};
|
||||
|
||||
export interface MakeReportResponse {
|
||||
@ -86,3 +87,44 @@ export interface Page {
|
||||
total_pages: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// Overview report type
|
||||
export interface OverViewDataResponse {
|
||||
overview_data: OverviewData[];
|
||||
page: Page;
|
||||
}
|
||||
|
||||
export interface OverviewData {
|
||||
subs: string;
|
||||
extraction_date: string;
|
||||
total_images: number;
|
||||
images_quality: ImagesQuality;
|
||||
average_accuracy_rate: AverageAccuracyRate;
|
||||
average_processing_time: AverageProcessingTime;
|
||||
usage: Usage;
|
||||
num_imei?: number;
|
||||
num_invoice?: number;
|
||||
}
|
||||
|
||||
export interface ImagesQuality {
|
||||
successful: number;
|
||||
successful_percent: number;
|
||||
bad: number;
|
||||
bad_percent: number;
|
||||
}
|
||||
|
||||
export interface AverageAccuracyRate {
|
||||
imei: number;
|
||||
purchase_date: number;
|
||||
retailer_name: number;
|
||||
}
|
||||
|
||||
export interface AverageProcessingTime {
|
||||
imei: number;
|
||||
invoice: number;
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
imei: number;
|
||||
invoice: number;
|
||||
}
|
||||
|
@ -1,35 +1,108 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button } from 'antd';
|
||||
import { SbtPageHeader } from 'components/page-header';
|
||||
import { ReportDetailTable } from 'components/report-detail';
|
||||
import { ReportOverViewTable } from 'components/report-detail';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface ReportFormValues {
|
||||
dateRange: [Dayjs, Dayjs];
|
||||
includeTest: string;
|
||||
subsidiary: string;
|
||||
}
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
// const [form] = Form.useForm<ReportFormValues>();
|
||||
// const [pagination, setPagination] = useState({
|
||||
// page: 1,
|
||||
// page_size: 10,
|
||||
// });
|
||||
// const [fromData, setFormData] = useState<{
|
||||
// start_date: string;
|
||||
// end_date: string;
|
||||
// subsidiary: string;
|
||||
// }>({
|
||||
// start_date: '',
|
||||
// end_date: '',
|
||||
// subsidiary: '',
|
||||
// });
|
||||
// const { isLoading, data } = useOverViewReport({
|
||||
// page: pagination.page,
|
||||
// page_size: pagination.page_size,
|
||||
// start_date: fromData.start_date,
|
||||
// end_date: fromData.end_date,
|
||||
// subsidiary: fromData.subsidiary,
|
||||
// });
|
||||
// const handleSubmit = (values: ReportFormValues) => {
|
||||
// console.log('check values >>>', values);
|
||||
// setFormData({
|
||||
// start_date: values.dateRange[0].format('YYYY-MM-DDTHH:mm:ssZ'),
|
||||
// end_date: values.dateRange[1].format('YYYY-MM-DDTHH:mm:ssZ'),
|
||||
// subsidiary: values.subsidiary,
|
||||
// });
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<SbtPageHeader
|
||||
title={t`Dashboard`}
|
||||
extra={
|
||||
<Button
|
||||
size='large'
|
||||
type='primary'
|
||||
onClick={() => navigate('/reports')}
|
||||
// icon={<PlusOutlined />}
|
||||
>
|
||||
{t`Go to Report page`}
|
||||
</Button>
|
||||
<>
|
||||
{/* <Button type='primary' icon={<DownloadOutlined />}>
|
||||
Download
|
||||
</Button> */}
|
||||
<Button type='primary' onClick={() => navigate('/reports')}>
|
||||
{t`Go to Report page`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{/* <Form
|
||||
form={form}
|
||||
style={{ display: 'flex', flexDirection: 'row', gap: 10 }}
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name='dateRange'
|
||||
label={t`Date`}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select a date range',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker.RangePicker />
|
||||
</Form.Item>
|
||||
|
||||
<ReportDetailTable />
|
||||
<Form.Item
|
||||
name='subsidiary'
|
||||
label={t`Subsidiary`}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select a subsidiary',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder='Select a subsidiary'
|
||||
style={{ width: 200 }}
|
||||
options={[
|
||||
{ value: 'all', label: 'ALL' },
|
||||
{ value: 'sesp', label: 'SESP' },
|
||||
{ value: 'seau', label: 'SEAU' },
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form> */}
|
||||
<ReportOverViewTable />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -252,7 +252,12 @@ const ReportDetail = () => {
|
||||
return (
|
||||
<>
|
||||
<SbtPageHeader
|
||||
title={t`Report`}
|
||||
title={
|
||||
<Tooltip
|
||||
title={id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>{t`Report ${id.slice(0, 16)}...`}</Tooltip>
|
||||
}
|
||||
extra={
|
||||
<Button size='large' type='primary' icon={<DownloadOutlined />}>
|
||||
{t`Download Report`}
|
||||
@ -279,8 +284,8 @@ const ReportDetail = () => {
|
||||
});
|
||||
},
|
||||
showSizeChanger: false,
|
||||
position: ['bottomLeft'],
|
||||
}}
|
||||
scroll={{ x: 2000 }}
|
||||
/>
|
||||
</ReportContainer>
|
||||
</>
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { ReportListParams } from 'models';
|
||||
import { getReportDetailList, getReportList, makeReport } from 'request/report';
|
||||
import {
|
||||
getOverViewReport,
|
||||
getReportDetailList,
|
||||
getReportList,
|
||||
makeReport,
|
||||
} from 'request/report';
|
||||
import {
|
||||
CustomUseMutationOptions,
|
||||
MakeReportParams,
|
||||
@ -36,3 +41,11 @@ export function useReportList(params?: ReportListParams, options?: any) {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useOverViewReport(params?: ReportListParams, options?: any) {
|
||||
return useQuery({
|
||||
queryKey: ['overview-report', params],
|
||||
queryFn: () => getOverViewReport(params),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
@ -1,43 +1,16 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
CustomUseMutationOptions,
|
||||
CustomUseQueryOptions,
|
||||
Pagination,
|
||||
User,
|
||||
} from 'models';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { CustomUseMutationOptions, User } from 'models';
|
||||
import {
|
||||
activateUser,
|
||||
changePassword,
|
||||
changePasswordForOther,
|
||||
deactivateUser,
|
||||
getSelf,
|
||||
getSession,
|
||||
getUsers,
|
||||
GetUsersParams,
|
||||
logIn,
|
||||
logOut,
|
||||
registerAccount,
|
||||
updateUser,
|
||||
} from 'request';
|
||||
|
||||
export function useSelf(options?: CustomUseQueryOptions<User>) {
|
||||
return useQuery({
|
||||
queryKey: ['user-self'],
|
||||
queryFn: getSelf,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUserSession(
|
||||
options?: CustomUseQueryOptions<Awaited<ReturnType<typeof getSession>>>,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['user-session'],
|
||||
queryFn: getSession,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogIn(options?: CustomUseMutationOptions) {
|
||||
return useMutation({
|
||||
mutationFn: logIn,
|
||||
@ -61,17 +34,6 @@ export function useRegister(options?: CustomUseMutationOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useUsers(
|
||||
params: GetUsersParams,
|
||||
options?: CustomUseQueryOptions<Pagination<User>>,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['users', params],
|
||||
queryFn: () => getUsers(params),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateUser(options?: CustomUseMutationOptions<User>) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
|
@ -1,5 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
import { notification } from 'antd';
|
||||
import axios, { AxiosError, HttpStatusCode } from 'axios';
|
||||
import { getLocale } from 'i18n';
|
||||
import { queryClient } from 'queries';
|
||||
import { ErrorData } from 'utils/error-handler';
|
||||
|
||||
const AXIOS_TIMEOUT_MS = 30 * 60 * 1000; // This config sastified long-live upload file request
|
||||
const EXPIRED_PASSWORD_SIGNAL = 'expired_password';
|
||||
@ -67,51 +72,51 @@ API.interceptors.request.use(
|
||||
);
|
||||
|
||||
// interceptor to handle unauthenticated (401) or server error (502)
|
||||
// API.interceptors.response.use(
|
||||
// function (value) {
|
||||
// return value;
|
||||
// },
|
||||
// function onError(error) {
|
||||
// if (
|
||||
// // user is currently not in login page and any API return unauthenticated status
|
||||
// error instanceof AxiosError &&
|
||||
// error.response?.status === HttpStatusCode.Unauthorized &&
|
||||
// window.location.pathname !== '/auth/login'
|
||||
// ) {
|
||||
// const isPasswordExpired = (
|
||||
// error as AxiosError<ErrorData>
|
||||
// ).response?.data.errors?.some((err) => {
|
||||
// return [err.code, err.message].includes(EXPIRED_PASSWORD_SIGNAL);
|
||||
// });
|
||||
// const isPasswordExpiredOnGlobalState = queryClient.getQueryData<boolean>([
|
||||
// 'isPasswordExpired',
|
||||
// ]);
|
||||
API.interceptors.response.use(
|
||||
function (value) {
|
||||
return value;
|
||||
},
|
||||
function onError(error) {
|
||||
if (
|
||||
// user is currently not in login page and any API return unauthenticated status
|
||||
error instanceof AxiosError &&
|
||||
error.response?.status === HttpStatusCode.Unauthorized &&
|
||||
window.location.pathname !== '/auth/login'
|
||||
) {
|
||||
const isPasswordExpired = (
|
||||
error as AxiosError<ErrorData>
|
||||
).response?.data.errors?.some((err) => {
|
||||
return [err.code, err.message].includes(EXPIRED_PASSWORD_SIGNAL);
|
||||
});
|
||||
const isPasswordExpiredOnGlobalState = queryClient.getQueryData<boolean>([
|
||||
'isPasswordExpired',
|
||||
]);
|
||||
|
||||
// if (isPasswordExpired) {
|
||||
// if (!isPasswordExpiredOnGlobalState) {
|
||||
// queryClient.setQueryData(['isPasswordExpired'], isPasswordExpired);
|
||||
// notification.warning({
|
||||
// key: EXPIRED_PASSWORD_SIGNAL,
|
||||
// message: i18n._(
|
||||
// t`Your current password has expired. Please change your password to continue.`,
|
||||
// ),
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// localStorage.removeItem('sbt-token');
|
||||
// queryClient.setQueryData(['isAuthenticated'], false);
|
||||
// }
|
||||
// }
|
||||
if (isPasswordExpired) {
|
||||
if (!isPasswordExpiredOnGlobalState) {
|
||||
queryClient.setQueryData(['isPasswordExpired'], isPasswordExpired);
|
||||
notification.warning({
|
||||
key: EXPIRED_PASSWORD_SIGNAL,
|
||||
message: i18n._(
|
||||
t`Your current password has expired. Please change your password to continue.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('sbt-token');
|
||||
queryClient.setQueryData(['isAuthenticated'], false);
|
||||
}
|
||||
}
|
||||
|
||||
// if (
|
||||
// error instanceof AxiosError &&
|
||||
// [HttpStatusCode.ServiceUnavailable, HttpStatusCode.BadGateway].includes(
|
||||
// error.response?.status,
|
||||
// )
|
||||
// ) {
|
||||
// queryClient.setQueryData(['imperativeError'], error);
|
||||
// }
|
||||
if (
|
||||
error instanceof AxiosError &&
|
||||
[HttpStatusCode.ServiceUnavailable, HttpStatusCode.BadGateway].includes(
|
||||
error.response?.status,
|
||||
)
|
||||
) {
|
||||
queryClient.setQueryData(['imperativeError'], error);
|
||||
}
|
||||
|
||||
// return Promise.reject(error);
|
||||
// },
|
||||
// );
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
MakeReportParams,
|
||||
MakeReportResponse,
|
||||
OverViewDataResponse,
|
||||
ReportDetailList,
|
||||
ReportDetailListParams,
|
||||
ReportListParams,
|
||||
@ -56,3 +57,20 @@ export async function getReportList(params?: ReportListParams) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOverViewReport(params?: ReportListParams) {
|
||||
try {
|
||||
const response = await API.get<OverViewDataResponse>('/ctel/overview/', {
|
||||
params: {
|
||||
page: params?.page,
|
||||
page_size: params?.page_size,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
subsidiary: params?.subsidiary,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE } from 'consts';
|
||||
import { Pagination, PaginationParams, User } from 'models';
|
||||
import { User } from 'models';
|
||||
import { API, PublicAPI } from './api';
|
||||
|
||||
export async function getSelf() {
|
||||
@ -49,30 +48,6 @@ export async function registerAccount(payload: RegisterPayload) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface GetUsersParams extends PaginationParams {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export async function getUsers({
|
||||
page = DEFAULT_PAGE,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
search,
|
||||
}: GetUsersParams) {
|
||||
const urlSearchParams = new URLSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
|
||||
if (search) {
|
||||
urlSearchParams.append('search', search);
|
||||
}
|
||||
|
||||
const response = await API.get<Pagination<User>>(
|
||||
`/users?${urlSearchParams.toString()}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
partialUser: Omit<Partial<User>, 'id'> & { id: User['id'] },
|
||||
) {
|
||||
|
@ -6,7 +6,7 @@ export function PublicRoute({ element }: { element: React.ReactNode }) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to='/reports' />;
|
||||
return <Navigate to='/dashboard' />;
|
||||
}
|
||||
|
||||
return <>{element}</>;
|
||||
|
Loading…
Reference in New Issue
Block a user