Implement dashboard, report page

This commit is contained in:
Vu Khanh Du 2024-01-31 16:54:39 +07:00
parent 40578d8b6a
commit 8ce84a940d
26 changed files with 3162 additions and 3799 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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"
},

View File

@ -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);
}

View File

@ -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);

View File

@ -1,3 +0,0 @@
export * from './ability';
export * from './context';
export * from './types';

View File

@ -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>;

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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

View File

@ -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';

View File

@ -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;

View 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;

View File

@ -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,
}));

View File

@ -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}>
{/* <AbilityContext.Provider value={appAbilitiy}> */}
<AppRoutes />
</AbilityContext.Provider>
{/* </AbilityContext.Provider> */}
</Internationalization>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

View File

@ -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</>,

View File

@ -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."
}

View File

@ -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.": ""
}

View File

@ -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;
}

View File

@ -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 />}
>
<>
{/* <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 />
</>
);
};

View File

@ -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>
</>

View File

@ -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,
});
}

View File

@ -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({

View File

@ -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);
},
);

View File

@ -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);
}
}

View File

@ -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'] },
) {

View File

@ -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}</>;