'use client' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslations } from 'next-intl' import { Paperclip } from 'lucide-react' import { QUIET_LINK_CLASS, HOVER_REVEAL_CLASS } from '@/components/ui/dry-table' import { useToast } from '@/components/ui/use-toast' import { cn, formatDate } from '@/lib/utils' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' import { exceedsHostedUploadLimit, isShrinkableImage, tooLargeMessage } from '@/lib/documents/upload-size' import { shrinkImageForUpload } from '@/lib/documents/shrink-image' import type { ReconciliationAttachment } from '@/lib/reconciliation/schemas' /** * The underlag of one account's balansdag: the files it was reconciled * against (kontoutdrag, engagemangsbesked, reskontralista, inventering). * Lists what is attached for the date, attaches more, and lets a member * withdraw a wrong file (a stamp, the file stays). Reads and writes the * attachments routes under /api/reconciliation/accounts/{key}/attachments. */ interface ReconciliationUnderlagProps { accountKey: string /** The balansdag the files document: the sign-off date in play. */ throughDate: string canWrite?: boolean } const ACCEPT = 'application/pdf,image/jpeg,image/png,image/webp' function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} kB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } export function ReconciliationUnderlag({ accountKey, throughDate, canWrite = true }: ReconciliationUnderlagProps) { const t = useTranslations('reconciliation_underlag') const { toast } = useToast() const [attachments, setAttachments] = useState(null) const [busy, setBusy] = useState(null) const inputRef = useRef(null) const base = `/api/reconciliation/accounts/${encodeURIComponent(accountKey)}/attachments` const load = useCallback(async () => { try { const res = await fetch(`${base}?through_date=${throughDate}`) if (!res.ok) { setAttachments([]) return } const json = await res.json() setAttachments((json.data?.attachments ?? []) as ReconciliationAttachment[]) } catch { setAttachments([]) } }, [base, throughDate]) useEffect(() => { void load() }, [load]) async function upload(files: FileList | null) { if (!files || files.length === 0) return setBusy('upload') try { for (const original of Array.from(files)) { // Hosted functions refuse bodies over 4.5 MB before the route runs // (no logs, no message); shrink images, refuse the rest with a reason. let file = original if (exceedsHostedUploadLimit(file.size) && isShrinkableImage(file.type)) { file = await shrinkImageForUpload(file) } if (exceedsHostedUploadLimit(file.size)) { toast({ title: t('too_large_title'), description: tooLargeMessage(file.size), variant: 'destructive' }) continue } const form = new FormData() form.set('file', file) form.set('through_date', throughDate) const res = await fetch(base, { method: 'POST', body: form }) const json = await res.json().catch(() => ({})) if (!res.ok) { toast({ title: t('upload_failed'), description: getUserErrorMessage(json, { statusCode: res.status }), variant: 'destructive' }) continue } toast({ title: t('attached', { name: file.name }) }) } await load() } finally { setBusy(null) if (inputRef.current) inputRef.current.value = '' } } async function remove(attachment: ReconciliationAttachment) { setBusy(attachment.id) try { const res = await fetch(`${base}/${attachment.id}`, { method: 'DELETE' }) const json = await res.json().catch(() => ({})) if (!res.ok) { toast({ title: t('remove_failed'), description: getUserErrorMessage(json, { statusCode: res.status }), variant: 'destructive' }) return } toast({ title: t('removed', { name: attachment.file_name }) }) await load() } finally { setBusy(null) } } return (

{t('heading_dated', { date: formatDate(throughDate) })}

{attachments !== null && attachments.length === 0 && ( {t('empty_short')} )} {canWrite && ( <> void upload(e.target.files)} aria-label={t('attach')} /> )}
{attachments === null || attachments.length === 0 ? null : (
    {attachments.map((a) => (
  • {a.file_name} {formatSize(a.size_bytes)} ยท {formatDate(a.uploaded_at)} {canWrite && ( )}
  • ))}
)}
) }