feat(expenses): utlägg as an answer to "Vem betalade?" in Underlag, not a page (#2317)

An out-of-pocket purchase differs from any other receipt only in the
credit account, so the Underlag pane now asks one question for an
unmatched underlag (Företaget / Jag, privat / En anställd / Ingen ännu)
and books a privately paid receipt in place through POST
/api/expense-claims, replacing the "Andra sätt att bokföra" dropdown and
the deep link into the two-step wizard. The verifikat editor stays
reachable below as the escape hatch (BFL 5 kap 6-7 §).

The person owed surfaces in Att göra under a new Betala band, one row per
person (lib/worklist expense_payout, counted in the total and exposed to
agents through the attention resource). The Utlägg nav row is gated on
existing claims, the same hybrid gate as Körjournal, since the entry
point for a new utlägg is now the Underlag pane.


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:
Jakob Wennberg
2026-09-05 15:57:14 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent bff44e5757
commit cbe5580886
17 changed files with 866 additions and 86 deletions
+16 -3
View File
@@ -3,7 +3,12 @@ import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
import AttGoraSection from '@/components/dashboard/AttGoraSection'
import ResumePane from '@/components/dashboard/ResumePane'
import { HemNotices } from '@/components/dashboard/HemNotices'
import { getWorklistCounts, listSuggestedMatches, SUGGESTED_MATCH_SCAN_CAP } from '@/lib/worklist'
import {
getWorklistCounts,
listExpensePayoutsDue,
listSuggestedMatches,
SUGGESTED_MATCH_SCAN_CAP,
} from '@/lib/worklist'
import { listResumeItems } from '@/lib/worklist/resume'
import { getCompanyNotices } from '@/lib/notices'
import { expiringBankConnectionsFrom } from '@/lib/notices/categories'
@@ -191,12 +196,19 @@ export async function HemPanesSection({
// the worklist count is the list's length (it used to scan the same rows
// twice). Everything else runs in the same wave.
const suggestedMatchesPromise = listSuggestedMatches(supabase, companyId, SUGGESTED_MATCH_SCAN_CAP)
const [worklist, suggestedMatches, resumeItems, bankConnectionsRes, postedEntries] =
// Same pattern for people owed for utlägg: Hem renders one row per person
// and the worklist count is the list's length.
const expensePayoutsPromise = listExpensePayoutsDue(supabase, companyId)
const [worklist, suggestedMatches, expensePayouts, resumeItems, bankConnectionsRes, postedEntries] =
await Promise.all([
// Pending-work counts come from lib/worklist: the same source as the
// sidebar badges, so the numbers can never diverge.
getWorklistCounts(supabase, companyId, { suggestedMatches: suggestedMatchesPromise }),
getWorklistCounts(supabase, companyId, {
suggestedMatches: suggestedMatchesPromise,
expensePayoutsDue: expensePayoutsPromise,
}),
suggestedMatchesPromise,
expensePayoutsPromise,
// In-progress work for the Fortsätt pane: pure draft-state derivation.
listResumeItems(supabase, companyId, now),
supabase.from('bank_connections').select('id, status, consent_expires, bank_name, last_sie_sweep').eq('company_id', companyId).eq('status', 'active'),
@@ -229,6 +241,7 @@ export async function HemPanesSection({
<AttGoraSection
worklist={worklist}
suggestedMatches={suggestedMatches.slice(0, 5)}
expensePayouts={expensePayouts}
expiringBankConnections={expiringBankConnections}
emptyLedger={emptyLedger}
hasActiveBankConnection={hasActiveBankConnection}
+2
View File
@@ -301,6 +301,7 @@ export default async function DashboardLayout({
])
const hasWebshop = navFlags.hasWebshop
const hasMileageTrips = navFlags.hasMileageTrips
const hasExpenseClaims = navFlags.hasExpenseClaims
const canonicalDomain = (() => {
try {
@@ -591,6 +592,7 @@ export default async function DashboardLayout({
salesOrdersEnabled={salesOrdersEnabled}
hasWebshop={hasWebshop}
hasMileage={hasMileage}
hasExpenseClaims={hasExpenseClaims}
isSandbox={isSandbox}
extensionNavItems={getExtensionNavItems()}
userName={userProfile?.full_name ?? null}