'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useLocale, useTranslations } from 'next-intl' import { Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table' import { ToastAction } from '@/components/ui/toast' import { useToast } from '@/components/ui/use-toast' import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' import { cn } from '@/lib/utils' import { formatQty } from '@/components/sales-orders/labels' import type { SalesOrder, SalesOrderItem } from '@/types' interface CreateInvoiceDialogProps { open: boolean onOpenChange: (open: boolean) => void order: SalesOrder onCreated: (order: SalesOrder, invoiceId: string) => void } function productLines(order: SalesOrder): SalesOrderItem[] { return [...(order.items ?? [])] .filter((i) => (i.line_type ?? 'product') === 'product') .sort((a, b) => a.sort_order - b.sort_order) } function remainingOf(item: SalesOrderItem): number { return Math.max(0, item.remaining_qty ?? item.quantity - (item.invoiced_qty ?? 0)) } function deliveredNotInvoicedOf(item: SalesOrderItem): number { return Math.max(0, Math.min(remainingOf(item), (item.delivered_qty ?? 0) - (item.invoiced_qty ?? 0))) } function parseQty(value: string | undefined): number { const n = parseFloat((value ?? '').replace(',', '.')) return Number.isFinite(n) && n > 0 ? n : 0 } /** * Creates a DRAFT kundfaktura from the order for the picked quantities. * Prefilled with what remains; the two shortcuts reseed the picks (all * remaining / delivered but not yet invoiced). The explicit line picks are * what is sent, so what the user sees is exactly what gets invoiced. */ export default function CreateInvoiceDialog({ open, onOpenChange, order, onCreated, }: CreateInvoiceDialogProps) { const t = useTranslations('sales_order_detail') const tCommon = useTranslations('common') const errorLocale = useLocale() as ErrorLocale const router = useRouter() const { toast } = useToast() const [quantities, setQuantities] = useState>({}) const [invoiceDate, setInvoiceDate] = useState('') const [dueDate, setDueDate] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const lines = productLines(order) function seed(mode: 'remaining' | 'delivered') { const next: Record = {} for (const item of productLines(order)) { const qty = mode === 'remaining' ? remainingOf(item) : deliveredNotInvoicedOf(item) next[item.id] = qty > 0 ? String(qty) : '' } setQuantities(next) } useEffect(() => { if (!open) return setInvoiceDate('') setDueDate('') const next: Record = {} for (const item of productLines(order)) { const qty = remainingOf(item) next[item.id] = qty > 0 ? String(qty) : '' } setQuantities(next) }, [open, order]) const picked = lines .map((item) => ({ sales_order_item_id: item.id, quantity: parseQty(quantities[item.id]) })) .filter((l) => l.quantity > 0) async function handleSubmit() { if (picked.length === 0) { toast({ title: t('invoice_nothing_selected'), variant: 'destructive' }) return } setIsSubmitting(true) try { const response = await fetch(`/api/sales-orders/${order.id}/create-invoice`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lines: picked, ...(invoiceDate ? { invoice_date: invoiceDate } : {}), ...(dueDate ? { due_date: dueDate } : {}), }), }) const json = await response.json().catch(() => null) if (!response.ok) { toast({ title: t('invoice_failed_title'), description: getErrorMessage(json, { locale: errorLocale, statusCode: response.status }), variant: 'destructive', }) return } const invoiceId: string = json.invoice_id toast({ title: t('invoice_created_title'), description: t('invoice_created_description'), action: ( router.push(`/invoices/${invoiceId}`)}> {t('open_invoice')} ), }) onCreated(json.data.order as SalesOrder, invoiceId) onOpenChange(false) } catch (err) { toast({ title: t('invoice_failed_title'), description: getErrorMessage(err, { locale: errorLocale }), variant: 'destructive', }) } finally { setIsSubmitting(false) } } return ( !isSubmitting && onOpenChange(v)}> {t('invoice_dialog_title')} {t('invoice_dialog_description')}
{lines.map((item) => ( ))}
{t('th_description')} {t('th_remaining_qty')} {t('th_invoice_qty')}
{item.description} {formatQty(remainingOf(item))} {item.unit} setQuantities((prev) => ({ ...prev, [item.id]: e.target.value })) } aria-label={`${t('th_invoice_qty')}: ${item.description}`} className="ml-auto h-8 w-24 text-right tabular-nums" />
setInvoiceDate(e.target.value)} className="tabular-nums" />
setDueDate(e.target.value)} className="tabular-nums" />
) }