Files
accounted/components/transactions/BatchCategorySelector.tsx
T
e51a2c8102 refactor(design): no amber boxes, attention is one ochre sentence (#1562)
The founder wants the yellow boxes gone everywhere. The design system
already agreed: status colors are data, not chrome (convention 12) and
attention is a single ochre sentence, never a banner (convention 6).
This enforces it:

- ConfirmationDialog: the amber warning panel is now an AttnLine, and
  the hardcoded Swedish default warningText is gone: it injected an
  immutability warning into dialogs whose authors never asked for one
  (every current caller passes the prop explicitly, so no behavior
  change at any call site).
- Badge warning variant: amber fill replaced with a hairline chip and
  ochre text.
- DestructiveConfirmDialog warning variant: neutral icon disc, default
  primary confirm button (only --destructive survives as chrome).
- BankSyncStatusChip stale state: same neutral shape as the healthy
  chip, ochre text carries the signal.
- SandboxBanner: solid amber bar becomes secondary-on-border chrome.
- BankIdAuth, BankIdCompanyPicker, SessionTimeoutModal: the last three
  raw-amber (bg-amber-*) holdouts moved onto tokens, the company-picker
  banner becoming a plain AttnLine.
- Mechanical sweep of the ~58 hand-rolled bg-warning/border-warning
  boxes across 45 files: fills to bg-muted/30 (icon discs bg-muted),
  borders to border-border, text-warning-foreground to text-attn. The
  account-class dots in account-number.tsx keep bg-warning: they are
  data indicators, not chrome.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:20:16 +02:00

129 lines
4.9 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Progress } from '@/components/ui/progress'
import { Paperclip } from 'lucide-react'
import VatTreatmentSelect from './VatTreatmentSelect'
import { EXPENSE_CATEGORIES, INCOME_CATEGORIES } from './transaction-types'
import type { TransactionCategory, VatTreatment } from '@/types'
const expenseCategories = EXPENSE_CATEGORIES
const incomeCategories = INCOME_CATEGORIES
interface BatchCategorySelectorProps {
open: boolean
onOpenChange: (open: boolean) => void
selectedCount: number
onSelectCategory: (category: TransactionCategory, vatTreatment?: VatTreatment) => void
progress: { done: number; total: number } | null
}
export default function BatchCategorySelector({
open,
onOpenChange,
selectedCount,
onSelectCategory,
progress,
}: BatchCategorySelectorProps) {
const t = useTranslations('tx_batch_selector')
const tCat = useTranslations('tx_categories')
// 'auto' sends no explicit treatment: the server derives the correct default
// per category (exempt for bank/card fees, 12% representation, else 25%).
// A hardcoded 'standard_25' initial value used to override that derivation
// and claim 25% moms on VAT-exempt bank fees.
const [vatTreatment, setVatTreatment] = useState<VatTreatment | 'none' | 'auto'>('auto')
const isProcessing = progress !== null
const handleSelectCategory = (category: TransactionCategory) => {
// 'auto' omits the treatment so the server derives it per category. An
// explicit 'Ingen moms' goes over the wire as 'exempt' (books no VAT
// line): the old collapse to undefined made an explicit no-VAT choice
// book the DERIVED default, i.e. 25% moms on most expense categories.
const resolvedVat =
vatTreatment === 'auto' ? undefined : vatTreatment === 'none' ? 'exempt' : vatTreatment
onSelectCategory(category, resolvedVat)
}
return (
<Dialog open={open} onOpenChange={isProcessing ? undefined : onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
{isProcessing
? t('title_processing', { done: progress.done, total: progress.total })
: t('title_default', { count: selectedCount })}
</DialogTitle>
<DialogDescription>
{isProcessing
? t('description_processing')
: t('description_default')}
</DialogDescription>
</DialogHeader>
{isProcessing ? (
<div className="py-4">
<Progress value={(progress.done / progress.total) * 100} />
<p className="text-sm text-muted-foreground mt-2 text-center">
{t('progress_label', { done: progress.done, total: progress.total })}
</p>
</div>
) : (
<div className="space-y-4 py-2">
{/* Underlag reminder */}
<div className="flex items-start gap-2 rounded-lg bg-muted/30 border border-border p-3">
<Paperclip className="h-4 w-4 text-attn mt-0.5 shrink-0" />
<p className="text-xs text-attn">
{t('underlag_reminder')}
</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('vat_label')}</h4>
<VatTreatmentSelect
value={vatTreatment}
onValueChange={setVatTreatment}
allowAuto
/>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('expenses_label')}</h4>
<div className="grid grid-cols-2 gap-1.5">
{expenseCategories.map((cat) => (
<Button
key={cat.value}
variant="outline"
size="sm"
className="justify-start text-xs"
onClick={() => handleSelectCategory(cat.value)}
>
{tCat(cat.labelKey)}
</Button>
))}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-2">{t('income_label')}</h4>
<div className="grid grid-cols-2 gap-1.5">
{incomeCategories.map((cat) => (
<Button
key={cat.value}
variant="outline"
size="sm"
className="justify-start text-xs"
onClick={() => handleSelectCategory(cat.value)}
>
{tCat(cat.labelKey)}
</Button>
))}
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
)
}