'use client'; import type { AdminErrorDto } from '@impuestos/contracts'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { EmptyState } from '@/components/ui/empty-state'; import { Input } from '@/components/ui/input'; import { Select } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { Table, Td, Th, Tr } from '@/components/ui/table'; import { useT } from '@/i18n/t'; import { api } from '@/lib/api'; const STAGES = ['qr_parse', 'ocr', 'dedupe', 'verify', 'job', 'other'] as const; const STATUSES = ['open', 'resolved'] as const; type Stage = (typeof STAGES)[number]; type Status = (typeof STATUSES)[number]; /** Flow H: everything that failed on the way in, with the two things staff can do about it. */ export function ErrorsScreen() { const t = useT(); const [stage, setStage] = useState(''); const [status, setStatus] = useState('open'); const [expanded, setExpanded] = useState(null); const errors = useQuery({ queryKey: ['admin', 'errors', stage, status], queryFn: ({ signal }) => api.listAdminErrors({ ...(stage ? { stage } : {}), ...(status ? { status } : {}) }, signal), }); return (

{t('admin.errors.title')}

{errors.isPending ? : null} {errors.isError ? (

{t('common.error.generic')}

) : null} {errors.data && errors.data.items.length === 0 ? ( ) : null} {errors.data && errors.data.items.length > 0 ? ( {errors.data.items.map((row) => ( setExpanded(expanded === row.id ? null : row.id)} /> ))}
{t('admin.errors.col.when')} {t('admin.errors.col.stage')} {t('admin.errors.col.user')} {t('admin.errors.col.message')} {t('admin.errors.col.status')}
) : null}
); } function ErrorRow({ row, expanded, onToggle, }: { row: AdminErrorDto; expanded: boolean; onToggle: () => void; }) { const t = useT(); return ( <> {/* A timestamp is machine data here, so it stays sortable rather than prose. */} {row.createdAt.slice(0, 16).replace('T', ' ')} {t(`admin.errors.stage.${row.stage}` as const)} {row.userEmail ?? t('common.none')} {t(`admin.errors.status.${row.status}` as const)} {expanded ? ( ) : null} ); } function ExpandedRow({ row }: { row: AdminErrorDto }) { const t = useT(); const queryClient = useQueryClient(); const [note, setNote] = useState(''); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'errors'] }); const resolve = useMutation({ mutationFn: () => api.resolveAdminError(row.id, { note: note.trim() }), onSuccess: invalidate, }); const retry = useMutation({ mutationFn: () => api.retryJob(row.id), onSuccess: invalidate, }); return (
{row.attempts !== null ? (

{t('admin.errors.attempts', { count: row.attempts })}

) : null}
        {JSON.stringify(row.payload ?? {}, null, 2)}
      
{row.source === 'job' ? (
{retry.isError ? (

{t('common.error.generic')}

) : null}
) : null} {row.source === 'ingest' && row.status === 'open' ? (
{ event.preventDefault(); resolve.mutate(); }} >
setNote(event.target.value)} placeholder={t('admin.errors.notePlaceholder')} className="h-9 rounded-lg text-sm" />
{resolve.isError ? (

{t('common.error.generic')}

) : null}
) : null}
); }