/** * RecordsPage — paginated lists + edit/delete for poo and location records (M2-T10). * Meter readings tab (read-only) per Modbus device dynamically appended (M5). * * - Poo list: GET /api/poo, query key ['poo', {limit, offset}], page size 100. * - Location list: GET /api/locations, query key ['locations', {limit, offset}], page size 100. * - Edit and delete use reusable components from src/records/. * - Delete has a二次确认 modal before calling DELETE. * - Pagination with Mantine Pagination; next/prev fetches per-page (no full-table pull). * - Meter tabs: one read-only tab per Modbus device; readings from /readings (last 24h, limit 1000). */ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Container, Title, Table, Pagination, Button, Group, Tabs, Text, Loader, Center, Alert, Stack, Badge, ScrollArea, } from '@mantine/core' import apiClient from '../api/client' import { EditPooModal, EditLocationModal, ConfirmDeleteModal } from '../records' import { useDeletePoo, useDeleteLocation } from '../records' import type { PooRecord, LocationRecord } from '../records' import { useDevices, useMetrics, useReadings } from '../energy/hooks' import type { ModbusDevice, MetricInfo } from '../energy/hooks' import { formatMetricValue } from '../energy/format' // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const PAGE_SIZE = 100 // --------------------------------------------------------------------------- // Poo list section // --------------------------------------------------------------------------- function PooList() { const [page, setPage] = useState(1) const offset = (page - 1) * PAGE_SIZE const { data, isLoading, isError } = useQuery({ queryKey: ['poo', { limit: PAGE_SIZE, offset }], queryFn: async () => { const res = await apiClient.GET('/api/poo', { params: { query: { limit: PAGE_SIZE, offset } }, }) return res.data }, }) const [editRecord, setEditRecord] = useState(null) const [deleteRecord, setDeleteRecord] = useState(null) const deleteMutation = useDeletePoo() async function handleDeleteConfirm() { if (!deleteRecord) return try { await deleteMutation.mutateAsync(deleteRecord.timestamp) setDeleteRecord(null) } catch { // Leave the modal open so the user can retry; error display is in the modal loading state. } } if (isLoading) { return (
) } if (isError || !data) { return ( Failed to load poo records. Please refresh. ) } const totalPages = data.items.length === PAGE_SIZE ? page + 1 : page return ( Page {page} · {data.items.length} record{data.items.length !== 1 ? 's' : ''} shown offset {offset} Timestamp Status Latitude Longitude Actions {data.items.length === 0 ? ( No records. ) : ( data.items.map((row) => ( {row.timestamp} {row.status} {row.latitude} {row.longitude} )) )}
{totalPages > 1 && ( )} {/* Edit modal */} {editRecord && ( setEditRecord(null)} onSaved={() => setEditRecord(null)} /> )} {/* Delete confirmation modal */} {deleteRecord && ( setDeleteRecord(null)} /> )}
) } // --------------------------------------------------------------------------- // Location list section // --------------------------------------------------------------------------- function LocationList() { const [page, setPage] = useState(1) const offset = (page - 1) * PAGE_SIZE const { data, isLoading, isError } = useQuery({ queryKey: ['locations', { limit: PAGE_SIZE, offset }], queryFn: async () => { const res = await apiClient.GET('/api/locations', { params: { query: { limit: PAGE_SIZE, offset } }, }) return res.data }, }) const [editRecord, setEditRecord] = useState(null) const [deleteRecord, setDeleteRecord] = useState(null) const deleteMutation = useDeleteLocation() async function handleDeleteConfirm() { if (!deleteRecord) return try { await deleteMutation.mutateAsync({ person: deleteRecord.person, datetime: deleteRecord.datetime, }) setDeleteRecord(null) } catch { // Leave modal open. } } if (isLoading) { return (
) } if (isError || !data) { return ( Failed to load location records. Please refresh. ) } const totalPages = data.items.length === PAGE_SIZE ? page + 1 : page return ( Page {page} · {data.items.length} record{data.items.length !== 1 ? 's' : ''} shown offset {offset} Person Datetime Latitude Longitude Altitude Actions {data.items.length === 0 ? ( No records. ) : ( data.items.map((row) => { const rowKey = `${row.person}__${row.datetime}` return ( {row.person} {row.datetime} {row.latitude} {row.longitude} {row.altitude ?? '—'} ) }) )}
{totalPages > 1 && ( )} {/* Edit modal */} {editRecord && ( setEditRecord(null)} onSaved={() => setEditRecord(null)} /> )} {/* Delete confirmation modal */} {deleteRecord && ( setDeleteRecord(null)} /> )}
) } // --------------------------------------------------------------------------- // MeterReadingsTable — read-only table for a single Modbus device's readings // --------------------------------------------------------------------------- /** How many ms of history to show per meter tab (last 24 h). */ const METER_SPAN_MS = 24 * 60 * 60 * 1000 /** Upper bound on readings fetched; never a full-table pull. */ const METER_READINGS_LIMIT = 1000 interface MeterReadingsTableProps { device: ModbusDevice } /** Format an ISO timestamp for display. */ function formatRecordedAt(iso: string): string { try { return new Date(iso).toLocaleString() } catch { return iso } } function MeterReadingsTable({ device }: MeterReadingsTableProps) { const metricsQuery = useMetrics(device.uuid) const readingsQuery = useReadings(device.uuid, { spanMs: METER_SPAN_MS, limit: METER_READINGS_LIMIT, }) const isLoading = metricsQuery.isLoading || readingsQuery.isLoading const isError = metricsQuery.isError || readingsQuery.isError const metrics: MetricInfo[] = metricsQuery.data?.metrics ?? [] const readings = readingsQuery.data?.items ?? [] const isTruncated = !isLoading && !isError && readings.length >= METER_READINGS_LIMIT if (isLoading) { return (
) } if (isError) { return ( Failed to load meter readings. Please refresh. ) } const colSpan = 1 + metrics.length // time column + one per metric return ( {isTruncated && ( Showing the most recent {METER_READINGS_LIMIT} readings (within the last 24h) )} Recorded At {metrics.map((m) => ( {m.label} {m.unit ? ( ({m.unit}) ) : null} ))} {readings.length === 0 ? ( No readings in the last 24 hours. ) : ( readings.map((row) => { const payload = row.payload as Record return ( {formatRecordedAt(row.recorded_at)} {metrics.map((m) => ( {formatMetricValue(payload[m.key], m)} ))} ) }) )}
) } // --------------------------------------------------------------------------- // RecordsPage — top-level // --------------------------------------------------------------------------- export function RecordsPage() { const devicesQuery = useDevices() const devices: ModbusDevice[] = devicesQuery.data?.items ?? [] return ( Records Poo Locations {devices.map((d) => ( {d.friendly_name} ))} {devices.map((d) => ( ))} ) }