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", "name": "sbt-ui",
"version": "0.1.0", "version": "0.1.0",
"scripts": { "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", "build": "npm run extract && npm run compile && tsc && vite build",
"serve": "vite preview", "serve": "vite preview",
"extract": "lingui extract --clean", "extract": "lingui extract --clean",
@ -31,16 +31,10 @@
"@ant-design/plots": "^1.2.3", "@ant-design/plots": "^1.2.3",
"@ant-design/pro-layout": "^7.10.3", "@ant-design/pro-layout": "^7.10.3",
"@babel/core": "^7.13.10", "@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", "@tanstack/react-query": "^4.20.4",
"antd": "^5.4.0", "antd": "^5.4.0",
"axios": "^1.2.2", "axios": "^1.2.2",
"chart.js": "^4.4.1", "chart.js": "^4.4.1",
"dagre": "^0.8.5",
"faker": "^6.6.6",
"history": "^5.3.0", "history": "^5.3.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
@ -48,7 +42,6 @@
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.6.1", "react-router-dom": "^6.6.1",
"reactflow": "^11.4.0",
"styled-components": "^5.3.6", "styled-components": "^5.3.6",
"uuid": "^9.0.0" "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 { Modal } from 'antd';
import { ANT_PREFIX_CLASS } from 'consts'; import { ANT_PREFIX_CLASS } from 'consts';
import React, { useEffect, useRef, useState } from 'react'; 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 }) { function FormBlocker({ block }) {
const [isBlocking, setIsBlocking] = useState(false); const [isBlocking, setIsBlocking] = useState(false);

View File

@ -1,11 +1,4 @@
import { import { AppstoreOutlined, BarChartOutlined } from '@ant-design/icons';
AppstoreOutlined,
BarChartOutlined,
FileSearchOutlined,
QuestionCircleOutlined,
RotateRightOutlined,
UsergroupAddOutlined,
} from '@ant-design/icons';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Menu, MenuProps } from 'antd'; import { Menu, MenuProps } from 'antd';
import React from 'react'; import React from 'react';
@ -41,10 +34,10 @@ function LeftMenu() {
const generalSubItems = [ const generalSubItems = [
getItem(t`Dashboard`, '/dashboard', <AppstoreOutlined />), getItem(t`Dashboard`, '/dashboard', <AppstoreOutlined />),
getItem(t`Inference`, '/inference', <RotateRightOutlined />), // getItem(t`Inference`, '/inference', <RotateRightOutlined />),
getItem(t`Reviews`, '/reviews', <FileSearchOutlined />), // getItem(t`Reviews`, '/reviews', <FileSearchOutlined />),
getItem(t`Reports`, '/reports', <BarChartOutlined />), getItem(t`Reports`, '/reports', <BarChartOutlined />),
getItem(t`Users`, '/users', <UsergroupAddOutlined />), // getItem(t`Users`, '/users', <UsergroupAddOutlined />),
]; ];
return ( return (
@ -62,7 +55,7 @@ function LeftMenu() {
style={{ borderRight: 'none' }} style={{ borderRight: 'none' }}
items={generalSubItems} items={generalSubItems}
/> />
<div {/* <div
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -74,7 +67,7 @@ function LeftMenu() {
> >
<QuestionCircleOutlined /> <QuestionCircleOutlined />
<span>Help</span> <span>Help</span>
</div> </div> */}
</div> </div>
); );
} }

View File

@ -11,7 +11,7 @@ import { useNavigate } from 'react-router-dom';
import { useGlobalState, useLocalStorage } from 'utils/hooks'; import { useGlobalState, useLocalStorage } from 'utils/hooks';
const UserHeader = () => { const UserHeader = () => {
const { user, logOut } = useAuth(); const { logOut } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [, setLocaleStorage] = useLocalStorage('sbt-locale', DEFAULT_LOCALE); const [, setLocaleStorage] = useLocalStorage('sbt-locale', DEFAULT_LOCALE);
const { data: locale, setData: setLocale } = useGlobalState( const { data: locale, setData: setLocale } = useGlobalState(
@ -19,10 +19,6 @@ const UserHeader = () => {
DEFAULT_LOCALE, DEFAULT_LOCALE,
); );
if (!user) {
return null;
}
const userMenu = ( const userMenu = (
<Menu className='sbt-header-menu' selectedKeys={[locale]}> <Menu className='sbt-header-menu' selectedKeys={[locale]}>
<Menu.SubMenu <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 ReportInformation } from './report-information';
export { default as ReportMultiTypeChart } from './report-multitype-chart'; export { default as ReportMultiTypeChart } from './report-multitype-chart';
export { default as ReportOverViewTable } from './report-overview-table';
export * from './report-pie-chart'; 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 navigate = useNavigate();
const [pagination, setPagination] = useState(() => ({ const [pagination, setPagination] = useState(() => ({
page: reportData?.page.total_pages || 1, page: report_data?.page.total_pages || 1,
page_size: 10, page_size: 10,
})); }));

View File

@ -1,6 +1,5 @@
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { AbilityContext, appAbilitiy } from 'acl';
import Internationalization from 'components/internaltionalization'; import Internationalization from 'components/internaltionalization';
import { GlobalSpin } from 'components/spin'; import { GlobalSpin } from 'components/spin';
import { queryClient } from 'queries'; import { queryClient } from 'queries';
@ -16,9 +15,9 @@ function App() {
<Suspense fallback={<GlobalSpin />}> <Suspense fallback={<GlobalSpin />}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Internationalization> <Internationalization>
<AbilityContext.Provider value={appAbilitiy}> {/* <AbilityContext.Provider value={appAbilitiy}> */}
<AppRoutes /> <AppRoutes />
</AbilityContext.Provider> {/* </AbilityContext.Provider> */}
</Internationalization> </Internationalization>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider> </QueryClientProvider>

View File

@ -25,10 +25,10 @@ export const MainLayout = ({ children }: { children: React.ReactNode }) => {
? JSON.parse(localStorage.getItem('user-name')) ? JSON.parse(localStorage.getItem('user-name'))
: ''; : '';
const items: MenuProps['items'] = [ const items: MenuProps['items'] = [
{ // {
key: 'user_management', // key: 'user_management',
label: <>User Management</>, // label: <>User Management</>,
}, // },
{ {
key: 'logout', key: 'logout',
label: <>Logout</>, label: <>Logout</>,

View File

@ -8,7 +8,6 @@
"Email format is not correct": "Email format is not correct", "Email format is not correct": "Email format is not correct",
"English": "English", "English": "English",
"Go to Report page": "Go to Report page", "Go to Report page": "Go to Report page",
"Inference": "Inference",
"Language": "Language", "Language": "Language",
"Login": "Login", "Login": "Login",
"Logout": "Logout", "Logout": "Logout",
@ -24,10 +23,9 @@
"Please enter a valid domain": "Please enter a valid domain", "Please enter a valid domain": "Please enter a valid domain",
"Please specify a password": "Please specify a password", "Please specify a password": "Please specify a password",
"Please specify a username": "Please specify a username", "Please specify a username": "Please specify a username",
"Report": "Report", "Report {0}...": "Report {0}...",
"Reports": "Reports", "Reports": "Reports",
"Retry": "Retry", "Retry": "Retry",
"Reviews": "Reviews",
"Service temporarily unavailable.": "Service temporarily unavailable.", "Service temporarily unavailable.": "Service temporarily unavailable.",
"Something went wrong.": "Something went wrong.", "Something went wrong.": "Something went wrong.",
"Sorry, something went wrong.": "Sorry, something went wrong.", "Sorry, something went wrong.": "Sorry, something went wrong.",
@ -40,12 +38,12 @@
"User log in successfully": "User log in successfully", "User log in successfully": "User log in successfully",
"Username": "Username", "Username": "Username",
"Username must not have more than {MAX_USERNAME_LENGTH} characters": "Username must not have more than {MAX_USERNAME_LENGTH} characters", "Username must not have more than {MAX_USERNAME_LENGTH} characters": "Username must not have more than {MAX_USERNAME_LENGTH} characters",
"Users": "Users",
"Vietnamese": "Vietnamese", "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 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 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 .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 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 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ệ", "Email format is not correct": "Định dạng email không hợp lệ",
"English": "Tiếng Anh", "English": "Tiếng Anh",
"Go to Report page": "", "Go to Report page": "",
"Inference": "",
"Language": "Ngôn ngữ", "Language": "Ngôn ngữ",
"Login": "Đăng nhập", "Login": "Đăng nhập",
"Logout": "Đăng xuất", "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 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 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", "Please specify a username": "Vui lòng nhập một tên tài khoản",
"Report": "", "Report {0}...": "",
"Reports": "", "Reports": "",
"Retry": "Thử lại", "Retry": "Thử lại",
"Reviews": "",
"Service temporarily unavailable.": "Dịch vụ máy chủ hiện tại không sẵn sàng.", "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", "Something went wrong.": "Có lỗi xảy ra",
"Sorry, something went wrong.": "Hệ thống gặp lỗi", "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", "User log in successfully": "Đăng nhập thành công",
"Username": "Tên tài khoản", "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ự", "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", "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 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 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 .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 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 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; page_size?: number;
start_date?: string; start_date?: string;
end_date?: string; end_date?: string;
subsidiary?: string;
}; };
export interface MakeReportResponse { export interface MakeReportResponse {
@ -86,3 +87,44 @@ export interface Page {
total_pages: number; total_pages: number;
count: 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 { t } from '@lingui/macro';
import { Button } from 'antd'; import { Button } from 'antd';
import { SbtPageHeader } from 'components/page-header'; import { SbtPageHeader } from 'components/page-header';
import { ReportDetailTable } from 'components/report-detail'; import { ReportOverViewTable } from 'components/report-detail';
import { Dayjs } from 'dayjs'; import { Dayjs } from 'dayjs';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
export interface ReportFormValues { export interface ReportFormValues {
dateRange: [Dayjs, Dayjs]; dateRange: [Dayjs, Dayjs];
includeTest: string; subsidiary: string;
} }
const Dashboard = () => { const Dashboard = () => {
const navigate = useNavigate(); 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 ( return (
<> <>
<SbtPageHeader <SbtPageHeader
title={t`Dashboard`} title={t`Dashboard`}
extra={ extra={
<Button <>
size='large' {/* <Button type='primary' icon={<DownloadOutlined />}>
type='primary' Download
onClick={() => navigate('/reports')} </Button> */}
// icon={<PlusOutlined />} <Button type='primary' onClick={() => navigate('/reports')}>
> {t`Go to Report page`}
{t`Go to Report page`} </Button>
</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 ( return (
<> <>
<SbtPageHeader <SbtPageHeader
title={t`Report`} title={
<Tooltip
title={id}
style={{ cursor: 'pointer' }}
>{t`Report ${id.slice(0, 16)}...`}</Tooltip>
}
extra={ extra={
<Button size='large' type='primary' icon={<DownloadOutlined />}> <Button size='large' type='primary' icon={<DownloadOutlined />}>
{t`Download Report`} {t`Download Report`}
@ -279,8 +284,8 @@ const ReportDetail = () => {
}); });
}, },
showSizeChanger: false, showSizeChanger: false,
position: ['bottomLeft'],
}} }}
scroll={{ x: 2000 }}
/> />
</ReportContainer> </ReportContainer>
</> </>

View File

@ -1,6 +1,11 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ReportListParams } from 'models'; import { ReportListParams } from 'models';
import { getReportDetailList, getReportList, makeReport } from 'request/report'; import {
getOverViewReport,
getReportDetailList,
getReportList,
makeReport,
} from 'request/report';
import { import {
CustomUseMutationOptions, CustomUseMutationOptions,
MakeReportParams, MakeReportParams,
@ -36,3 +41,11 @@ export function useReportList(params?: ReportListParams, options?: any) {
...options, ...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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { import { CustomUseMutationOptions, User } from 'models';
CustomUseMutationOptions,
CustomUseQueryOptions,
Pagination,
User,
} from 'models';
import { import {
activateUser, activateUser,
changePassword, changePassword,
changePasswordForOther, changePasswordForOther,
deactivateUser, deactivateUser,
getSelf,
getSession,
getUsers,
GetUsersParams,
logIn, logIn,
logOut, logOut,
registerAccount, registerAccount,
updateUser, updateUser,
} from 'request'; } 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) { export function useLogIn(options?: CustomUseMutationOptions) {
return useMutation({ return useMutation({
mutationFn: logIn, 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>) { export function useUpdateUser(options?: CustomUseMutationOptions<User>) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ 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 { 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 AXIOS_TIMEOUT_MS = 30 * 60 * 1000; // This config sastified long-live upload file request
const EXPIRED_PASSWORD_SIGNAL = 'expired_password'; const EXPIRED_PASSWORD_SIGNAL = 'expired_password';
@ -67,51 +72,51 @@ API.interceptors.request.use(
); );
// interceptor to handle unauthenticated (401) or server error (502) // interceptor to handle unauthenticated (401) or server error (502)
// API.interceptors.response.use( API.interceptors.response.use(
// function (value) { function (value) {
// return value; return value;
// }, },
// function onError(error) { function onError(error) {
// if ( if (
// // user is currently not in login page and any API return unauthenticated status // user is currently not in login page and any API return unauthenticated status
// error instanceof AxiosError && error instanceof AxiosError &&
// error.response?.status === HttpStatusCode.Unauthorized && error.response?.status === HttpStatusCode.Unauthorized &&
// window.location.pathname !== '/auth/login' window.location.pathname !== '/auth/login'
// ) { ) {
// const isPasswordExpired = ( const isPasswordExpired = (
// error as AxiosError<ErrorData> error as AxiosError<ErrorData>
// ).response?.data.errors?.some((err) => { ).response?.data.errors?.some((err) => {
// return [err.code, err.message].includes(EXPIRED_PASSWORD_SIGNAL); return [err.code, err.message].includes(EXPIRED_PASSWORD_SIGNAL);
// }); });
// const isPasswordExpiredOnGlobalState = queryClient.getQueryData<boolean>([ const isPasswordExpiredOnGlobalState = queryClient.getQueryData<boolean>([
// 'isPasswordExpired', 'isPasswordExpired',
// ]); ]);
// if (isPasswordExpired) { if (isPasswordExpired) {
// if (!isPasswordExpiredOnGlobalState) { if (!isPasswordExpiredOnGlobalState) {
// queryClient.setQueryData(['isPasswordExpired'], isPasswordExpired); queryClient.setQueryData(['isPasswordExpired'], isPasswordExpired);
// notification.warning({ notification.warning({
// key: EXPIRED_PASSWORD_SIGNAL, key: EXPIRED_PASSWORD_SIGNAL,
// message: i18n._( message: i18n._(
// t`Your current password has expired. Please change your password to continue.`, t`Your current password has expired. Please change your password to continue.`,
// ), ),
// }); });
// } }
// } else { } else {
// localStorage.removeItem('sbt-token'); localStorage.removeItem('sbt-token');
// queryClient.setQueryData(['isAuthenticated'], false); queryClient.setQueryData(['isAuthenticated'], false);
// } }
// } }
// if ( if (
// error instanceof AxiosError && error instanceof AxiosError &&
// [HttpStatusCode.ServiceUnavailable, HttpStatusCode.BadGateway].includes( [HttpStatusCode.ServiceUnavailable, HttpStatusCode.BadGateway].includes(
// error.response?.status, error.response?.status,
// ) )
// ) { ) {
// queryClient.setQueryData(['imperativeError'], error); queryClient.setQueryData(['imperativeError'], error);
// } }
// return Promise.reject(error); return Promise.reject(error);
// }, },
// ); );

View File

@ -1,6 +1,7 @@
import { import {
MakeReportParams, MakeReportParams,
MakeReportResponse, MakeReportResponse,
OverViewDataResponse,
ReportDetailList, ReportDetailList,
ReportDetailListParams, ReportDetailListParams,
ReportListParams, ReportListParams,
@ -56,3 +57,20 @@ export async function getReportList(params?: ReportListParams) {
console.log(error); 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 { User } from 'models';
import { Pagination, PaginationParams, User } from 'models';
import { API, PublicAPI } from './api'; import { API, PublicAPI } from './api';
export async function getSelf() { export async function getSelf() {
@ -49,30 +48,6 @@ export async function registerAccount(payload: RegisterPayload) {
return response.data; 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( export async function updateUser(
partialUser: Omit<Partial<User>, 'id'> & { id: User['id'] }, partialUser: Omit<Partial<User>, 'id'> & { id: User['id'] },
) { ) {

View File

@ -6,7 +6,7 @@ export function PublicRoute({ element }: { element: React.ReactNode }) {
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
if (isAuthenticated) { if (isAuthenticated) {
return <Navigate to='/reports' />; return <Navigate to='/dashboard' />;
} }
return <>{element}</>; return <>{element}</>;