Tau.Acuvim/portal/frontend/src/pages/AdminFleetDashboardPage.tsx
Diseri Pearson 94ace2df0e Portal frontend: apply Prettier formatting baseline
One-time `prettier --write` across the 22 source files not already touched
by the preceding tooling commit (b5ceedc). Pure formatting — no behaviour
change, no logic change. Reviewable as "all whitespace/reflow".

From here the lint-staged pre-commit hook keeps new and edited code
consistent automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:47:08 +02:00

169 lines
5.1 KiB
TypeScript

import { Button, Card, Col, Row, Space, Statistic, Table, Tag, Typography, Empty } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
ApartmentOutlined,
ThunderboltOutlined,
TeamOutlined,
CheckCircleOutlined,
DollarOutlined,
DownloadOutlined,
} from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { fetchFleetDashboard, type FleetCustomerSummary } from '../api/fleet';
import { downloadFleetDashboardXlsx } from '../api/dashboard';
const { Text } = Typography;
export function AdminFleetDashboardPage() {
const navigate = useNavigate();
const { data, isLoading } = useQuery({
queryKey: ['fleet-dashboard'],
queryFn: fetchFleetDashboard,
refetchInterval: 30_000,
});
const columns: ColumnsType<FleetCustomerSummary> = [
{ title: 'Code', dataIndex: 'code', key: 'code', render: (v) => <Text strong>{v}</Text> },
{ title: 'Name', dataIndex: 'name', key: 'name' },
{
title: 'Status',
dataIndex: 'isActive',
key: 'active',
render: (v: boolean) =>
v ? <Tag color="green">Active</Tag> : <Tag color="red">Disabled</Tag>,
},
{
title: 'Last push',
dataIndex: 'lastSeenAt',
key: 'last',
render: (v: string | null) => (v ? lagDescription(v) : <Text type="secondary">Never</Text>),
},
{ title: 'Sites', dataIndex: 'sites', key: 'sites' },
{ title: 'Devices', dataIndex: 'devices', key: 'devices' },
{
title: 'Today (rows)',
dataIndex: 'measurementsToday',
key: 'mt',
render: (n: number) => n.toLocaleString(),
},
{
title: 'Today (kWh imp.)',
dataIndex: 'kwhImportedToday',
key: 'kwh',
render: (v: number | null) => (v == null ? '—' : v.toFixed(2)),
},
{
title: 'Today (cost)',
dataIndex: 'costToday',
key: 'cost',
render: (v: number | null) =>
v == null ? <Text type="secondary"></Text> : <Text strong>{v.toFixed(2)}</Text>,
},
];
return (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={5}>
<Card>
<Statistic
title="Customers"
value={data?.totalCustomers ?? 0}
prefix={<TeamOutlined />}
loading={isLoading}
/>
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic
title="Active (last hr)"
value={data?.activeCustomers ?? 0}
suffix={data ? `/ ${data.totalCustomers}` : ''}
prefix={<CheckCircleOutlined />}
loading={isLoading}
/>
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic
title="Measurements today"
value={data?.totalMeasurementsToday ?? 0}
prefix={<ApartmentOutlined />}
loading={isLoading}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="kWh today"
value={data?.totalKwhImportedToday ?? 0}
precision={2}
prefix={<ThunderboltOutlined />}
loading={isLoading}
/>
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic
title="Cost today"
value={data?.totalCostToday ?? 0}
precision={2}
prefix={<DollarOutlined />}
valueStyle={{ color: '#3f8600' }}
loading={isLoading}
/>
</Card>
</Col>
</Row>
<Card
title="Customers"
extra={
<Space>
<Button
icon={<DownloadOutlined />}
onClick={() => downloadFleetDashboardXlsx()}
disabled={!data || data.customers.length === 0}
>
Export to Excel
</Button>
</Space>
}
>
{data && data.customers.length === 0 ? (
<Empty description="No customers registered yet. Go to Customers to register the first one." />
) : (
<Table<FleetCustomerSummary>
rowKey="id"
columns={columns}
dataSource={data?.customers ?? []}
loading={isLoading}
pagination={{ pageSize: 25 }}
onRow={(record) => ({
onClick: () => navigate(`/admin/customers/${record.id}`),
style: { cursor: 'pointer' },
})}
/>
)}
</Card>
</div>
);
}
function lagDescription(iso: string): React.ReactNode {
const ts = new Date(iso).getTime();
const ageMs = Date.now() - ts;
const ageMin = Math.floor(ageMs / 60_000);
if (ageMin < 1) return <Tag color="green">just now</Tag>;
if (ageMin < 5) return <Tag color="green">{ageMin}m ago</Tag>;
if (ageMin < 60) return <Tag color="orange">{ageMin}m ago</Tag>;
const ageHr = Math.floor(ageMin / 60);
if (ageHr < 24) return <Tag color="red">{ageHr}h ago</Tag>;
return <Tag color="red">{Math.floor(ageHr / 24)}d ago</Tag>;
}