feat(inbox): visible differens + double-click balance fill in Bokför direkt (#1544)
* feat(inbox): show differens in Bokfor direkt sum row + double-click balance fill User feedback: when hand-computing VAT the unbalanced amount had to be worked out manually; the existing diff indicator was tiny, muted and below the fold. Now the remaining debit/credit gap renders red in the Summa row (and the sums turn red) while unbalanced, matching JournalEntryForm. Also ports JournalEntryForm's opt-in balancing: double-clicking a debit or credit field fills the amount that balances the entry (no-op when balanced or when the fill belongs on the other side), with a hint line replacing the old indicator. Display-only + client-side prefill; the engine still rejects unbalanced entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(inbox): use roundOre for balancing diff per antipattern guard Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
27383fd02b
commit
a07dcf4a55
@@ -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 =
|
||||
</tbody>
|
||||
<tfoot className="bg-muted/20 text-xs">
|
||||
<tr>
|
||||
<td className="px-3 py-2 text-right font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Summa
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{/* The remaining debit/credit gap, right where the user
|
||||
reconciles the sums: what is still missing to balance. */}
|
||||
{totals.diff !== 0 ? (
|
||||
<span className="tabular-nums font-medium text-destructive">
|
||||
Differens {Math.abs(totals.diff).toFixed(2)}
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<span className="text-right font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Summa
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums font-medium">
|
||||
<td
|
||||
className={cn(
|
||||
'px-3 py-2 text-right tabular-nums font-medium',
|
||||
totals.diff !== 0 && 'text-destructive'
|
||||
)}
|
||||
>
|
||||
{totals.debit.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums font-medium">
|
||||
<td
|
||||
className={cn(
|
||||
'px-3 py-2 text-right tabular-nums font-medium',
|
||||
totals.diff !== 0 && 'text-destructive'
|
||||
)}
|
||||
>
|
||||
{totals.credit.toFixed(2)}
|
||||
</td>
|
||||
<td />
|
||||
@@ -969,12 +1026,11 @@ export default function BookDirectlyDialog({ open, onOpenChange, item, docUrl =
|
||||
<Badge variant="success" className="text-[11px]">
|
||||
Balanserad
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1.5 tabular-nums">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-warning" />
|
||||
Diff {totals.diff.toFixed(2)}
|
||||
) : totals.diff !== 0 ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Dubbelklicka i ett tomt beloppsfält för att fylla i differensen
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user