frontend phase 15

This commit is contained in:
hamid
2026-07-10 20:28:06 +03:30
parent bc51cf59b4
commit 70cf00ce4a
151 changed files with 10711 additions and 44 deletions
@@ -0,0 +1,85 @@
'use client';
import { FunctionComponent, 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 { AuditLogEntry } from '@/services/admin/types';
import AppIcon from '../common/AppIcon';
export interface AuditLogRowProps {
entry: AuditLogEntry;
}
/** 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);
}
/**
* One row of the append-only audit viewer, with an expandable `changedFields` diff (old → new per field;
* PII is server-redacted as `<redacted>`). Read-only by design — there is **no** edit/delete affordance
* (phase §5). Presentational; the caller passes the paged entries.
* @component AuditLogRow
*/
const AuditLogRow: FunctionComponent<AuditLogRowProps> = ({ entry }) => {
const t = useTranslations('admin');
const locale = useLocale();
const [open, setOpen] = useState(false);
const fields = entry.changedFields ? Object.entries(entry.changedFields) : [];
return (
<Paper
elevation={0}
data-audit-id={entry.id}
sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 2, overflow: 'hidden' }}
>
<Stack
direction="row"
sx={{ gap: 2, alignItems: 'center', p: 1.5, cursor: fields.length ? 'pointer' : 'default', flexWrap: 'wrap' }}
onClick={fields.length ? () => setOpen((v) => !v) : undefined}
>
<Chip size="small" variant="outlined" label={`${entry.entityType} #${entry.entityId}`} />
<Typography variant="body2" sx={{ fontWeight: 700 }}>
{entry.action}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', flexGrow: 1 }}>
{entry.actorUserId != null ? `#${entry.actorUserId}` : '—'}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{formatShamsiDateTime(entry.occurredAt, locale)}
</Typography>
{fields.length ? <AppIcon icon="expand" size={18} color="var(--bal-text-secondary)" /> : null}
</Stack>
<Collapse in={open && fields.length > 0}>
<Box sx={{ px: 1.5, pb: 1.5 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 700 }}>
{t('audit_diff_title')}
</Typography>
<Table size="small" sx={{ mt: 0.5 }}>
<TableHead>
<TableRow>
<TableCell sx={{ color: 'text.secondary' }}>{t('audit_diff_field')}</TableCell>
<TableCell sx={{ color: 'text.secondary' }}>{t('audit_diff_old')}</TableCell>
<TableCell sx={{ color: 'text.secondary' }}>{t('audit_diff_new')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{fields.map(([field, delta]) => (
<TableRow key={field}>
<TableCell sx={{ fontWeight: 600 }}>{field}</TableCell>
<TableCell sx={{ color: 'text.secondary' }}>{displayValue(delta.old)}</TableCell>
<TableCell>{displayValue(delta.new)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</Paper>
);
};
export default AuditLogRow;