Files
accounted/components/transactions/ExpensePayoutMatchDialog.tsx
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

91 lines
3.4 KiB
TypeScript

'use client'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { formatCurrency, formatDate } from '@/lib/utils'
import { Loader2 } from 'lucide-react'
import type { TransactionWithInvoice } from './transaction-types'
interface ExpensePayoutMatchDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
/** Row carrying `potential_expense_payout`; the dialog renders nothing without it. */
transaction: TransactionWithInvoice | null
isConfirming: boolean
onConfirm: () => void
}
/**
* Confirm dialog for booking an outgoing bank row as the repayment of one
* person's registered utlägg: the person's liability account is debited,
* the row's cash account credited, the claims flip to paid and the row is
* linked to the voucher, all in one RPC call. Two legs, amount from the bank
* row, so everything to approve is known up front (convention 10).
*/
export default function ExpensePayoutMatchDialog({
open,
onOpenChange,
transaction,
isConfirming,
onConfirm,
}: ExpensePayoutMatchDialogProps) {
const t = useTranslations('tx_expense_payout_match')
const match = transaction?.potential_expense_payout ?? null
const currency = transaction?.currency || 'SEK'
const amount = transaction ? Math.abs(transaction.amount) : 0
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{t('title')}</DialogTitle>
<DialogDescription>{t('description')}</DialogDescription>
</DialogHeader>
{transaction && match && (
<div className="space-y-4">
<div className="rounded-lg border border-border p-4 space-y-1">
<p className="font-medium">{transaction.description}</p>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground tabular-nums">{formatDate(transaction.date)}</span>
<span className="font-medium tabular-nums">{formatCurrency(-amount, currency)}</span>
</div>
</div>
<div className="rounded-lg border border-border p-4 space-y-1 text-sm">
<p className="font-medium">{match.claimant_name}</p>
<p className="text-muted-foreground">
{match.claim_count === 1
? t('claims_one', { date: formatDate(match.oldest_expense_date) })
: t('claims_other', { count: match.claim_count, date: formatDate(match.oldest_expense_date) })}
</p>
<p className="text-xs text-muted-foreground tabular-nums pt-2 border-t border-border">
{match.liability_account} D {formatCurrency(amount, 'SEK')} · 19xx K {formatCurrency(amount, 'SEK')}
</p>
<p className="text-xs text-muted-foreground">{t('outcome')}</p>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isConfirming}>
{t('cancel')}
</Button>
<Button onClick={onConfirm} disabled={isConfirming || !match}>
{isConfirming && <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />}
{t('confirm')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}