feat(CC-065): add OCR Review page (pending/failed OCR docs from documents table)
This commit is contained in:
parent
5121cb4cef
commit
036ed20135
1 changed files with 156 additions and 0 deletions
156
src/app/dashboard/documents/ocr/page.tsx
Normal file
156
src/app/dashboard/documents/ocr/page.tsx
Normal 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 "Eșuat" necesită reîncărcare sau corecție manuală în interfața Paperless-ngx.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue