* 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>
82 lines
3.3 KiB
TypeScript
82 lines
3.3 KiB
TypeScript
import { createClient } from '@/lib/supabase/server'
|
|
import { NextResponse } from 'next/server'
|
|
import { requireCompanyId } from '@/lib/company/context'
|
|
|
|
const MAX_ROWS = 500
|
|
|
|
export async function GET(request: Request) {
|
|
const supabase = await createClient()
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const companyId = await requireCompanyId(supabase, user.id)
|
|
|
|
const { searchParams } = new URL(request.url)
|
|
const unmatched = searchParams.get('unmatched') === 'true'
|
|
const reconciled = searchParams.get('reconciled') === 'true'
|
|
const currency = searchParams.get('currency') || undefined
|
|
const dateFrom = searchParams.get('date_from') || undefined
|
|
const dateTo = searchParams.get('date_to') || undefined
|
|
// When set, return only ignored rows — used by the reconciliation view to
|
|
// surface a "Visa ignorerade" undo list. The default (no param) behaviour
|
|
// continues to exclude ignored rows from unmatched results.
|
|
const onlyIgnored = searchParams.get('only_ignored') === 'true'
|
|
// account_number is accepted for API symmetry with the reconciliation status
|
|
// endpoint; transactions don't carry a cash_account FK today (PSD2 account
|
|
// identity is embedded in external_id), so we use it to derive a default
|
|
// currency when the caller didn't supply one. Anything more precise needs
|
|
// the cash_account_id backfill tracked as Tier 4.
|
|
const accountNumberParam = searchParams.get('account_number') || undefined
|
|
|
|
let derivedCurrency = currency
|
|
if (!derivedCurrency && accountNumberParam) {
|
|
const { data: cashAccount } = await supabase
|
|
.from('cash_accounts')
|
|
.select('currency')
|
|
.eq('company_id', companyId)
|
|
.eq('ledger_account', accountNumberParam)
|
|
.maybeSingle()
|
|
if (cashAccount?.currency) derivedCurrency = cashAccount.currency as string
|
|
}
|
|
|
|
let query = supabase
|
|
.from('transactions')
|
|
.select('id, date, description, amount, currency, amount_sek, exchange_rate, reference, journal_entry_id, reconciliation_method, is_ignored')
|
|
.eq('company_id', companyId)
|
|
|
|
// unmatched and reconciled are mutually exclusive — unmatched wins if both set
|
|
if (unmatched) {
|
|
query = query.is('journal_entry_id', null)
|
|
// Hide rows the user has explicitly suppressed from the reconciliation
|
|
// view. Other callers (e.g. BookDirectlyDialog) also benefit — once
|
|
// ignored, the row stops surfacing in the "to book" funnel everywhere.
|
|
if (!onlyIgnored) query = query.eq('is_ignored', false)
|
|
} else if (reconciled) {
|
|
query = query.not('journal_entry_id', 'is', null)
|
|
}
|
|
|
|
if (onlyIgnored) query = query.eq('is_ignored', true)
|
|
|
|
if (derivedCurrency) query = query.eq('currency', derivedCurrency)
|
|
if (dateFrom) query = query.gte('date', dateFrom)
|
|
if (dateTo) query = query.lte('date', dateTo)
|
|
|
|
// Fetch one extra row so we can tell the caller whether the result was truncated.
|
|
query = query.order('date', { ascending: false }).limit(MAX_ROWS + 1)
|
|
|
|
const { data, error } = await query
|
|
|
|
if (error) {
|
|
return NextResponse.json({ error: error.message }, { status: 500 })
|
|
}
|
|
|
|
const rows = data || []
|
|
const hasMore = rows.length > MAX_ROWS
|
|
const truncated = hasMore ? rows.slice(0, MAX_ROWS) : rows
|
|
|
|
return NextResponse.json({ data: truncated, has_more: hasMore, limit: MAX_ROWS })
|
|
}
|