Files
accounted/components/deadlines/DeadlineGroupCard.tsx
T
Jakob Wennberg bc5c673f7d fix(ui): reach touch-only actions, confirm before posting, drop a dead ring (#1280)
* fix(ui): reach touch-only actions, confirm before posting, drop a dead ring

Three defects from the UI craft audit where the interface is wrong, not
just inconsistent.

Unreachable on touch: 'Markera klar' on deadlines and the bulk-select
checkbox on /pending were 'opacity-0 group-hover:opacity-100'. Coarse
pointers never fire hover and DeadlineForm has no completion control, so
on a phone there was no way to mark a deadline done at all. Factored the
reveal into HOVER_REVEAL_CLASS, which adds pointer-coarse:opacity-100.

Unguarded ledger writes: 'Bokför' on the journal-entry detail page and on
the invoice detail page posted an immutable verifikat on one click, one
screen after the list confirmed the identical action. Both now open a
ConfirmDialog describing the outcome up front (convention 10), reusing
the list's indicative voucher-number prefetch.

Dead ring: SummaryCard emitted ring-1 ring-primary/40 and ring-1
ring-warning/40 on the same element, so the two set the same custom
property and one silently lost. The override is an exception, so it is a
Badge now (conventions 5 and 12).

Also corrects the design.md primitives table, which named ui/table.tsx as
the data-table primitive while 17 files use ui/dry-table.tsx; building a
list page from the documented row produced ~15% taller rows and a
different hover tint, which had already happened twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): let the post-confirm dialog close after a successful commit

ConfirmDialog calls onOpenChange(false) immediately after `await onConfirm()`
resolves, but handleCommit/handleBook clear their in-flight flag in a
`finally` block, so the guard's closure still saw isCommitting/isUpdating as
true and swallowed the close: the dialog would sit open over a booking that
had already succeeded.

The guard was redundant as well as wrong. ConfirmDialog already blocks
Radix-initiated closes while pending, via `onOpenChange={(next) => !pending
&& onOpenChange(next)}` on the Dialog itself. Passing the setter directly
matches every other dialog on both pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:33:36 +02:00

112 lines
3.8 KiB
TypeScript

'use client'
import { useTranslations } from 'next-intl'
import { Deadline } from '@/types'
import { cn } from '@/lib/utils'
import { isDeadlineOverdue } from '@/lib/calendar/utils'
import { HOVER_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { deadlineDateLabel } from './DeadlineRow'
import { Pencil } from 'lucide-react'
/**
* System tax deadlines that legally share the skattekonto date ("den 12:e"):
* for a small monthly-moms employer, moms + AGI (+ debiterad preliminärskatt)
* all fall due the same day. Rendering them as one grouped row instead of
* 2-3 identical-date rows keeps the list scannable.
*/
export const SKATTEKONTO_GROUP_TYPES = new Set([
'moms_monthly',
'moms_quarterly',
'moms_yearly',
'f_skatt',
'arbetsgivardeklaration',
'skatteinbetalning',
])
export function isSkattekontoDeadline(d: Deadline): boolean {
return (
!d.is_completed &&
d.source === 'system' &&
d.tax_deadline_type !== null &&
SKATTEKONTO_GROUP_TYPES.has(d.tax_deadline_type)
)
}
interface DeadlineGroupRowProps {
/** Two or more skattekonto deadlines sharing the same due_date. */
deadlines: Deadline[]
onEdit?: (deadline: Deadline) => void
/** "Markera klar": the list confirms before toggling. */
onRequestToggle: (deadline: Deadline) => void
}
/**
* One thread row for all skattekonto obligations on a shared due date
* (concept scene 17 row language): the date renders once, each obligation is
* a sub-row with its own quiet "Markera klar".
*/
export function DeadlineGroupCard({ deadlines, onEdit, onRequestToggle }: DeadlineGroupRowProps) {
const t = useTranslations('deadlines')
const first = deadlines[0]
const overdue = deadlines.some(isDeadlineOverdue)
return (
<div className="flex items-start gap-3 py-3 -mx-2 px-2">
{/* Shared skattekonto date: always Skatteverket's mark (white ground,
badge not chrome). */}
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-white">
<img src="/logos/skatteverket_color.svg" alt="Skatteverket" className="h-4 w-4 object-contain" />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm leading-6">
<span className="tabular-nums text-muted-foreground">
{deadlineDateLabel(first.due_date)}
</span>
<span className="text-muted-foreground/50"> · </span>
<span>{t('group_skattekonto_title')}</span>
</p>
<p
className={cn(
'text-xs leading-5',
overdue ? 'text-destructive' : 'text-muted-foreground',
)}
>
{t('group_same_day', { count: deadlines.length })}
</p>
<div className="mt-1">
{deadlines.map((deadline) => (
<div
key={deadline.id}
onClick={() => onEdit?.(deadline)}
className={cn(
'group flex items-center gap-3 rounded-md py-1.5 -mx-2 px-2 transition-colors duration-150',
onEdit && 'cursor-pointer hover:bg-secondary/35',
)}
>
<p className="min-w-0 flex-1 truncate text-sm">{deadline.title}</p>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onRequestToggle(deadline)
}}
className={cn(QUIET_LINK_CLASS, HOVER_REVEAL_CLASS, 'shrink-0')}
>
{t('group_mark_done')}
</button>
{onEdit && (
<Pencil
className="h-3.5 w-3.5 shrink-0 text-muted-foreground/0 transition-colors group-hover:text-muted-foreground/50"
aria-hidden="true"
/>
)}
</div>
))}
</div>
</div>
</div>
)
}