ea1bf01f1e
* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
2.5 KiB
TypeScript
88 lines
2.5 KiB
TypeScript
import type { JournalEntryLine } from '@/types'
|
||
|
||
export interface CorrectionLineInput {
|
||
account_number: string
|
||
debit_amount: string | number
|
||
credit_amount: string | number
|
||
}
|
||
|
||
export interface AccountRow {
|
||
account_number: string
|
||
original: number
|
||
storno: number
|
||
correction: number
|
||
delta: number
|
||
}
|
||
|
||
function toNumber(v: string | number | null | undefined): number {
|
||
if (v == null) return 0
|
||
const n = typeof v === 'string' ? parseFloat(v) : v
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
function round2(n: number): number {
|
||
return Math.round(n * 100) / 100
|
||
}
|
||
|
||
/**
|
||
* Build per-account diff rows: original net, storno (= −original), proposed
|
||
* correction net, and förändring (= storno + correction = correction − original).
|
||
*
|
||
* Net per row is debit − credit. Accounts appearing only on one side still get
|
||
* a row, so the user sees account swaps clearly (old account drains to zero,
|
||
* new account picks up the value).
|
||
*
|
||
* Corrected lines with account_number.length !== 4 are skipped — those are
|
||
* incomplete user input mid-edit, not real proposals.
|
||
*/
|
||
export function buildCorrectionRows(
|
||
original: JournalEntryLine[],
|
||
corrected: CorrectionLineInput[]
|
||
): AccountRow[] {
|
||
const map = new Map<string, AccountRow>()
|
||
|
||
const ensure = (acc: string): AccountRow => {
|
||
let row = map.get(acc)
|
||
if (!row) {
|
||
row = { account_number: acc, original: 0, storno: 0, correction: 0, delta: 0 }
|
||
map.set(acc, row)
|
||
}
|
||
return row
|
||
}
|
||
|
||
for (const line of original) {
|
||
if (!line.account_number) continue
|
||
const net = toNumber(line.debit_amount) - toNumber(line.credit_amount)
|
||
const row = ensure(line.account_number)
|
||
row.original += net
|
||
row.storno -= net
|
||
}
|
||
|
||
for (const line of corrected) {
|
||
if (!line.account_number || line.account_number.length !== 4) continue
|
||
const net = toNumber(line.debit_amount) - toNumber(line.credit_amount)
|
||
const row = ensure(line.account_number)
|
||
row.correction += net
|
||
}
|
||
|
||
for (const row of map.values()) {
|
||
row.original = round2(row.original)
|
||
row.storno = round2(row.storno)
|
||
row.correction = round2(row.correction)
|
||
row.delta = round2(row.storno + row.correction)
|
||
}
|
||
|
||
return Array.from(map.values()).sort((a, b) =>
|
||
a.account_number.localeCompare(b.account_number)
|
||
)
|
||
}
|
||
|
||
export function formatSignedAmount(n: number): string {
|
||
if (n === 0) return '–'
|
||
const abs = Math.abs(n).toLocaleString('sv-SE', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})
|
||
return n > 0 ? `+${abs}` : `−${abs}`
|
||
}
|