Files
loyaly-merchant/src/features/admin/components/MonitoringTables.tsx
2026-09-29 00:44:48 +05:30

172 lines
4.9 KiB
TypeScript

'use client';
import {proportional} from '@astryxdesign/core/Table';
import type {TableColumn} from '@astryxdesign/core/Table';
import {HStack} from '@astryxdesign/core/Layout';
import {Text} from '@astryxdesign/core/Text';
import {StatusDot} from '@astryxdesign/core/StatusDot';
import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable';
import type {
AdminAlert,
AdminCameraStatus,
AdminEvent,
AdminSeverity,
} from '@/features/admin/types/monitoring';
/**
* The loaded branch of every drill-down section.
*
* None of these render today — every capability behind them is off, so their
* sections show PendingIntegration instead. They exist so that turning a level
* on is a flag flip: the rows are already written against the shapes in
* types/monitoring.ts, and a missing field prints a dash, never a guess.
*/
const DASH = '—';
function when(iso?: string): string {
return iso ? new Date(iso).toLocaleString() : DASH;
}
export const adminHref = {
merchants: '/admin/merchants',
company: (companyId: string) =>
`/admin/merchants/${encodeURIComponent(companyId)}`,
/** The merchant page, opened on its Shops tab — where "back" from a shop goes. */
shops: (companyId: string) => `${adminHref.company(companyId)}?tab=shops`,
store: (companyId: string, storeId: string) =>
`${adminHref.company(companyId)}/shops/${encodeURIComponent(storeId)}`,
camera: (companyId: string, storeId: string, cameraId: string) =>
`${adminHref.store(companyId, storeId)}/cameras/${encodeURIComponent(cameraId)}`,
};
/* ── Cameras ─────────────────────────────────────────────────────────── */
const CAMERA_STATUS: Record<
AdminCameraStatus,
{label: string; dot: 'success' | 'error' | 'neutral'}
> = {
online: {label: 'Online', dot: 'success'},
offline: {label: 'Offline', dot: 'error'},
unknown: {label: 'Unknown', dot: 'neutral'},
};
export function CameraStatus({status}: {status: AdminCameraStatus}) {
const s = CAMERA_STATUS[status];
return (
<HStack gap={1.5} vAlign="center">
<StatusDot variant={s.dot} label={s.label} />
<Text size="sm" color="secondary">
{s.label}
</Text>
</HStack>
);
}
const SEVERITY: Record<
AdminSeverity,
{label: string; dot: 'error' | 'warning' | 'neutral'}
> = {
critical: {label: 'Critical', dot: 'error'},
warning: {label: 'Warning', dot: 'warning'},
info: {label: 'Info', dot: 'neutral'},
};
function Severity({severity}: {severity?: AdminSeverity}) {
if (!severity) return <Text size="sm">{DASH}</Text>;
const s = SEVERITY[severity];
return (
<HStack gap={1.5} vAlign="center">
<StatusDot variant={s.dot} label={s.label} />
<Text size="sm" color="secondary">
{s.label}
</Text>
</HStack>
);
}
type EventRow = Record<string, unknown> & {
id: string;
at: string;
type: string;
camera: string;
severity?: AdminSeverity;
};
export function EventTable({events}: {events: AdminEvent[]}) {
const columns: TableColumn<EventRow>[] = [
{key: 'at', header: 'Time', width: proportional(2)},
{key: 'type', header: 'Event', width: proportional(2)},
{key: 'camera', header: 'Camera', width: proportional(2)},
{
key: 'severity',
header: 'Severity',
width: proportional(1),
renderCell: (row) => <Severity severity={row.severity} />,
},
];
const rows: EventRow[] = events.map((e) => ({
id: e.id,
at: when(e.at),
type: e.type,
camera: e.cameraName ?? DASH,
severity: e.severity,
}));
return (
<ResponsiveTable
data={rows}
columns={columns}
idKey="id"
primaryKey="type"
density="compact"
/>
);
}
type AlertRow = Record<string, unknown> & {
id: string;
at: string;
title: string;
camera: string;
severity: AdminSeverity;
status: string;
};
const ALERT_STATUS: Record<AdminAlert['status'], string> = {
open: 'Open',
acknowledged: 'Acknowledged',
resolved: 'Resolved',
};
export function AlertTable({alerts}: {alerts: AdminAlert[]}) {
const columns: TableColumn<AlertRow>[] = [
{
key: 'severity',
header: 'Severity',
width: proportional(1),
renderCell: (row) => <Severity severity={row.severity} />,
},
{key: 'title', header: 'Alert', width: proportional(3)},
{key: 'camera', header: 'Camera', width: proportional(2)},
{key: 'at', header: 'Time', width: proportional(2)},
{key: 'status', header: 'Status', width: proportional(1)},
];
const rows: AlertRow[] = alerts.map((a) => ({
id: a.id,
at: when(a.at),
title: a.title,
camera: a.cameraName ?? DASH,
severity: a.severity,
status: ALERT_STATUS[a.status],
}));
return (
<ResponsiveTable
data={rows}
columns={columns}
idKey="id"
primaryKey="title"
density="compact"
/>
);
}