2bec5acedb
* feat(ui): concept scenes 17/24/32/33 for Viktiga datum, Skattekonto, Periodiseringar and Import (rest-of-nav 1) Viktiga datum: thread rows with type-icon circles behind a type seg (Alla/Skatt/Fakturering/Egna), Narmast countdown pane, Ny deadline lifted to the page header, both banners replaced by one AttnLine, mark-done via ConfirmDialog. DeadlineCard/DeadlineFilters die; DeadlineRow is the row. Skattekonto: card-less saldo hero with OCR + quiet copy, shortfall AttnLine computed from the next drain date with a betalningsuppgifter dialog (bankgiro 5050-1055 + OCR), one dry-table with Kommande/Forfallna/ Genomforda band rows, chips only on unbooked genomforda rows, quiet hover actions. Tabs and per-row badge noise are gone; the concept's Saldo column is dropped because SKV stores no per-row running balance. Periodiseringar: banner becomes an AttnLine with inline Bokfor forfallna (now confirm-first), house seg with Aktiva count, dry-table with muted normal states and animated RowFoldout for installments, Los upp nu as a quiet hover link through the shared ConfirmDialog. Import: tabs collapse into one two-column row list (Importera | Exportera) in the concept row language; SIE export moves into a small dialog and Molnsynkronisering folds the CloudBackupCard open in place. The /import?view=export#sie-export and /import#cloud-backup deep links keep working. Sandbox notice is an AttnLine. All four pages get stagger-enter, a help popover behind ? and sv+en keys for every new string. 9189 tests green, lint clean, guards pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(ui): align dashboard loading skeletons with the migrated page silhouettes Folds in the parallel WIP from this checkout at the founder's request: every loading.tsx under (dashboard) now mirrors its migrated page row-for-row (24px title block, pill actions, borderless table heads, single-line rows), and the shared (dashboard)/loading.tsx takes Hem's greeting + Att gora silhouette. Also lands the pending DECISIONS.md lines (onboarding swap plan note + rest-of-nav 1 deviations). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * polish(ui): authority logos, stat-tile skattekonto hero, quieter type on rest-of-nav 1 Viktiga datum: statutory deadlines wear the receiving authority's mark (Skatteverket for tax dates, Bolagsverket for arsredovisning/arsstamma) as a small badge on white; other deadlines keep the neutral type icons. Row dates go muted, titles drop font-medium, the Narmast countdown steps down to the house text-4xl display scale. Skattekonto: the 32px serif hero becomes two compact metric tiles in the KPIHeroCards idiom (saldo with the Skatteverket mark + OCR meta, nasta dragning with date and event count), matching how numbers read on the migrated pages. Periodiseringar: chevron column dropped; rows expand on click exactly like the verifikat list. Import: provider logo chips return on Hamta fran annat system (live- version parity) and Koppla bank carries the Enable Banking mark. Adds skatteverket(_color), bolagsverket, enable-banking plus claude/ anthropic marks (for future use) under public/logos/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * polish(ui): keep Importera and Exportera as separate tabs on the import page Founder feedback: the merged two-column landing goes back to the familiar split. The house seg switches between the Importera rows and the Exportera rows (SIE 4 dialog + Molnsynkronisering fold), ?view=export selects the export tab again and the hash deep links flip to it before opening their surface. Row language and logo chips stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): readable Enable Banking mark and full-width skattekonto The enable-banking.webp is the full stacked logo in white-on-transparent: invisible on the light chip and mush at 16px. The chip now uses a cropped 368px icon square (enable-banking-icon.png) with the marketing site's grayscale+brightness treatment in light mode and a white lift in dark. Skattekonto loses its max-w-3xl cap so the table stretches the content column exactly like Bokforing and Transaktioner; the saldo tiles take KPI-card width (lg:grid-cols-4). Import's tab columns stretch too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): address review-bot findings on rest-of-nav 1 CodeRabbit triage, all three confirmed real: the Bokfor forfallna attn action is hidden for read-only users instead of rendering a no-op link; a failed deadline edit rethrows so the form stays open with the user's input; authority marks are reserved for statutory (system-generated) deadlines: a manual tax-category deadline keeps the neutral icon. Compliance swarm's two high findings verified clean, no change needed: /api/bookkeeping/accruals/:id/dissolve and /api/deadlines/:id (+ /complete) all run through withRouteContext with company_id scoping and 404 on miss. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
194 lines
6.2 KiB
TypeScript
194 lines
6.2 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslations } from 'next-intl'
|
|
import { Deadline } from '@/types'
|
|
import { cn } from '@/lib/utils'
|
|
import { isDeadlineOverdue, parseDate, startOfDay } from '@/lib/calendar/utils'
|
|
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
|
|
import {
|
|
CalendarClock,
|
|
Check,
|
|
FileText,
|
|
Landmark,
|
|
Pencil,
|
|
ReceiptText,
|
|
Truck,
|
|
} from 'lucide-react'
|
|
|
|
interface DeadlineRowProps {
|
|
deadline: Deadline
|
|
/** Row click: open the edit dialog. */
|
|
onEdit?: (deadline: Deadline) => void
|
|
/** "Markera klar" (or un-done): the list confirms before toggling. */
|
|
onRequestToggle: (deadline: Deadline) => void
|
|
}
|
|
|
|
function daysFromToday(dateStr: string): number {
|
|
const today = startOfDay(new Date())
|
|
const target = parseDate(dateStr)
|
|
return Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
const TYPE_ICONS: Record<Deadline['deadline_type'], typeof CalendarClock> = {
|
|
tax: Landmark,
|
|
invoicing: ReceiptText,
|
|
delivery: Truck,
|
|
report: FileText,
|
|
other: CalendarClock,
|
|
}
|
|
|
|
// Statutory deadlines carry the receiving authority's mark: Bolagsverket for
|
|
// the company-law dates, Skatteverket for everything tax-shaped. Non-system
|
|
// deadlines keep the neutral type icons.
|
|
const BOLAGSVERKET_TYPES = new Set(['arsredovisning', 'arsstamma'])
|
|
|
|
export function deadlineAuthorityLogo(deadline: Deadline): { src: string; alt: string } | null {
|
|
if (deadline.tax_deadline_type) {
|
|
return BOLAGSVERKET_TYPES.has(deadline.tax_deadline_type)
|
|
? { src: '/logos/bolagsverket.jpg', alt: 'Bolagsverket' }
|
|
: { src: '/logos/skatteverket_color.svg', alt: 'Skatteverket' }
|
|
}
|
|
// Manual deadlines (including user-created tax-category ones) keep the
|
|
// neutral type icons: the authority crest is reserved for statutory dates
|
|
// the system generated from the tax settings.
|
|
return null
|
|
}
|
|
|
|
/** The shared 28px round mark: authority logo on white, or a muted type icon. */
|
|
export function DeadlineMark({
|
|
deadline,
|
|
completed = false,
|
|
}: {
|
|
deadline: Deadline
|
|
completed?: boolean
|
|
}) {
|
|
const logo = deadlineAuthorityLogo(deadline)
|
|
if (completed) {
|
|
return (
|
|
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground/60">
|
|
<Check className="h-3.5 w-3.5" aria-hidden="true" />
|
|
</span>
|
|
)
|
|
}
|
|
if (logo) {
|
|
// Deliberately white in both themes: the authority marks are badges, not
|
|
// chrome, and their brand colors need a white ground.
|
|
return (
|
|
<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={logo.src} alt={logo.alt} className="h-4 w-4 object-contain" />
|
|
</span>
|
|
)
|
|
}
|
|
const Icon = TYPE_ICONS[deadline.deadline_type] ?? CalendarClock
|
|
return (
|
|
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground">
|
|
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const SWEDISH_MONTHS_SHORT = [
|
|
'jan', 'feb', 'mar', 'apr', 'maj', 'jun',
|
|
'jul', 'aug', 'sep', 'okt', 'nov', 'dec',
|
|
]
|
|
|
|
export function deadlineDateLabel(dateStr: string): string {
|
|
const date = parseDate(dateStr)
|
|
return `${date.getDate()} ${SWEDISH_MONTHS_SHORT[date.getMonth()]}`
|
|
}
|
|
|
|
/**
|
|
* One deadline as a concept thread row (scene 17): icon circle, "12 aug ·
|
|
* Title" line, muted status sentence below, quiet hover actions. Rows are
|
|
* borderless; the section headers carry the structure.
|
|
*/
|
|
export function DeadlineRow({ deadline, onEdit, onRequestToggle }: DeadlineRowProps) {
|
|
const t = useTranslations('deadlines')
|
|
const overdue = isDeadlineOverdue(deadline)
|
|
const completed = deadline.is_completed
|
|
const diff = daysFromToday(deadline.due_date)
|
|
|
|
const relative = completed
|
|
? null
|
|
: diff === 0
|
|
? t('rel_today')
|
|
: diff === 1
|
|
? t('rel_tomorrow')
|
|
: diff < 0
|
|
? t('rel_days_ago', { count: Math.abs(diff) })
|
|
: diff <= 14
|
|
? t('rel_in_days', { count: diff })
|
|
: null
|
|
|
|
const subParts = [
|
|
relative,
|
|
deadline.due_time ? `kl ${deadline.due_time.slice(0, 5)}` : null,
|
|
deadline.customer?.name ?? null,
|
|
].filter(Boolean) as string[]
|
|
|
|
return (
|
|
<div
|
|
onClick={() => onEdit?.(deadline)}
|
|
className={cn(
|
|
'group flex items-start gap-3 rounded-md py-3 -mx-2 px-2 transition-colors duration-150',
|
|
onEdit && 'cursor-pointer hover:bg-secondary/35',
|
|
completed && 'opacity-60',
|
|
)}
|
|
>
|
|
<DeadlineMark deadline={deadline} completed={completed} />
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<p className={cn('text-sm leading-6', completed && 'line-through text-muted-foreground')}>
|
|
<span className="tabular-nums text-muted-foreground">
|
|
{deadlineDateLabel(deadline.due_date)}
|
|
</span>
|
|
<span className="text-muted-foreground/50"> · </span>
|
|
<span>{deadline.title}</span>
|
|
</p>
|
|
{subParts.length > 0 && (
|
|
<p
|
|
className={cn(
|
|
'text-xs leading-5',
|
|
overdue && !completed ? 'text-destructive' : 'text-muted-foreground',
|
|
)}
|
|
>
|
|
{subParts.join(' · ')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center gap-3 pt-1">
|
|
{completed ? (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onRequestToggle(deadline)
|
|
}}
|
|
className={cn(QUIET_LINK_CLASS, 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100')}
|
|
>
|
|
{t('mark_not_done')}
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onRequestToggle(deadline)
|
|
}}
|
|
className={cn(QUIET_LINK_CLASS, 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100')}
|
|
>
|
|
{t('group_mark_done')}
|
|
</button>
|
|
)}
|
|
{onEdit && (
|
|
<Pencil
|
|
className="h-3.5 w-3.5 text-muted-foreground/0 transition-colors group-hover:text-muted-foreground/50"
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|