From 94ace2df0ee7d65efc116afad63b2a6f121579c7 Mon Sep 17 00:00:00 2001 From: Diseri Pearson Date: Tue, 19 May 2026 23:47:08 +0200 Subject: [PATCH] Portal frontend: apply Prettier formatting baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- portal/frontend/src/api/auth.ts | 5 +- portal/frontend/src/api/dashboard.ts | 5 +- portal/frontend/src/api/fleet.ts | 7 +- portal/frontend/src/api/measurements.ts | 6 +- portal/frontend/src/api/rates.ts | 9 +- .../frontend/src/components/RequireAuth.tsx | 4 +- .../frontend/src/components/RequireRole.tsx | 6 +- .../customers/CustomerFormModal.tsx | 16 +- .../customers/TokenShownOnceModal.tsx | 11 +- .../src/components/layout/AppLayout.tsx | 29 +- .../src/components/settings/BrandingForm.tsx | 52 ++- .../settings/ConfigOverviewCard.tsx | 191 +++++++--- .../components/settings/GrafanaInfoCard.tsx | 23 +- .../settings/rates/MunicipalityList.tsx | 112 +++++- .../settings/rates/PeriodEditor.tsx | 5 +- .../components/users/CustomerAccessModal.tsx | 6 +- .../src/components/users/UserFormDrawer.tsx | 21 +- .../src/pages/AdminCustomerDetailPage.tsx | 360 +++++++++++++++--- .../src/pages/AdminFleetDashboardPage.tsx | 45 ++- portal/frontend/src/pages/AdminSitesPage.tsx | 98 ++++- portal/frontend/src/pages/DashboardPage.tsx | 148 +++++-- .../frontend/src/pages/MeasurementsPage.tsx | 188 ++++++--- 22 files changed, 1056 insertions(+), 291 deletions(-) diff --git a/portal/frontend/src/api/auth.ts b/portal/frontend/src/api/auth.ts index aab8e91..7ec3329 100644 --- a/portal/frontend/src/api/auth.ts +++ b/portal/frontend/src/api/auth.ts @@ -30,7 +30,10 @@ export async function updateMyProfile(displayName: string): Promise return data; } -export async function changeMyPassword(currentPassword: string, newPassword: string): Promise { +export async function changeMyPassword( + currentPassword: string, + newPassword: string, +): Promise { await api.post('/auth/me/change-password', { currentPassword, newPassword }); } diff --git a/portal/frontend/src/api/dashboard.ts b/portal/frontend/src/api/dashboard.ts index 3886982..5b0b975 100644 --- a/portal/frontend/src/api/dashboard.ts +++ b/portal/frontend/src/api/dashboard.ts @@ -24,7 +24,10 @@ export interface DashboardSummary { chart: DashboardChartPoint[]; } -export async function fetchDashboardSummary(fromUtc: string, toUtc: string): Promise { +export async function fetchDashboardSummary( + fromUtc: string, + toUtc: string, +): Promise { const { data } = await api.get('/dashboard/summary', { params: { from: fromUtc, to: toUtc }, }); diff --git a/portal/frontend/src/api/fleet.ts b/portal/frontend/src/api/fleet.ts index 3f399c9..8d5b755 100644 --- a/portal/frontend/src/api/fleet.ts +++ b/portal/frontend/src/api/fleet.ts @@ -151,9 +151,8 @@ export async function fetchFleetCustomerCost( fromUtc: string, toUtc: string, ): Promise { - const { data } = await api.get( - `/fleet/customers/${id}/cost`, - { params: { from: fromUtc, to: toUtc } }, - ); + const { data } = await api.get(`/fleet/customers/${id}/cost`, { + params: { from: fromUtc, to: toUtc }, + }); return data; } diff --git a/portal/frontend/src/api/measurements.ts b/portal/frontend/src/api/measurements.ts index 9efcfbe..620ff97 100644 --- a/portal/frontend/src/api/measurements.ts +++ b/portal/frontend/src/api/measurements.ts @@ -46,7 +46,8 @@ export async function fetchRawMeasurements(params: { params: { from: params.fromUtc, to: params.toUtc, - deviceIds: params.deviceIds && params.deviceIds.length > 0 ? params.deviceIds.join(',') : undefined, + deviceIds: + params.deviceIds && params.deviceIds.length > 0 ? params.deviceIds.join(',') : undefined, limit: params.limit, offset: params.offset, }, @@ -64,7 +65,8 @@ export function downloadRawMeasurementsExport(params: { from: params.fromUtc, to: params.toUtc, }); - if (params.deviceIds && params.deviceIds.length > 0) q.set('deviceIds', params.deviceIds.join(',')); + if (params.deviceIds && params.deviceIds.length > 0) + q.set('deviceIds', params.deviceIds.join(',')); if (params.rowCap) q.set('rowCap', String(params.rowCap)); window.location.href = `/api/measurements/raw/export.xlsx?${q.toString()}`; } diff --git a/portal/frontend/src/api/rates.ts b/portal/frontend/src/api/rates.ts index a5342d3..884a616 100644 --- a/portal/frontend/src/api/rates.ts +++ b/portal/frontend/src/api/rates.ts @@ -77,7 +77,9 @@ export async function deleteMunicipality(id: number): Promise { } export async function listTariffs(municipalityId: number): Promise { - const { data } = await api.get(`/rates/municipalities/${municipalityId}/tariffs`); + const { data } = await api.get( + `/rates/municipalities/${municipalityId}/tariffs`, + ); return data; } @@ -86,7 +88,10 @@ export async function getTariff(tariffId: number): Promise { return data; } -export async function createTariff(municipalityId: number, payload: UpsertTariff): Promise { +export async function createTariff( + municipalityId: number, + payload: UpsertTariff, +): Promise { const { data } = await api.post( `/admin/rates/municipalities/${municipalityId}/tariffs`, payload, diff --git a/portal/frontend/src/components/RequireAuth.tsx b/portal/frontend/src/components/RequireAuth.tsx index ed0ae31..406e8fe 100644 --- a/portal/frontend/src/components/RequireAuth.tsx +++ b/portal/frontend/src/components/RequireAuth.tsx @@ -9,7 +9,9 @@ export function RequireAuth({ children }: { children: ReactNode }) { if (loading) { return ( -
+
); diff --git a/portal/frontend/src/components/RequireRole.tsx b/portal/frontend/src/components/RequireRole.tsx index d610707..688c41c 100644 --- a/portal/frontend/src/components/RequireRole.tsx +++ b/portal/frontend/src/components/RequireRole.tsx @@ -7,11 +7,7 @@ export function RequireRole({ role, children }: { role: string; children: ReactN if (!user || !user.roles.includes(role)) { return ( - + ); } diff --git a/portal/frontend/src/components/customers/CustomerFormModal.tsx b/portal/frontend/src/components/customers/CustomerFormModal.tsx index 2d83778..5006d48 100644 --- a/portal/frontend/src/components/customers/CustomerFormModal.tsx +++ b/portal/frontend/src/components/customers/CustomerFormModal.tsx @@ -1,6 +1,10 @@ import { useEffect } from 'react'; import { Modal, Form, Input, Switch, Alert, Typography } from 'antd'; -import type { CustomerListItem, CreateCustomerPayload, UpdateCustomerPayload } from '../../api/customers'; +import type { + CustomerListItem, + CreateCustomerPayload, + UpdateCustomerPayload, +} from '../../api/customers'; const { Text } = Typography; @@ -68,7 +72,11 @@ export function CustomerFormModal({ open, mode, submitting, error, onClose, onSu > - + {isEdit && ( @@ -78,8 +86,8 @@ export function CustomerFormModal({ open, mode, submitting, error, onClose, onSu )} {!isEdit && ( - A push token will be generated and shown once. Set it as FleetIngest__Token in - the customer's environment. + A push token will be generated and shown once. Set it as{' '} + FleetIngest__Token in the customer's environment. )} diff --git a/portal/frontend/src/components/customers/TokenShownOnceModal.tsx b/portal/frontend/src/components/customers/TokenShownOnceModal.tsx index 685d224..0ad657b 100644 --- a/portal/frontend/src/components/customers/TokenShownOnceModal.tsx +++ b/portal/frontend/src/components/customers/TokenShownOnceModal.tsx @@ -53,16 +53,19 @@ export function TokenShownOnceModal({ open, customerCode, token, onClose }: Prop /> Set this as FleetIngest__Token in the customer's .env, - alongside FleetIngest__Url and FleetIngest__Enabled=true. + alongside FleetIngest__Url and FleetIngest__Enabled=true + . If this is a rotation (not the first issue), the old token continues to - work for 24h. Update the customer's .env and restart their portal - within that window to avoid dropped pushes. + work for 24h. Update the customer's .env and restart their portal within + that window to avoid dropped pushes. - + @@ -164,11 +191,22 @@ export function BrandingForm() { }} > {preview?.logoUrl && ( - + )} {preview?.applicationName || 'Application name'}
-
+
Sidebar / secondary surface
)} @@ -61,12 +70,14 @@ export function GrafanaInfoCard() { {data?.defaultDashboardUid || '(unset)'} - {data?.dashboards.length ?? 0} + + {data?.dashboards.length ?? 0} + - Add dashboards by dropping JSON into grafana/dashboards/ and adding a matching entry - to Grafana.Dashboards in configuration. + Add dashboards by dropping JSON into grafana/dashboards/ and adding a + matching entry to Grafana.Dashboards in configuration. diff --git a/portal/frontend/src/components/settings/rates/MunicipalityList.tsx b/portal/frontend/src/components/settings/rates/MunicipalityList.tsx index ebce8a5..c6e0aa6 100644 --- a/portal/frontend/src/components/settings/rates/MunicipalityList.tsx +++ b/portal/frontend/src/components/settings/rates/MunicipalityList.tsx @@ -1,14 +1,33 @@ import { useState } from 'react'; import { - Table, Button, Space, Modal, Input, Switch, Tag, Popconfirm, Form, Typography, message, Card, + Table, + Button, + Space, + Modal, + Input, + Switch, + Tag, + Popconfirm, + Form, + Typography, + message, + Card, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { - listMunicipalities, createMunicipality, updateMunicipality, deleteMunicipality, - listTariffs, getTariff, deleteTariff, - type Municipality, type TariffSummary, type UpsertMunicipality, type TariffDetail, + listMunicipalities, + createMunicipality, + updateMunicipality, + deleteMunicipality, + listTariffs, + getTariff, + deleteTariff, + type Municipality, + type TariffSummary, + type UpsertMunicipality, + type TariffDetail, } from '../../../api/rates'; import { TariffDrawer } from './TariffDrawer'; @@ -41,17 +60,29 @@ export function MunicipalityList() { const createMut = useMutation({ mutationFn: (payload: UpsertMunicipality) => createMunicipality(payload), - onSuccess: () => { message.success('Created'); closeMuni(); qc.invalidateQueries({ queryKey: ['municipalities'] }); }, + onSuccess: () => { + message.success('Created'); + closeMuni(); + qc.invalidateQueries({ queryKey: ['municipalities'] }); + }, onError: () => message.error('Create failed'), }); const updateMut = useMutation({ - mutationFn: ({ id, payload }: { id: number; payload: UpsertMunicipality }) => updateMunicipality(id, payload), - onSuccess: () => { message.success('Updated'); closeMuni(); qc.invalidateQueries({ queryKey: ['municipalities'] }); }, + mutationFn: ({ id, payload }: { id: number; payload: UpsertMunicipality }) => + updateMunicipality(id, payload), + onSuccess: () => { + message.success('Updated'); + closeMuni(); + qc.invalidateQueries({ queryKey: ['municipalities'] }); + }, onError: () => message.error('Update failed'), }); const deleteMut = useMutation({ mutationFn: (id: number) => deleteMunicipality(id), - onSuccess: () => { message.success('Deleted'); qc.invalidateQueries({ queryKey: ['municipalities'] }); }, + onSuccess: () => { + message.success('Deleted'); + qc.invalidateQueries({ queryKey: ['municipalities'] }); + }, onError: () => message.error('Delete failed (any tariffs are removed with it)'), }); const deleteTariffMut = useMutation({ @@ -93,27 +124,37 @@ export function MunicipalityList() { const columns: ColumnsType = [ { title: 'Name', dataIndex: 'name', key: 'name' }, - { title: 'Time zone', dataIndex: 'timeZoneId', key: 'tz', render: (v: string | null) => v ?? UTC }, + { + title: 'Time zone', + dataIndex: 'timeZoneId', + key: 'tz', + render: (v: string | null) => v ?? UTC, + }, { title: 'Tariffs', dataIndex: 'tariffCount', key: 'count' }, { title: 'Active', dataIndex: 'isActive', key: 'isActive', - render: (v: boolean) => (v ? Active : Disabled), + render: (v: boolean) => + v ? Active : Disabled, }, { title: 'Actions', key: 'actions', render: (_, m) => ( - + deleteMut.mutate(m.id)} > - + ), @@ -123,8 +164,12 @@ export function MunicipalityList() { return ( <> - Configure municipalities and their tariffs. Expand a row to see tariffs. - + + Configure municipalities and their tariffs. Expand a row to see tariffs. + + @@ -154,7 +199,12 @@ export function MunicipalityList() { onOk={() => muniForm.submit()} confirmLoading={createMut.isPending || updateMut.isPending} > - form={muniForm} layout="vertical" onFinish={onMuniSubmit} requiredMark={false}> + + form={muniForm} + layout="vertical" + onFinish={onMuniSubmit} + requiredMark={false} + > @@ -198,9 +248,23 @@ function TariffSubTable({ const columns: ColumnsType = [ { title: 'Name', dataIndex: 'name', key: 'name' }, - { title: 'Effective', key: 'effective', render: (_, t) => `${t.effectiveFrom}${t.effectiveTo ? ' → ' + t.effectiveTo : ' →'}` }, - { title: 'Default rate', dataIndex: 'defaultRatePerKwh', key: 'rate', render: (v: number) => v.toFixed(4) }, - { title: 'Fixed', dataIndex: 'fixedMonthlyCharge', key: 'fixed', render: (v: number) => v.toFixed(2) }, + { + title: 'Effective', + key: 'effective', + render: (_, t) => `${t.effectiveFrom}${t.effectiveTo ? ' → ' + t.effectiveTo : ' →'}`, + }, + { + title: 'Default rate', + dataIndex: 'defaultRatePerKwh', + key: 'rate', + render: (v: number) => v.toFixed(4), + }, + { + title: 'Fixed', + dataIndex: 'fixedMonthlyCharge', + key: 'fixed', + render: (v: number) => v.toFixed(2), + }, { title: 'VAT %', dataIndex: 'vatPercentage', key: 'vat', render: (v: number) => v.toFixed(2) }, { title: 'Periods', dataIndex: 'periodCount', key: 'periods' }, { @@ -213,14 +277,18 @@ function TariffSubTable({ key: 'actions', render: (_, t) => ( - + onDelete(t.id)} > - + ), @@ -230,7 +298,9 @@ function TariffSubTable({ return ( - + rowKey="id" diff --git a/portal/frontend/src/components/settings/rates/PeriodEditor.tsx b/portal/frontend/src/components/settings/rates/PeriodEditor.tsx index 5d0a0ae..6415ae5 100644 --- a/portal/frontend/src/components/settings/rates/PeriodEditor.tsx +++ b/portal/frontend/src/components/settings/rates/PeriodEditor.tsx @@ -68,7 +68,10 @@ export function PeriodEditor({ value, onChange }: Props) { {DAY_OPTIONS.map((d, dIdx) => { const active = (p.daysOfWeek & d.value) !== 0; return ( - + } />; + return ( + navigate('/admin/customers')}>Back} + /> + ); } const siteCols: ColumnsType = [ { title: 'Name', dataIndex: 'name', key: 'name' }, - { title: 'Address', dataIndex: 'address', key: 'addr', render: v => v ?? }, - { title: 'Active', dataIndex: 'isActive', key: 'a', render: (v: boolean) => v ? Active : Inactive }, + { + title: 'Address', + dataIndex: 'address', + key: 'addr', + render: (v) => v ?? , + }, + { + title: 'Active', + dataIndex: 'isActive', + key: 'a', + render: (v: boolean) => (v ? Active : Inactive), + }, ]; const deviceCols: ColumnsType = [ { title: 'Name', dataIndex: 'name', key: 'name' }, - { title: 'External ID', dataIndex: 'externalId', key: 'ext', render: v => {v} }, - { title: 'Description', dataIndex: 'description', key: 'desc', render: v => v ?? }, - { title: 'Active', dataIndex: 'isActive', key: 'a', render: (v: boolean) => v ? Active : Inactive }, + { + title: 'External ID', + dataIndex: 'externalId', + key: 'ext', + render: (v) => {v}, + }, + { + title: 'Description', + dataIndex: 'description', + key: 'desc', + render: (v) => v ?? , + }, + { + title: 'Active', + dataIndex: 'isActive', + key: 'a', + render: (v: boolean) => (v ? Active : Inactive), + }, ]; const measCols: ColumnsType = [ - { title: 'Time (UTC)', dataIndex: 'time', key: 't', render: (v: string) => new Date(v).toISOString().replace('T', ' ').slice(0, 19) }, + { + title: 'Time (UTC)', + dataIndex: 'time', + key: 't', + render: (v: string) => new Date(v).toISOString().replace('T', ' ').slice(0, 19), + }, { title: 'Device', dataIndex: 'deviceName', key: 'd' }, - { title: 'Active power (kW)', dataIndex: 'activePowerKw', key: 'p', render: (v: number) => v.toFixed(3) }, - { title: 'kWh imported (cumulative)', dataIndex: 'energyImportedKwh', key: 'e', render: (v: number | null) => v == null ? '—' : v.toFixed(2) }, + { + title: 'Active power (kW)', + dataIndex: 'activePowerKw', + key: 'p', + render: (v: number) => v.toFixed(3), + }, + { + title: 'kWh imported (cumulative)', + dataIndex: 'energyImportedKwh', + key: 'e', + render: (v: number | null) => (v == null ? '—' : v.toFixed(2)), + }, ]; const eventCols: ColumnsType = [ - { title: 'Received (UTC)', dataIndex: 'receivedAt', key: 'r', render: (v: string) => new Date(v).toISOString().replace('T', ' ').slice(0, 19) }, + { + title: 'Received (UTC)', + dataIndex: 'receivedAt', + key: 'r', + render: (v: string) => new Date(v).toISOString().replace('T', ' ').slice(0, 19), + }, { title: 'Type', dataIndex: 'batchType', key: 'bt' }, { title: 'Accepted', dataIndex: 'rowsAccepted', key: 'a' }, - { title: 'Rejected', dataIndex: 'rowsRejected', key: 'rj', render: (v: number) => v > 0 ? {v} : v }, + { + title: 'Rejected', + dataIndex: 'rowsRejected', + key: 'rj', + render: (v: number) => (v > 0 ? {v} : v), + }, { title: 'Bytes', dataIndex: 'batchBytes', key: 'b' }, - { title: 'Time spread', dataIndex: 'timeSpread', key: 'ts', render: (v: string | null) => v ?? '—' }, - { title: 'Error', dataIndex: 'error', key: 'e', render: (v: string | null) => v ? {v} : '—' }, + { + title: 'Time spread', + dataIndex: 'timeSpread', + key: 'ts', + render: (v: string | null) => v ?? '—', + }, + { + title: 'Error', + dataIndex: 'error', + key: 'e', + render: (v: string | null) => (v ? {v} : '—'), + }, ]; return ( - - {data.code} · {data.name} + + + {data.code} · {data.name} + {data.isActive ? Active : Disabled} {data.id} - {new Date(data.createdAt).toLocaleString()} - {data.firstSeenAt ? new Date(data.firstSeenAt).toLocaleString() : Never} - {data.lastSeenAt ? new Date(data.lastSeenAt).toLocaleString() : Never} + + {new Date(data.createdAt).toLocaleString()} + + + {data.firstSeenAt ? ( + new Date(data.firstSeenAt).toLocaleString() + ) : ( + Never + )} + + + {data.lastSeenAt ? ( + new Date(data.lastSeenAt).toLocaleString() + ) : ( + Never + )} + @@ -124,27 +236,64 @@ export function AdminCustomerDetailPage() { { key: 'ingest', label: `Recent ingest (${data.recentIngestEvents.length})`, - children: rowKey="receivedAt" columns={eventCols} dataSource={data.recentIngestEvents} pagination={false} size="small" />, + children: ( + + rowKey="receivedAt" + columns={eventCols} + dataSource={data.recentIngestEvents} + pagination={false} + size="small" + /> + ), }, { key: 'measurements', label: `Recent measurements (${data.recentMeasurements.length})`, - children: rowKey={(r) => `${r.time}-${r.deviceId}`} columns={measCols} dataSource={data.recentMeasurements} pagination={false} size="small" />, + children: ( + + rowKey={(r) => `${r.time}-${r.deviceId}`} + columns={measCols} + dataSource={data.recentMeasurements} + pagination={false} + size="small" + /> + ), }, { key: 'sites', label: `Sites (${data.sites.length})`, - children: rowKey="id" columns={siteCols} dataSource={data.sites} pagination={false} size="small" />, + children: ( + + rowKey="id" + columns={siteCols} + dataSource={data.sites} + pagination={false} + size="small" + /> + ), }, { key: 'devices', label: `Devices (${data.devices.length})`, - children: rowKey="id" columns={deviceCols} dataSource={data.devices} pagination={false} size="small" />, + children: ( + + rowKey="id" + columns={deviceCols} + dataSource={data.devices} + pagination={false} + size="small" + /> + ), }, { key: 'tariffs', label: `Tariffs (${data.tariffs.length})`, - children: , + children: ( + + ), }, { key: 'cost', @@ -159,7 +308,13 @@ export function AdminCustomerDetailPage() { } // ── Tariffs tab: collapsible per-tariff cards with the period table inline ─ -function TariffsTab({ tariffs, municipalitiesCount }: { tariffs: FleetTariffView[]; municipalitiesCount: number }) { +function TariffsTab({ + tariffs, + municipalitiesCount, +}: { + tariffs: FleetTariffView[]; + municipalitiesCount: number; +}) { if (tariffs.length === 0) { return ( @@ -170,14 +325,26 @@ function TariffsTab({ tariffs, municipalitiesCount }: { tariffs: FleetTariffView } const periodCols: ColumnsType = [ - { title: 'Period', dataIndex: 'name', key: 'name', render: (v: string) => {v} }, { - title: 'Days', dataIndex: 'daysOfWeek', key: 'd', + title: 'Period', + dataIndex: 'name', + key: 'name', + render: (v: string) => {v}, + }, + { + title: 'Days', + dataIndex: 'daysOfWeek', + key: 'd', render: (mask: number) => formatDays(mask), }, { title: 'Start', dataIndex: 'startTime', key: 's' }, { title: 'End', dataIndex: 'endTime', key: 'e' }, - { title: 'Rate (/kWh)', dataIndex: 'ratePerKwh', key: 'r', render: (n: number) => n.toFixed(4) }, + { + title: 'Rate (/kWh)', + dataIndex: 'ratePerKwh', + key: 'r', + render: (n: number) => n.toFixed(4), + }, ]; return ( @@ -196,13 +363,18 @@ function TariffsTab({ tariffs, municipalitiesCount }: { tariffs: FleetTariffView } extra={ - {t.effectiveFrom}{t.effectiveTo ? ` → ${t.effectiveTo}` : ' →'} + {t.effectiveFrom} + {t.effectiveTo ? ` → ${t.effectiveTo}` : ' →'} } > - {t.defaultRatePerKwh.toFixed(4)}/kWh - {t.fixedMonthlyCharge.toFixed(2)} + + {t.defaultRatePerKwh.toFixed(4)}/kWh + + + {t.fixedMonthlyCharge.toFixed(2)} + {t.vatPercentage.toFixed(2)}% {t.periods.length > 0 ? ( @@ -247,18 +419,35 @@ function CostTab({ customerId }: { customerId: string }) { { title: 'kWh', dataIndex: 'kwh', key: 'kwh', render: (n: number) => n.toFixed(3) }, { title: 'Base cost', dataIndex: 'baseCost', key: 'b', render: (n: number) => n.toFixed(2) }, { title: 'VAT', dataIndex: 'vatAmount', key: 'v', render: (n: number) => n.toFixed(2) }, - { title: 'Total', dataIndex: 'totalCost', key: 't', render: (n: number) => {n.toFixed(2)} }, + { + title: 'Total', + dataIndex: 'totalCost', + key: 't', + render: (n: number) => {n.toFixed(2)}, + }, ]; const deviceCols: ColumnsType = [ - { title: 'Device', dataIndex: 'deviceName', key: 'd', render: (v: string) => {v} }, { - title: 'Municipality', dataIndex: 'municipalityName', key: 'm', - render: (v: string | null) => v ?? + title: 'Device', + dataIndex: 'deviceName', + key: 'd', + render: (v: string) => {v}, + }, + { + title: 'Municipality', + dataIndex: 'municipalityName', + key: 'm', + render: (v: string | null) => v ?? , }, { title: 'kWh', dataIndex: 'kwh', key: 'kwh', render: (n: number) => n.toFixed(3) }, { title: 'Base cost', dataIndex: 'baseCost', key: 'b', render: (n: number) => n.toFixed(2) }, - { title: 'Total', dataIndex: 'totalCost', key: 't', render: (n: number) => {n.toFixed(2)} }, + { + title: 'Total', + dataIndex: 'totalCost', + key: 't', + render: (n: number) => {n.toFixed(2)}, + }, ]; return ( @@ -270,11 +459,19 @@ function CostTab({ customerId }: { customerId: string }) { allowClear={false} value={range} showTime={false} - onChange={(v) => v && v[0] && v[1] && setRange([v[0].startOf('day'), v[1].endOf('day')])} + onChange={(v) => + v && v[0] && v[1] && setRange([v[0].startOf('day'), v[1].endOf('day')]) + } ranges={{ - 'Today': [dayjs().startOf('day'), dayjs().add(1, 'day').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')], + Today: [dayjs().startOf('day'), dayjs().add(1, 'day').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')], }} /> @@ -289,15 +486,58 @@ function CostTab({ customerId }: { customerId: string }) { {isLoading && } - {error && } + {error && ( + + )} {data && ( <> - } /> - } /> - } /> - } /> + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + {data.bucketsWithoutTariff > 0 && ( @@ -318,15 +558,21 @@ function CostTab({ customerId }: { customerId: string }) { - rowKey="date" size="small" pagination={false} - columns={dayCols} dataSource={data.daily} + rowKey="date" + size="small" + pagination={false} + columns={dayCols} + dataSource={data.daily} /> - rowKey="deviceId" size="small" pagination={false} - columns={deviceCols} dataSource={data.perDevice} + rowKey="deviceId" + size="small" + pagination={false} + columns={deviceCols} + dataSource={data.perDevice} /> diff --git a/portal/frontend/src/pages/AdminFleetDashboardPage.tsx b/portal/frontend/src/pages/AdminFleetDashboardPage.tsx index c3c9f44..7ba28b2 100644 --- a/portal/frontend/src/pages/AdminFleetDashboardPage.tsx +++ b/portal/frontend/src/pages/AdminFleetDashboardPage.tsx @@ -1,7 +1,11 @@ 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, + ApartmentOutlined, + ThunderboltOutlined, + TeamOutlined, + CheckCircleOutlined, + DollarOutlined, DownloadOutlined, } from '@ant-design/icons'; import { useQuery } from '@tanstack/react-query'; @@ -23,26 +27,38 @@ export function AdminFleetDashboardPage() { { title: 'Code', dataIndex: 'code', key: 'code', render: (v) => {v} }, { title: 'Name', dataIndex: 'name', key: 'name' }, { - title: 'Status', dataIndex: 'isActive', key: 'active', - render: (v: boolean) => (v ? Active : Disabled) + title: 'Status', + dataIndex: 'isActive', + key: 'active', + render: (v: boolean) => + v ? Active : Disabled, }, { - title: 'Last push', dataIndex: 'lastSeenAt', key: 'last', - render: (v: string | null) => v ? lagDescription(v) : Never + title: 'Last push', + dataIndex: 'lastSeenAt', + key: 'last', + render: (v: string | null) => (v ? lagDescription(v) : Never), }, { 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 (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 (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 ? : {v.toFixed(2)} + title: 'Today (cost)', + dataIndex: 'costToday', + key: 'cost', + render: (v: number | null) => + v == null ? : {v.toFixed(2)}, }, ]; @@ -51,7 +67,12 @@ export function AdminFleetDashboardPage() { - } loading={isLoading} /> + } + loading={isLoading} + /> diff --git a/portal/frontend/src/pages/AdminSitesPage.tsx b/portal/frontend/src/pages/AdminSitesPage.tsx index d3fc99f..0bb72f3 100644 --- a/portal/frontend/src/pages/AdminSitesPage.tsx +++ b/portal/frontend/src/pages/AdminSitesPage.tsx @@ -4,9 +4,18 @@ import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { - listSites, createSite, updateSite, deleteSite, - listSiteDevices, createDevice, updateDevice, deleteDevice, - type Site, type Device, type UpsertSite, type UpsertDevice, + listSites, + createSite, + updateSite, + deleteSite, + listSiteDevices, + createDevice, + updateDevice, + deleteDevice, + type Site, + type Device, + type UpsertSite, + type UpsertDevice, } from '../api/sites'; import { SiteFormModal } from '../components/sites/SiteFormModal'; import { DeviceFormModal } from '../components/sites/DeviceFormModal'; @@ -15,9 +24,18 @@ const { Text } = Typography; export function AdminSitesPage() { const qc = useQueryClient(); - const [siteModal, setSiteModal] = useState<{ open: boolean; editing: Site | null }>({ open: false, editing: null }); - const [deviceModal, setDeviceModal] = useState<{ open: boolean; siteId: string | null; editing: Device | null }>({ - open: false, siteId: null, editing: null, + const [siteModal, setSiteModal] = useState<{ open: boolean; editing: Site | null }>({ + open: false, + editing: null, + }); + const [deviceModal, setDeviceModal] = useState<{ + open: boolean; + siteId: string | null; + editing: Device | null; + }>({ + open: false, + siteId: null, + editing: null, }); const [expanded, setExpanded] = useState([]); @@ -25,22 +43,34 @@ export function AdminSitesPage() { const createSiteMut = useMutation({ mutationFn: (p: UpsertSite) => createSite(p), - onSuccess: () => { message.success('Site created'); setSiteModal({ open: false, editing: null }); qc.invalidateQueries({ queryKey: ['sites'] }); }, + onSuccess: () => { + message.success('Site created'); + setSiteModal({ open: false, editing: null }); + qc.invalidateQueries({ queryKey: ['sites'] }); + }, onError: () => message.error('Create failed'), }); const updateSiteMut = useMutation({ mutationFn: ({ id, payload }: { id: string; payload: UpsertSite }) => updateSite(id, payload), - onSuccess: () => { message.success('Site updated'); setSiteModal({ open: false, editing: null }); qc.invalidateQueries({ queryKey: ['sites'] }); }, + onSuccess: () => { + message.success('Site updated'); + setSiteModal({ open: false, editing: null }); + qc.invalidateQueries({ queryKey: ['sites'] }); + }, onError: () => message.error('Update failed'), }); const deleteSiteMut = useMutation({ mutationFn: (id: string) => deleteSite(id), - onSuccess: () => { message.success('Site deleted'); qc.invalidateQueries({ queryKey: ['sites'] }); }, + onSuccess: () => { + message.success('Site deleted'); + qc.invalidateQueries({ queryKey: ['sites'] }); + }, onError: () => message.error('Delete failed'), }); const createDeviceMut = useMutation({ - mutationFn: ({ siteId, payload }: { siteId: string; payload: UpsertDevice }) => createDevice(siteId, payload), + mutationFn: ({ siteId, payload }: { siteId: string; payload: UpsertDevice }) => + createDevice(siteId, payload), onSuccess: (_, vars) => { message.success('Device created'); setDeviceModal({ open: false, siteId: null, editing: null }); @@ -50,7 +80,8 @@ export function AdminSitesPage() { onError: () => message.error('Create failed'), }); const updateDeviceMut = useMutation({ - mutationFn: ({ deviceId, payload }: { deviceId: string; payload: UpsertDevice }) => updateDevice(deviceId, payload), + mutationFn: ({ deviceId, payload }: { deviceId: string; payload: UpsertDevice }) => + updateDevice(deviceId, payload), onSuccess: () => { message.success('Device updated'); setDeviceModal({ open: false, siteId: null, editing: null }); @@ -84,26 +115,40 @@ export function AdminSitesPage() { const columns: ColumnsType = [ { title: 'Name', dataIndex: 'name', key: 'name' }, - { title: 'Address', dataIndex: 'address', key: 'address', render: (v) => v ?? }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + render: (v) => v ?? , + }, { title: 'Devices', dataIndex: 'deviceCount', key: 'deviceCount' }, { title: 'Active', dataIndex: 'isActive', - render: (v: boolean) => (v ? Active : Disabled), + render: (v: boolean) => + v ? Active : Disabled, }, { title: 'Actions', key: 'actions', render: (_, site) => ( - + deleteSiteMut.mutate(site.id)} > - + ), @@ -114,7 +159,11 @@ export function AdminSitesPage() { } onClick={() => setSiteModal({ open: true, editing: null })}> + } @@ -159,7 +208,10 @@ export function AdminSitesPage() { } function DeviceSubTable({ - site, onCreate, onEdit, onDelete, + site, + onCreate, + onEdit, + onDelete, }: { site: Site; onCreate: () => void; @@ -185,14 +237,18 @@ function DeviceSubTable({ key: 'actions', render: (_, d) => ( - + onDelete(d)} > - + ), @@ -202,7 +258,9 @@ function DeviceSubTable({ return ( - + rowKey="id" diff --git a/portal/frontend/src/pages/DashboardPage.tsx b/portal/frontend/src/pages/DashboardPage.tsx index f074f03..7a08e45 100644 --- a/portal/frontend/src/pages/DashboardPage.tsx +++ b/portal/frontend/src/pages/DashboardPage.tsx @@ -1,9 +1,24 @@ import { useMemo, useState } from 'react'; import { - Alert, Button, Card, Col, DatePicker, Row, Space, Spin, Statistic, Table, Tag, Typography, + Alert, + Button, + Card, + Col, + DatePicker, + Row, + Space, + Spin, + Statistic, + Table, + Tag, + Typography, } from 'antd'; import { - DollarOutlined, DownloadOutlined, ThunderboltOutlined, ApartmentOutlined, ClockCircleOutlined, + DollarOutlined, + DownloadOutlined, + ThunderboltOutlined, + ApartmentOutlined, + ClockCircleOutlined, } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { useQuery } from '@tanstack/react-query'; @@ -12,8 +27,11 @@ import dayjs, { type Dayjs } from 'dayjs'; import { useAppInfo } from '../hooks/useAppInfo'; import { AdminFleetDashboardPage } from './AdminFleetDashboardPage'; import { - fetchDashboardSummary, downloadDashboardSummaryXlsx, downloadRawMeasurementsXlsx, - type DashboardDeviceRow, type DashboardSummary, + fetchDashboardSummary, + downloadDashboardSummaryXlsx, + downloadRawMeasurementsXlsx, + type DashboardDeviceRow, + type DashboardSummary, } from '../api/dashboard'; const { Text } = Typography; @@ -48,12 +66,20 @@ function ClientDashboard() { v && v[0] && v[1] && setRange([v[0].startOf('day'), v[1].endOf('day')])} + onChange={(v) => + v && v[0] && v[1] && setRange([v[0].startOf('day'), v[1].endOf('day')]) + } ranges={{ - 'Today': [dayjs().startOf('day'), dayjs().add(1, 'day').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')], + Today: [dayjs().startOf('day'), dayjs().add(1, 'day').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')], }} /> @@ -76,7 +102,8 @@ function ClientDashboard() { {error && ( @@ -169,46 +196,93 @@ function ChartPanel({ data, loading }: { data: DashboardSummary | undefined; loa name: 'kW', nameTextStyle: { fontSize: 11 }, }, - series: [{ - name: 'Active power', - type: 'line', - smooth: true, - showSymbol: false, - areaStyle: { opacity: 0.15 }, - data: points.map(p => [p.time, p.totalKw]), - }], + series: [ + { + name: 'Active power', + type: 'line', + smooth: true, + showSymbol: false, + areaStyle: { opacity: 0.15 }, + data: points.map((p) => [p.time, p.totalKw]), + }, + ], }; }, [data]); if (loading) { - return
- -
; + return ( +
+ +
+ ); } if (!data || data.chart.length === 0) { - return
- - - No measurements yet in this window. - -
; + return ( +
+ + + No measurements yet in this window. + +
+ ); } - return ; + return ( + + ); } function DeviceTable({ rows, loading }: { rows: DashboardDeviceRow[]; loading: boolean }) { const columns: ColumnsType = [ - { title: 'Device', dataIndex: 'deviceName', key: 'd', render: (v: string) => {v} }, - { title: 'Site', dataIndex: 'siteName', key: 's', render: (v: string | null) => v ?? }, - { title: 'kWh', dataIndex: 'kwh', key: 'k', render: (v: number) => v.toFixed(3), align: 'right' as const }, - { title: 'Peak kW', dataIndex: 'peakKw', key: 'p', - render: (v: number | null) => v == null ? '—' : v.toFixed(3), align: 'right' as const }, - { title: 'Last seen (UTC)', dataIndex: 'lastSeen', key: 'l', - render: (v: string | null) => v ? lastSeenTag(v) : never }, - { title: 'Cost', dataIndex: 'cost', key: 'c', align: 'right' as const, - render: (v: number | null) => v == null ? : {v.toFixed(2)} }, + { + title: 'Device', + dataIndex: 'deviceName', + key: 'd', + render: (v: string) => {v}, + }, + { + title: 'Site', + dataIndex: 'siteName', + key: 's', + render: (v: string | null) => v ?? , + }, + { + title: 'kWh', + dataIndex: 'kwh', + key: 'k', + render: (v: number) => v.toFixed(3), + align: 'right' as const, + }, + { + title: 'Peak kW', + dataIndex: 'peakKw', + key: 'p', + render: (v: number | null) => (v == null ? '—' : v.toFixed(3)), + align: 'right' as const, + }, + { + title: 'Last seen (UTC)', + dataIndex: 'lastSeen', + key: 'l', + render: (v: string | null) => (v ? lastSeenTag(v) : never), + }, + { + title: 'Cost', + dataIndex: 'cost', + key: 'c', + align: 'right' as const, + render: (v: number | null) => + v == null ? : {v.toFixed(2)}, + }, ]; return ( diff --git a/portal/frontend/src/pages/MeasurementsPage.tsx b/portal/frontend/src/pages/MeasurementsPage.tsx index 9bd0827..82297a5 100644 --- a/portal/frontend/src/pages/MeasurementsPage.tsx +++ b/portal/frontend/src/pages/MeasurementsPage.tsx @@ -1,13 +1,24 @@ import { useMemo, useState } from 'react'; import { - Alert, Button, Card, DatePicker, InputNumber, Select, Space, Table, Tag, Typography, + 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, + fetchRawMeasurements, + listAllDevices, + downloadRawMeasurementsExport, type RawMeasurementRow, } from '../api/measurements'; @@ -37,42 +48,97 @@ export function MeasurementsPage() { }); 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, + 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 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, + 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) }, + { + 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; @@ -91,27 +157,43 @@ export function MeasurementsPage() { allowClear={false} value={range} showTime={false} - onChange={(v) => v && v[0] && v[1] && (setRange([v[0].startOf('day'), v[1].endOf('day')]), setOffset(0))} + onChange={(v) => + 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')], + 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: + + Meters: + { setLimit(v); setOffset(0); }} - options={PREVIEW_LIMIT_OPTIONS.map(n => ({ value: n, label: n.toString() }))} + onChange={(v) => { + setLimit(v); + setOffset(0); + }} + options={PREVIEW_LIMIT_OPTIONS.map((n) => ({ value: n, label: n.toString() }))} style={{ width: 96 }} /> · @@ -146,7 +231,8 @@ export function MeasurementsPage() { {error && ( @@ -158,7 +244,9 @@ export function MeasurementsPage() { Measurements {totalCount.toLocaleString()} rows match {selectedDeviceIds.length > 0 && ( - {selectedDeviceIds.length} meter{selectedDeviceIds.length === 1 ? '' : 's'} selected + + {selectedDeviceIds.length} meter{selectedDeviceIds.length === 1 ? '' : 's'} selected + )} } @@ -171,11 +259,14 @@ export function MeasurementsPage() { type="primary" icon={} disabled={totalCount === 0} - onClick={() => downloadRawMeasurementsExport({ - fromUtc: fromIso, toUtc: toIso, - deviceIds: selectedDeviceIds.length > 0 ? selectedDeviceIds : undefined, - rowCap: exportRowCap, - })} + onClick={() => + downloadRawMeasurementsExport({ + fromUtc: fromIso, + toUtc: toIso, + deviceIds: selectedDeviceIds.length > 0 ? selectedDeviceIds : undefined, + rowCap: exportRowCap, + }) + } > Export to Excel @@ -199,7 +290,10 @@ export function MeasurementsPage() { : `Showing ${showingFrom.toLocaleString()}–${showingTo.toLocaleString()} of ${totalCount.toLocaleString()}`}
-