- The Utlägg nav row is computed server-side, so the first booked claim now refreshes the App Router tree instead of staying hidden until a full reload. - The dialog's default date is the local calendar date; toISOString() is UTC and dated a receipt booked after midnight CEST to the previous day. - listExpensePayoutsDue pages through every registered claim with fetchAllRows instead of stopping at 500 rows: a person omitted or a total understated there is money the company owes someone. - The attention resource's payout instruction names the liability account per row (2893 / 2018 / 2820) instead of only 2893/2820. Claude-Session: https://claude.ai/code/session_01P8YsvPqjfGxGZUkGeBVUWQ Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5.1
parent
eb2ae1da17
commit
41a5728ca7
@@ -3,6 +3,7 @@
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
||||
import { useCompanySettings } from '@/lib/reference-data/hooks'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
@@ -495,6 +496,9 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
const [bookDirectOpen, setBookDirectOpen] = useState(false)
|
||||
// "Vem betalade?" answered with a person: the utlägg confirm step.
|
||||
const [registerExpensePayer, setRegisterExpensePayer] = useState<ExpensePayer | null>(null)
|
||||
// The layout computes the Utlägg nav gate (hasExpenseClaims) on the server;
|
||||
// the first booked claim must re-run it or the row stays hidden until reload.
|
||||
const router = useRouter()
|
||||
// Bulk-book selected underlag (Modell B): the "Bokför valda" selection-bar
|
||||
// action. The dialog filters the selection to bookable items itself.
|
||||
const [bulkBookOpen, setBulkBookOpen] = useState(false)
|
||||
@@ -2244,6 +2248,7 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
payer={registerExpensePayer}
|
||||
onSuccess={async () => {
|
||||
await Promise.all([fetchItems(), handleSelect(selected.id)])
|
||||
router.refresh()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -62,7 +62,12 @@ interface Props {
|
||||
const OWNER_FALLBACK_NAME = 'Ägare'
|
||||
|
||||
function todayIso(): string {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
// Local calendar date: toISOString() is UTC and would date a receipt booked
|
||||
// after midnight CEST to the previous day (wrong period, wrong FX rate).
|
||||
const now = new Date()
|
||||
const mm = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(now.getDate()).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${mm}-${dd}`
|
||||
}
|
||||
|
||||
function parseAmount(raw: string): number {
|
||||
|
||||
@@ -439,7 +439,7 @@ export const attentionResource: McpResource = {
|
||||
})),
|
||||
next: {
|
||||
description:
|
||||
'Betala ut från företagskontot och bokför utbetalningen (2893/2820 D mot 19xx K) via /expenses eller POST /api/expense-claims/payouts.',
|
||||
'Betala ut från företagskontot och bokför utbetalningen: debitera radens liability_account (2893 ägare i AB, 2018 ägare i enskild firma, 2820 anställd) mot 19xx K, via /expenses eller POST /api/expense-claims/payouts.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
+24
-21
@@ -12,6 +12,7 @@ import { OPEN_ROT_RUT_PAYOUT_STATUSES } from '@/lib/invoices/rot-rut-payout-matc
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { roundOre } from '@/lib/money'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import {
|
||||
MATCHABLE_INVOICE_STATUSES,
|
||||
MATCHABLE_SUPPLIER_INVOICE_STATUSES,
|
||||
@@ -564,13 +565,6 @@ export async function countReconciliationDue(
|
||||
return keys.filter((k) => !coveredKeys.has(k)).length
|
||||
}
|
||||
|
||||
/**
|
||||
* Upper bound on registered-claim rows scanned per company. Claims are
|
||||
* marked paid in batches, so a backlog beyond this is pathological; the
|
||||
* list clamps rather than paginating on every home render.
|
||||
*/
|
||||
export const EXPENSE_PAYOUT_SCAN_CAP = 500
|
||||
|
||||
/**
|
||||
* People owed for registered, unpaid utlägg, newest debt last. The canonical
|
||||
* "att betala ut" predicate: expense_claims.status = 'registered'. Grouped
|
||||
@@ -581,25 +575,34 @@ export async function listExpensePayoutsDue(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
): Promise<ExpensePayoutDue[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('expense_claims')
|
||||
.select('employee_id, claimant_name, liability_account, amount_sek, expense_date')
|
||||
.eq('company_id', companyId)
|
||||
.eq('status', 'registered')
|
||||
.order('expense_date', { ascending: true })
|
||||
.limit(EXPENSE_PAYOUT_SCAN_CAP)
|
||||
if (error) {
|
||||
logAndZero('expense_payout', companyId, error)
|
||||
return []
|
||||
}
|
||||
const byPerson = new Map<string, ExpensePayoutDue>()
|
||||
for (const row of (data ?? []) as Array<{
|
||||
type ClaimRow = {
|
||||
id?: string
|
||||
employee_id: string | null
|
||||
claimant_name: string
|
||||
liability_account: string
|
||||
amount_sek: number | string
|
||||
expense_date: string
|
||||
}>) {
|
||||
}
|
||||
let rows: ClaimRow[]
|
||||
try {
|
||||
// Every registered claim, paginated past the PostgREST row cap: a person
|
||||
// omitted or a total understated here is money the company owes someone.
|
||||
rows = await fetchAllRows<ClaimRow>(({ from, to }) =>
|
||||
supabase
|
||||
.from('expense_claims')
|
||||
.select('id, employee_id, claimant_name, liability_account, amount_sek, expense_date')
|
||||
.eq('company_id', companyId)
|
||||
.eq('status', 'registered')
|
||||
.order('expense_date', { ascending: true })
|
||||
.order('id', { ascending: true })
|
||||
.range(from, to),
|
||||
)
|
||||
} catch (err) {
|
||||
logAndZero('expense_payout', companyId, err as { message?: string })
|
||||
return []
|
||||
}
|
||||
const byPerson = new Map<string, ExpensePayoutDue>()
|
||||
for (const row of rows) {
|
||||
const key = row.employee_id ?? `owner:${row.claimant_name}`
|
||||
const amount = Number(row.amount_sek) || 0
|
||||
const existing = byPerson.get(key)
|
||||
|
||||
Reference in New Issue
Block a user