'use client'; import { FunctionComponent, KeyboardEvent, useState } from 'react'; import { useLocale, useTranslations } from 'next-intl'; import { Box, Chip, Collapse, Paper, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material'; import { formatShamsiDateTime } from '@/utils'; import type { AdminUserSummary, AuditLogEntry } from '@/services/admin/types'; import AppIcon from '../common/AppIcon'; export interface AuditLogRowProps { entry: AuditLogEntry; /** Resolved actor name (3.2's batch id→label lookup) — falls back to `#id` until the REQ-061 lookup resolves. */ actorLabel?: string; } /** Stringify a diff value (JSON for objects, `—` for null). */ function displayValue(v: unknown): string { if (v == null) return '—'; if (typeof v === 'object') return JSON.stringify(v); return String(v); } /** The actor cell's display text — the resolved name when known, else the honest `#id` fallback. */ export function actorDisplay(actorUserId: number | null, actorLabel?: string): string { if (actorUserId == null) return '—'; return actorLabel ?? `#${actorUserId}`; } /** Resolve a batch `lookupUsers` result into the label `AuditLogRow`/`actorDisplay` expects. */ export function actorLabelFrom(userMap: Map | undefined, userId: number | null): string | undefined { if (userId == null) return undefined; return userMap?.get(userId)?.displayName; } /** * One row of the append-only audit viewer, with an expandable `changedFields` diff (old → new per field; * PII is server-redacted as ``). Read-only by design — there is **no** edit/delete affordance * (phase §5). Presentational; the caller passes the paged entries + (once resolved) the actor's name. The * expand chevron rotates and the header carries `aria-expanded` + button semantics so open/closed state is * visible and keyboard-toggleable (ui-phase-11). * @component AuditLogRow */ const AuditLogRow: FunctionComponent = ({ entry, actorLabel }) => { const t = useTranslations('admin'); const locale = useLocale(); const [open, setOpen] = useState(false); const fields = entry.changedFields ? Object.entries(entry.changedFields) : []; const hasDetail = fields.length > 0; const toggle = () => setOpen((v) => !v); const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } }; return ( {entry.action} {actorDisplay(entry.actorUserId, actorLabel)} {formatShamsiDateTime(entry.occurredAt, locale)} {hasDetail ? ( ) : null} {t('audit_diff_title')} {t('audit_diff_field')} {t('audit_diff_old')} {t('audit_diff_new')} {fields.map(([field, delta]) => ( {field} {displayValue(delta.old)} {displayValue(delta.new)} ))}
); }; export default AuditLogRow;