feat(CC-065): add OCR Review page (pending/failed OCR docs from documents table)

This commit is contained in:
admin-valentin 2026-08-01 21:29:53 +00:00
parent 5121cb4cef
commit 036ed20135

View file

@ -0,0 +1,156 @@
'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
interface Document {
id: string; filename: string; classification: string;
ocrStatus: string | null; paperlessId: string | null;
checksum: string | null; createdAt: string;
}
const B = '/dashboard';
const OCR_META: Record<string, { label: string; cls: string; desc: string }> = {
pending: { label: 'În așteptare', cls: 'bg-signal-warn/10 text-signal-warn', desc: 'OCR nu a procesat documentul' },
done: { label: 'Complet', cls: 'bg-signal-ok/10 text-signal-ok', desc: 'Text extras cu succes' },
failed: { label: 'Eșuat', cls: 'bg-signal-danger/10 text-signal-danger', desc: 'OCR a eșuat, necesită corecție' },
not_needed: { label: 'Fără OCR', cls: 'bg-muted text-ink-faint', desc: 'Nu necesită procesare OCR' },
};
export default function OcrReviewPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: pendingDocs = [], isLoading: loadingPending } = useQuery({
queryKey: ['docs-ocr-pending', tenantId],
queryFn: () => apiFetch<Document[]>('/v1/documents?ocrStatus=pending&limit=100', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 60_000,
});
const { data: failedDocs = [], isLoading: loadingFailed } = useQuery({
queryKey: ['docs-ocr-failed', tenantId],
queryFn: () => apiFetch<Document[]>('/v1/documents?ocrStatus=failed&limit=100', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 60_000,
});
const { data: allDocs = [] } = useQuery({
queryKey: ['docs-all', tenantId],
queryFn: () => apiFetch<Document[]>('/v1/documents?limit=200', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 120_000,
});
const isLoading = loadingPending || loadingFailed;
const ocrStats: Record<string, number> = {};
for (const d of allDocs) {
const k = d.ocrStatus ?? 'not_needed';
ocrStats[k] = (ocrStats[k] || 0) + 1;
}
return (
<div className="max-w-4xl space-y-6 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">OCR Review</h1>
<p className="text-sm text-ink-faint mt-1">
{pendingDocs.length} documente pendente · {failedDocs.length} eșuate
</p>
</div>
<Link href={`${B}/documents`} className="text-xs text-bronze-deep hover:underline">
Toate documentele
</Link>
</div>
{/* OCR status summary */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{Object.entries(OCR_META).map(([status, meta]) => (
<div key={status} className={`rounded-xl border p-4 text-center ${meta.cls.split(' ')[0]}`}>
<p className={`font-display text-3xl font-bold ${meta.cls.split(' ').slice(1).join(' ')}`}>
{ocrStats[status] ?? 0}
</p>
<p className="text-xs text-ink-faint mt-1">{meta.label}</p>
</div>
))}
</div>
{isLoading ? (
<div className="card p-8 text-center text-sm text-ink-faint">Se încarcă</div>
) : pendingDocs.length === 0 && failedDocs.length === 0 ? (
<div className="card p-12 text-center space-y-2">
<p className="text-3xl"></p>
<p className="text-sm text-ink-faint">Toate documentele au fost procesate OCR.</p>
</div>
) : (
<>
{/* Failed OCR - needs attention first */}
{failedDocs.length > 0 && (
<div className="card p-5 space-y-3">
<h2 className="text-sm font-semibold text-signal-danger">
OCR Eșuat ({failedDocs.length}) necesită corecție manuală
</h2>
<div className="divide-y divide-border/50">
{failedDocs.map((d) => (
<div key={d.id} className="flex items-center gap-4 py-2.5">
<span className="text-xl">📄</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-ink font-medium truncate">{d.filename}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] text-ink-faint uppercase">{d.classification}</span>
{d.paperlessId && <span className="text-[10px] text-ink-faint font-mono">pls:{d.paperlessId}</span>}
</div>
</div>
<span className="text-[10px] text-ink-faint shrink-0">
{new Date(d.createdAt).toLocaleDateString('ro-RO')}
</span>
</div>
))}
</div>
</div>
)}
{/* Pending OCR */}
{pendingDocs.length > 0 && (
<div className="card p-5 space-y-3">
<h2 className="text-sm font-semibold text-signal-warn">
OCR Pendente ({pendingDocs.length}) în coadă
</h2>
<div className="divide-y divide-border/50">
{pendingDocs.slice(0, 20).map((d) => (
<div key={d.id} className="flex items-center gap-4 py-2.5">
<span className="text-xl">📄</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-ink font-medium truncate">{d.filename}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] text-ink-faint uppercase">{d.classification}</span>
{d.checksum && <span className="text-[10px] text-ink-faint/50 font-mono truncate max-w-20">{d.checksum.slice(0, 8)}</span>}
</div>
</div>
<span className="text-[10px] text-ink-faint shrink-0">
{new Date(d.createdAt).toLocaleDateString('ro-RO')}
</span>
</div>
))}
{pendingDocs.length > 20 && (
<p className="text-xs text-center text-ink-faint pt-2">și alte {pendingDocs.length - 20} documente</p>
)}
</div>
</div>
)}
<div className="rounded-lg border border-border/50 bg-muted/30 p-4">
<p className="text-xs text-ink-faint leading-relaxed">
<strong className="text-ink">Procesare OCR:</strong> Documentele sunt procesate automat de Paperless-ngx.
Cele cu status &quot;Eșuat&quot; necesită reîncărcare sau corecție manuală în interfața Paperless-ngx.
</p>
</div>
</>
)}
</div>
);
}