* feat(bookkeeping): Ny verifikat modal, ledger-style list, SIE no-underlag exemptions Verifikat UX - "Ny verifikat" opens in a modal (NewJournalEntryDialog) instead of an inline tab; the review step renders inline in the dialog rather than stacking a second dialog. - JournalEntryForm: konteringsrader are the focus, with a compact pre-filled metadata bar (datum/serie/text/valuta/period) on top; verifikationstext auto-fills from the first row's account. - JournalEntryList: belopp shown on collapsed rows; expanded view is an aligned Konto/Benämning/Debet/Kredit table. SIE imports no longer flood "Att hantera: saknade underlag" - Import gains an opt-in (off by default) toggle to mark imported verifikat as "Inget underlag krävs"; a "Rekommenderas vid migrering" badge nudges it for historical years. - Multi-select batch-mark in the list for selective cleanup. - Filter-scoped bulk mark (POST /api/bookkeeping/no-doc-required/bulk-missing): marks every missing-doc verifikat matching the active filters across all pages, with a dry_run count to confirm scope — the scalable remedy for a post-import flood. - Shared helper markEntriesNoDocRequired + per-entry batch route. Tests: no-doc helper, batch route, bulk-missing route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): address PR #698 review findings - JournalEntryForm: restore the explicit "no underlag" acknowledgement in the modal's inline review. When no document is attached, the confirm button reads "Bokför utan underlag" (BFL 5 kap 6-7 §§), equivalent to the blocking dialog the non-bare flow shows — the bare path no longer posts behind only a passive banner. - batch no-doc route: guard the ownership query with source_type IN NEEDS_DOC_SOURCE_TYPES so a crafted request can't exempt non-document-requiring entries (defense in depth on top of company + posted scoping). - bulk-missing route: resolve doc/exemption status by querying only the candidate ids (chunked) instead of loading the company's full document_attachments and journal_entry_no_doc_required tables into memory — data minimisation + bounded memory for large migrations (the most-repeated reviewer finding). Triaged as non-issues (left as-is): partial-import exemption (gated on result.success == zero errors), reason write-back (sidecar row is FK-linked and carries the reason), and "bulk-exempting manual entries" (consistent with the existing per-entry NoDocRequiredToggle). No DB migration — reuses the existing journal_entry_no_doc_required table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): centralize bulk-missing date/series validation in Zod Move the ISO-date and verifikationsserie format checks into the Zod schema so malformed input is rejected with a clean 400 instead of being silently nulled (or, for a shaped-but-invalid date, throwing a 500 via fetchAllRows). The date refinement rejects values like 9999-99-99 / 2026-02-30 that a bare /^\d{4}-\d{2}-\d{2}$/ regex lets through. Addresses the PR #698 reviewer nit on split schema-vs-runtime validation. +2 route tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslations } from 'next-intl'
|
|
import { Copy, Loader2 } from 'lucide-react'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import JournalEntryForm, { type FormLine } from '@/components/bookkeeping/JournalEntryForm'
|
|
|
|
export interface CopyPrefill {
|
|
sourceId: string
|
|
sourceVoucherLabel: string
|
|
lines: FormLine[]
|
|
description: string
|
|
notes: string
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
/** Fired after a verifikat is created/saved as draft. */
|
|
onCreated: () => void
|
|
/** When set, the form is pre-filled from a copied verifikat. */
|
|
copyPrefill?: CopyPrefill | null
|
|
/** True while the copy source is being fetched. */
|
|
isLoading?: boolean
|
|
}
|
|
|
|
/**
|
|
* "Ny verifikat" as a modal — the dialog you type a manual voucher into,
|
|
* instead of an inline tab. Wraps the standalone JournalEntryForm; the form's
|
|
* own review/confirm dialogs stack on top of this one.
|
|
*/
|
|
export default function NewJournalEntryDialog({
|
|
open,
|
|
onOpenChange,
|
|
onCreated,
|
|
copyPrefill,
|
|
isLoading,
|
|
}: Props) {
|
|
const t = useTranslations('bookkeeping')
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-3xl max-h-[95dvh] sm:max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('new_entry_dialog_title')}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center gap-2 py-12 text-muted-foreground">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
<span className="text-sm">{t('loading_source_voucher')}</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{copyPrefill && (
|
|
<div className="flex items-start gap-3 rounded-lg border border-border bg-muted/30 p-3 text-sm">
|
|
<Copy className="h-4 w-4 mt-0.5 shrink-0 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="font-medium">
|
|
{t('copy_banner_title', {
|
|
label: copyPrefill.sourceVoucherLabel || t('copy_banner_unknown_label'),
|
|
})}
|
|
</p>
|
|
<p className="text-muted-foreground mt-0.5">{t('copy_banner_body')}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<JournalEntryForm
|
|
key={copyPrefill?.sourceId ?? 'fresh'}
|
|
bare
|
|
onCreated={onCreated}
|
|
initialLines={copyPrefill?.lines}
|
|
initialDescription={copyPrefill?.description}
|
|
initialNotes={copyPrefill?.notes}
|
|
/>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|