diff --git a/components/extensions/general/BookDirectlyDialog.tsx b/components/extensions/general/BookDirectlyDialog.tsx index 1151583d..10e5ddbc 100644 --- a/components/extensions/general/BookDirectlyDialog.tsx +++ b/components/extensions/general/BookDirectlyDialog.tsx @@ -14,8 +14,9 @@ import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { useToast } from '@/components/ui/use-toast' -import { Loader2, Plus, Trash2, AlertTriangle, Search, Check, BookmarkPlus } from 'lucide-react' +import { Loader2, Plus, Trash2, Search, Check, BookmarkPlus } from 'lucide-react' import { cn, formatCurrency } from '@/lib/utils' +import { roundOre } from '@/lib/money' import AccountCombobox from '@/components/bookkeeping/AccountCombobox' import { loadBasCatalog, type CatalogAccount } from '@/lib/bookkeeping/bas-catalog-client' import DocumentViewerPane from '@/components/bookkeeping/DocumentViewerPane' @@ -533,6 +534,37 @@ export default function BookDirectlyDialog({ open, onOpenChange, item, docUrl = setLines((prev) => prev.length <= 2 ? prev : prev.filter((_, i) => i !== idx)) }, []) + // Outstanding imbalance from every line except `excludeIndex`. + // Positive => debit side is short (a debit on the target row balances it); + // negative => credit side is short. Same semantics as JournalEntryForm. + const computeBalancingDiff = useCallback( + (excludeIndex: number) => { + const others = lines.filter((_, i) => i !== excludeIndex) + const d = others.reduce((sum, l) => sum + (parseFloat(l.debit_amount) || 0), 0) + const c = others.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0) + return roundOre(c - d) + }, + [lines] + ) + + // Opt-in balancing (ported from JournalEntryForm): double-click a debit or + // credit field to fill the amount that makes the entry balance. No-op if + // already balanced or if the balancing entry belongs on the other side. + const handleFillBalance = useCallback( + (idx: number, side: 'debit' | 'credit') => { + const diff = computeBalancingDiff(idx) + const fill = side === 'debit' ? diff : -diff + if (fill <= 0) return + updateLine( + idx, + side === 'debit' + ? { debit_amount: fill.toFixed(2), credit_amount: '' } + : { credit_amount: fill.toFixed(2), debit_amount: '' } + ) + }, + [computeBalancingDiff, updateLine] + ) + // Replace the line set with a booking template's computed rows. The picker // hands back JournalEntryForm-shaped lines; we keep only the three fields // book-direct posts. A meaningful supplier description is preserved: the @@ -879,6 +911,7 @@ export default function BookDirectlyDialog({ open, onOpenChange, item, docUrl = inputMode="decimal" value={line.debit_amount} onChange={(e) => updateLine(idx, { debit_amount: e.target.value, credit_amount: e.target.value ? '' : line.credit_amount })} + onDoubleClick={() => handleFillBalance(idx, 'debit')} disabled={isSubmitting} className="text-right tabular-nums" placeholder="0,00" @@ -891,6 +924,7 @@ export default function BookDirectlyDialog({ open, onOpenChange, item, docUrl = inputMode="decimal" value={line.credit_amount} onChange={(e) => updateLine(idx, { credit_amount: e.target.value, debit_amount: e.target.value ? '' : line.debit_amount })} + onDoubleClick={() => handleFillBalance(idx, 'credit')} disabled={isSubmitting} className="text-right tabular-nums" placeholder="0,00" @@ -914,13 +948,36 @@ export default function BookDirectlyDialog({ open, onOpenChange, item, docUrl = - - Summa + +
+ {/* The remaining debit/credit gap, right where the user + reconciles the sums: what is still missing to balance. */} + {totals.diff !== 0 ? ( + + Differens {Math.abs(totals.diff).toFixed(2)} + + ) : ( + + )} + + Summa + +
- + {totals.debit.toFixed(2)} - + {totals.credit.toFixed(2)} @@ -969,12 +1026,11 @@ export default function BookDirectlyDialog({ open, onOpenChange, item, docUrl = Balanserad - ) : ( - - - Diff {totals.diff.toFixed(2)} + ) : totals.diff !== 0 ? ( + + Dubbelklicka i ett tomt beloppsfält för att fylla i differensen - )} + ) : null}