'use client' import { useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { Deadline, DeadlineType } from '@/types' import { DeadlineRow, deadlineDateLabel } from './DeadlineRow' import { DeadlineGroupCard, isSkattekontoDeadline } from './DeadlineGroupCard' import { ConfirmDialog } from '@/components/ui/confirm-dialog' import { isDeadlineOverdue, parseDate, startOfDay } from '@/lib/calendar/utils' type TypeSegment = 'all' | 'tax' | 'invoicing' | 'own' const SEGMENT_TYPES: Record, ReadonlyArray> = { tax: ['tax'], invoicing: ['invoicing'], own: ['delivery', 'report', 'other'], } interface DeadlineListProps { deadlines: Deadline[] onDeadlineToggle: (deadline: Deadline) => Promise onDeadlineEdit: (deadline: Deadline) => void } /** * Viktiga datum as the concept thread (scene 17): a type seg, borderless * rows under hairline section headers, and a "Närmast" countdown pane on the * right. Marking done confirms up front via ConfirmDialog. */ export function DeadlineList({ deadlines, onDeadlineToggle, onDeadlineEdit, }: DeadlineListProps) { const t = useTranslations('deadlines') const [segment, setSegment] = useState('all') const [confirmTarget, setConfirmTarget] = useState(null) const filteredDeadlines = useMemo(() => { if (segment === 'all') return deadlines const types = SEGMENT_TYPES[segment] return deadlines.filter((d) => types.includes(d.deadline_type)) }, [deadlines, segment]) const groupedDeadlines = useMemo(() => { const today = new Date().toISOString().split('T')[0] const overdue: Deadline[] = [] const todayDeadlines: Deadline[] = [] const upcoming: Deadline[] = [] const completed: Deadline[] = [] for (const d of filteredDeadlines) { if (d.is_completed) { completed.push(d) } else if (isDeadlineOverdue(d)) { overdue.push(d) } else if (d.due_date === today) { todayDeadlines.push(d) } else { upcoming.push(d) } } return { overdue, today: todayDeadlines, upcoming, completed } }, [filteredDeadlines]) // "Närmast" pane: days to the nearest pending deadline (across the // unfiltered list: the countdown is page context, not a filter readout). const nearest = useMemo(() => { const pending = deadlines .filter((d) => !d.is_completed && !isDeadlineOverdue(d)) .sort((a, b) => a.due_date.localeCompare(b.due_date)) const first = pending[0] if (!first) return null const today = startOfDay(new Date()) const days = Math.round( (parseDate(first.due_date).getTime() - today.getTime()) / (1000 * 60 * 60 * 24), ) return { deadline: first, days: Math.max(0, days) } }, [deadlines]) const overdueCount = useMemo( () => deadlines.filter((d) => !d.is_completed && isDeadlineOverdue(d)).length, [deadlines], ) const handleRequestToggle = (deadline: Deadline) => { // Un-completing is a harmless revert: no confirmation (matches the undo // toast the page already offers). if (deadline.is_completed) { void onDeadlineToggle(deadline) return } setConfirmTarget(deadline) } const sections = [ { key: 'overdue', label: t('section_overdue'), items: groupedDeadlines.overdue }, { key: 'today', label: t('section_today'), items: groupedDeadlines.today }, { key: 'upcoming', label: t('section_upcoming'), items: groupedDeadlines.upcoming }, { key: 'completed', label: t('section_completed'), items: groupedDeadlines.completed }, ].filter((s) => s.items.length > 0) // Skattekonto obligations legally share the same due date (moms + AGI + // preliminärskatt all fall on "den 12:e"): collapse 2+ such rows into one // grouped row per date instead of near-identical rows. Other deadlines // render as before, in their original order. type ListEntry = | { kind: 'single'; deadline: Deadline } | { kind: 'group'; date: string; items: Deadline[] } const toEntries = (items: Deadline[]): ListEntry[] => { const byDate = new Map() for (const d of items) { if (!isSkattekontoDeadline(d)) continue const group = byDate.get(d.due_date) ?? [] group.push(d) byDate.set(d.due_date, group) } const emittedDates = new Set() const entries: ListEntry[] = [] for (const d of items) { const group = byDate.get(d.due_date) if (isSkattekontoDeadline(d) && group && group.length >= 2) { if (!emittedDates.has(d.due_date)) { emittedDates.add(d.due_date) entries.push({ kind: 'group', date: d.due_date, items: group }) } continue } entries.push({ kind: 'single', deadline: d }) } return entries } return (
{/* Toolbar: the type seg (concept: Alla / Skatt / Fakturering / Egna) */}
{( [ { key: 'all', label: t('seg_all') }, { key: 'tax', label: t('seg_tax') }, { key: 'invoicing', label: t('seg_invoicing') }, { key: 'own', label: t('seg_own') }, ] as const ).map(({ key, label }) => ( ))}
{/* The thread */} {filteredDeadlines.length === 0 ? (

{segment !== 'all' ? t('empty_filtered') : t('empty_title')}

) : (
{sections.map(({ key, label, items }) => (

{label}

{items.length}
{toEntries(items).map((entry) => entry.kind === 'group' ? ( ) : ( ), )}
))}
)} {/* "Närmast" pane (concept aside) */}
!open && setConfirmTarget(null)} title={t('confirm_done_title')} description={ confirmTarget ? `${deadlineDateLabel(confirmTarget.due_date)} · ${confirmTarget.title}` : undefined } confirmLabel={t('group_confirm')} onConfirm={async () => { if (confirmTarget) await onDeadlineToggle(confirmTarget) }} />
) }