chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- 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
Claude Fable 5.1
Jakob Wennberg
parent
18cbc4c30a
commit
f266c386f3
@@ -1,137 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useCashAccounts } from '@/lib/reference-data/hooks'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'Accounted:cash-account:'
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Current selection: a BAS ledger account number ('1930', '1932', …).
|
||||
* `null` would only be meaningful if "all accounts" were an option, which
|
||||
* isn't currently supported (reconciliation is always single-account).
|
||||
*/
|
||||
value: string
|
||||
onChange: (accountNumber: string) => void
|
||||
/**
|
||||
* Optional label above the select. Pass null to render without a label.
|
||||
*/
|
||||
label?: string | null
|
||||
/**
|
||||
* Called once after the initial fetch completes so callers can suppress a
|
||||
* skeleton until the selector is ready.
|
||||
*/
|
||||
onReady?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Cash account selector for reconciliation, drift, and any UI that scopes a
|
||||
* read to a particular settlement account (1930 SEK, 1932 EUR, …).
|
||||
*
|
||||
* Loads /api/cash-accounts for the active company, persists the last selection
|
||||
* per company in sessionStorage, and renders the same Select primitive as the
|
||||
* fiscal-year picker so the UX stays consistent.
|
||||
*
|
||||
* sessionStorage (not localStorage) so the selection clears when the tab/
|
||||
* session ends. The data is a UI preference, not a credential; persisting
|
||||
* which BAS account a company uses across sessions in browser storage would
|
||||
* couple company id + financial account reference for the lifetime of the
|
||||
* browser profile (GDPR Art. 25(2) data minimisation, ISO 27001 A.8.12).
|
||||
*/
|
||||
export function CashAccountSelector({
|
||||
value,
|
||||
onChange,
|
||||
label = 'Konto',
|
||||
onReady,
|
||||
className,
|
||||
}: Props) {
|
||||
const { company } = useCompany()
|
||||
// Session-cached and seeded by the dashboard layout (lib/reference-data):
|
||||
// on a normal visit the list is here on the first render, so the restore
|
||||
// below runs in the first effect tick and onReady fires without a round
|
||||
// trip. Restore once per company load, not on every background refresh.
|
||||
const { cashAccounts: accounts, isLoading } = useCashAccounts()
|
||||
const loaded = !isLoading
|
||||
const restoredForRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!company?.id) {
|
||||
onReady?.()
|
||||
return
|
||||
}
|
||||
if (!loaded || restoredForRef.current === company.id) return
|
||||
restoredForRef.current = company.id
|
||||
|
||||
// Restore last selection or pick the primary as default.
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = window.sessionStorage.getItem(STORAGE_KEY_PREFIX + company.id)
|
||||
const inFetched = (ledger: string) =>
|
||||
accounts.some(a => a.ledger_account === ledger)
|
||||
|
||||
if (stored && inFetched(stored)) {
|
||||
if (stored !== value) onChange(stored)
|
||||
} else {
|
||||
const primary = accounts.find(a => a.is_primary)
|
||||
const fallback = primary ?? accounts[0]
|
||||
if (fallback && fallback.ledger_account !== value) {
|
||||
onChange(fallback.ledger_account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReady?.()
|
||||
// onReady/onChange are lifecycle callbacks: fire once per load, not on
|
||||
// parent re-renders that re-create them. `value` is read once at restore.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [company?.id, loaded, accounts])
|
||||
|
||||
const handleChange = (next: string) => {
|
||||
if (company?.id && typeof window !== 'undefined') {
|
||||
window.sessionStorage.setItem(STORAGE_KEY_PREFIX + company.id, next)
|
||||
}
|
||||
onChange(next)
|
||||
}
|
||||
|
||||
// Fallback when the table is empty (fresh company, no PSD2 connections yet):
|
||||
// show a single hardcoded '1930' option so the rest of the UI still works.
|
||||
const options = accounts.length > 0
|
||||
? accounts.map(a => ({
|
||||
value: a.ledger_account,
|
||||
label: `${a.ledger_account} ${a.name ?? a.iban ?? a.currency}`,
|
||||
}))
|
||||
: [{ value: '1930', label: '1930 Bankkonto' }]
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label && <Label>{label}</Label>}
|
||||
<div className={`flex items-center gap-2 ${label ? 'mt-1' : ''}`}>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={handleChange}
|
||||
disabled={!loaded && accounts.length === 0}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[280px]">
|
||||
<SelectValue placeholder={loaded ? 'Välj konto' : 'Laddar…'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map(o => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DataStateProps {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
/** When true (and not loading/error), render `empty` instead of `children`. */
|
||||
isEmpty?: boolean
|
||||
/** Retry handler: typically `useFetch().refetch`. Shows a retry button. */
|
||||
onRetry?: () => void
|
||||
/** Loading placeholder. Defaults to three skeleton rows. */
|
||||
skeleton?: React.ReactNode
|
||||
/** Shown when `isEmpty`. Pass an `EmptyState` / preset (e.g. `<EmptyInvoices />`). */
|
||||
empty?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the loading / error / empty / ready states for a data-driven section,
|
||||
* so callers stop hand-rolling that branch every time.
|
||||
*
|
||||
* Pairs with `useFetch`:
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error, refetch } = useFetch<Account[]>(url, { select: b => b.data })
|
||||
* return (
|
||||
* <DataState
|
||||
* loading={loading}
|
||||
* error={error}
|
||||
* onRetry={refetch}
|
||||
* isEmpty={!data?.length}
|
||||
* empty={<EmptyState title={t('none_title')} description={t('none_desc')} />}
|
||||
* >
|
||||
* <AccountsTable accounts={data!} />
|
||||
* </DataState>
|
||||
* )
|
||||
*
|
||||
* Loading uses the `Skeleton` primitive; empty expects an `EmptyState`; the
|
||||
* error branch uses the only chrome-permitted semantic colour (`destructive`).
|
||||
*/
|
||||
export function DataState({
|
||||
loading,
|
||||
error,
|
||||
isEmpty = false,
|
||||
onRetry,
|
||||
skeleton,
|
||||
empty,
|
||||
children,
|
||||
className,
|
||||
}: DataStateProps) {
|
||||
const t = useTranslations('common')
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{skeleton ?? (
|
||||
<div className="space-y-3" aria-busy="true" aria-live="polite">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-3/4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}
|
||||
>
|
||||
<AlertCircle className="h-8 w-8 text-destructive mb-3" aria-hidden="true" />
|
||||
<p className="text-sm font-medium mb-1">{t('load_error')}</p>
|
||||
<p className="text-sm text-muted-foreground max-w-sm mb-6 text-balance">{error}</p>
|
||||
{onRetry && (
|
||||
<Button variant="outline" onClick={onRetry}>
|
||||
{t('retry')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
return <div className={className}>{empty}</div>
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
Reference in New Issue
Block a user