import { useMemo, useState } from 'react'; import { Alert, Button, Card, DatePicker, InputNumber, Select, Space, Table, Tag, Typography, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { DownloadOutlined, ReloadOutlined } from '@ant-design/icons'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; import dayjs, { type Dayjs } from 'dayjs'; import { fetchRawMeasurements, listAllDevices, downloadRawMeasurementsExport, type RawMeasurementRow, } from '../api/measurements'; const { Text } = Typography; const { RangePicker } = DatePicker; // Hard cap on the in-page preview to keep the table fast. Excel export has its // own cap (250 000 backend-side) since users actually want the whole dataset. const PREVIEW_LIMIT_OPTIONS = [100, 200, 500, 1000]; export function MeasurementsPage() { const [range, setRange] = useState<[Dayjs, Dayjs]>(() => [ dayjs().subtract(1, 'day').startOf('day'), dayjs().add(1, 'day').startOf('day'), ]); const [selectedDeviceIds, setSelectedDeviceIds] = useState([]); const [limit, setLimit] = useState(200); const [offset, setOffset] = useState(0); const [exportRowCap, setExportRowCap] = useState(100_000); const fromIso = range[0].toISOString(); const toIso = range[1].toISOString(); const { data: devices = [], isLoading: loadingDevices } = useQuery({ queryKey: ['all-devices'], queryFn: listAllDevices, }); const { data, isLoading, isFetching, error, refetch } = useQuery({ queryKey: [ 'raw-measurements', fromIso, toIso, selectedDeviceIds.sort().join(','), limit, offset, ], queryFn: () => fetchRawMeasurements({ fromUtc: fromIso, toUtc: toIso, deviceIds: selectedDeviceIds.length > 0 ? selectedDeviceIds : undefined, limit, offset, }), placeholderData: keepPreviousData, }); const deviceOptions = useMemo( () => devices.map((d) => ({ value: d.id, label: `${d.name} — ${d.siteName}`, disabled: !d.isActive, })), [devices], ); const columns: ColumnsType = [ { title: 'Time (UTC)', dataIndex: 'time', key: 't', width: 180, render: (v: string) => new Date(v).toISOString().replace('T', ' ').slice(0, 19), }, { title: 'Device', dataIndex: 'deviceName', key: 'd', render: (v: string) => {v}, }, { title: 'Site', dataIndex: 'siteName', key: 's', render: (v: string | null) => v ?? , }, { title: 'Active kW', dataIndex: 'activePowerKw', key: 'kw', align: 'right' as const, render: (v: number) => v.toFixed(3), }, { title: 'kWh imported (cum.)', dataIndex: 'energyImportedKwh', key: 'imp', align: 'right' as const, render: (v: number | null) => (v == null ? '—' : v.toFixed(2)), }, { title: 'kWh exported (cum.)', dataIndex: 'energyExportedKwh', key: 'exp', align: 'right' as const, render: (v: number | null) => (v == null ? '—' : v.toFixed(2)), }, { title: 'PF', dataIndex: 'powerFactor', key: 'pf', align: 'right' as const, render: (v: number | null) => (v == null ? '—' : v.toFixed(3)), }, { title: 'V', dataIndex: 'voltageV', key: 'v', align: 'right' as const, render: (v: number | null) => (v == null ? '—' : v.toFixed(1)), }, { title: 'Hz', dataIndex: 'frequencyHz', key: 'hz', align: 'right' as const, render: (v: number | null) => (v == null ? '—' : v.toFixed(2)), }, ]; const totalCount = data?.totalCount ?? 0; const showingFrom = totalCount === 0 ? 0 : offset + 1; const showingTo = Math.min(offset + limit, totalCount); const canPrev = offset > 0; const canNext = offset + limit < totalCount; return ( Range (UTC): v && v[0] && v[1] && (setRange([v[0].startOf('day'), v[1].endOf('day')]), setOffset(0)) } ranges={{ Today: [dayjs().startOf('day'), dayjs().add(1, 'day').startOf('day')], Yesterday: [dayjs().subtract(1, 'day').startOf('day'), dayjs().startOf('day')], 'Last 7d': [ dayjs().subtract(7, 'day').startOf('day'), dayjs().add(1, 'day').startOf('day'), ], 'Last 30d': [ dayjs().subtract(30, 'day').startOf('day'), dayjs().add(1, 'day').startOf('day'), ], 'This month': [dayjs().startOf('month'), dayjs().add(1, 'day').startOf('day')], 'All time': [dayjs('2020-01-01'), dayjs().add(1, 'day').startOf('day')], }} /> Meters: { setLimit(v); setOffset(0); }} options={PREVIEW_LIMIT_OPTIONS.map((n) => ({ value: n, label: n.toString() }))} style={{ width: 96 }} /> · Export row cap: setExportRowCap(typeof v === 'number' ? v : 100_000)} style={{ width: 130 }} /> max 250 000 — shrink the range if you exceed this {error && ( )} Measurements {totalCount.toLocaleString()} rows match {selectedDeviceIds.length > 0 && ( {selectedDeviceIds.length} meter{selectedDeviceIds.length === 1 ? '' : 's'} selected )} } extra={ } > rowKey={(r) => `${r.time}-${r.deviceName}`} size="small" columns={columns} dataSource={data?.rows ?? []} loading={isLoading} pagination={false} scroll={{ x: 'max-content' }} /> {totalCount === 0 ? 'No measurements in this window.' : `Showing ${showingFrom.toLocaleString()}–${showingTo.toLocaleString()} of ${totalCount.toLocaleString()}`} ); }