Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
5.9 KiB
TypeScript
166 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import { cn } from '@/lib/utils'
|
|
import { Invoice, Deadline } from '@/types'
|
|
import { isInvoiceOverdue, isSameDay, formatDateISO, STATUS_COLORS } from '@/lib/calendar/utils'
|
|
|
|
interface CalendarDayCellProps {
|
|
date: Date
|
|
currentMonth: number
|
|
invoices: Invoice[]
|
|
deadlines: Deadline[]
|
|
onDayClick: (date: Date) => void
|
|
}
|
|
|
|
export function CalendarDayCell({
|
|
date,
|
|
currentMonth,
|
|
invoices,
|
|
deadlines,
|
|
onDayClick,
|
|
}: CalendarDayCellProps) {
|
|
const dateStr = formatDateISO(date)
|
|
const isCurrentMonth = date.getMonth() === currentMonth
|
|
const isToday = isSameDay(date, new Date())
|
|
|
|
// Filter invoices and deadlines for this day
|
|
const dayInvoices = invoices.filter(inv => inv.due_date === dateStr)
|
|
const dayDeadlines = deadlines.filter(d => d.due_date === dateStr)
|
|
|
|
// Count invoices by status
|
|
const overdueInvoices = dayInvoices.filter(isInvoiceOverdue)
|
|
const paidInvoices = dayInvoices.filter(inv => inv.status === 'paid')
|
|
const pendingInvoices = dayInvoices.filter(
|
|
inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.status !== 'credited' && !isInvoiceOverdue(inv)
|
|
)
|
|
|
|
// Group deadlines by status
|
|
const deadlinesByStatus = dayDeadlines.reduce((acc, d) => {
|
|
if (d.is_completed) {
|
|
acc.completed = (acc.completed || 0) + 1
|
|
} else if (d.status) {
|
|
acc[d.status] = (acc[d.status] || 0) + 1
|
|
}
|
|
return acc
|
|
}, {} as Record<string, number>)
|
|
|
|
const hasItems = dayInvoices.length > 0 || dayDeadlines.length > 0
|
|
const hasOverdue = overdueInvoices.length > 0 || deadlinesByStatus.overdue > 0
|
|
const hasActionNeeded = deadlinesByStatus.action_needed > 0
|
|
|
|
return (
|
|
<button
|
|
onClick={() => onDayClick(date)}
|
|
className={cn(
|
|
'min-h-[80px] p-1 border-b border-r text-left transition-colors',
|
|
'hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-inset',
|
|
!isCurrentMonth && 'bg-muted/30 text-muted-foreground',
|
|
isToday && 'bg-primary/5',
|
|
hasOverdue && 'border-l-2 border-l-destructive',
|
|
!hasOverdue && hasActionNeeded && 'border-l-2 border-l-orange-500'
|
|
)}
|
|
>
|
|
<div className={cn(
|
|
'text-sm font-medium mb-1',
|
|
isToday && 'text-primary font-bold'
|
|
)}>
|
|
{date.getDate()}
|
|
</div>
|
|
|
|
{hasItems && (
|
|
<div className="space-y-0.5">
|
|
{/* Overdue invoices */}
|
|
{overdueInvoices.length > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-destructive" />
|
|
<span className="text-xs text-destructive truncate">
|
|
{overdueInvoices.length} förfallen
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pending invoices */}
|
|
{pendingInvoices.length > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
<span className="text-xs text-muted-foreground truncate">
|
|
{pendingInvoices.length} faktura
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paid invoices */}
|
|
{paidInvoices.length > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-success" />
|
|
<span className="text-xs text-muted-foreground truncate">
|
|
{paidInvoices.length} betald
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Overdue deadlines */}
|
|
{deadlinesByStatus.overdue > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className={cn('w-2 h-2 rounded-sm', STATUS_COLORS.overdue.dot)} />
|
|
<span className="text-xs text-destructive truncate">
|
|
{deadlinesByStatus.overdue} försenad
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action needed deadlines */}
|
|
{deadlinesByStatus.action_needed > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className={cn('w-2 h-2 rounded-sm', STATUS_COLORS.action_needed.dot)} />
|
|
<span className="text-xs text-orange-700 truncate">
|
|
{deadlinesByStatus.action_needed} åtgärd
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Upcoming deadlines */}
|
|
{deadlinesByStatus.upcoming > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className={cn('w-2 h-2 rounded-sm', STATUS_COLORS.upcoming.dot)} />
|
|
<span className="text-xs text-muted-foreground truncate">
|
|
{deadlinesByStatus.upcoming} deadline
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* In progress deadlines */}
|
|
{deadlinesByStatus.in_progress > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className={cn('w-2 h-2 rounded-sm', STATUS_COLORS.in_progress.dot)} />
|
|
<span className="text-xs text-yellow-700 truncate">
|
|
{deadlinesByStatus.in_progress} pågår
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Submitted deadlines */}
|
|
{deadlinesByStatus.submitted > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<div className={cn('w-2 h-2 rounded-sm', STATUS_COLORS.submitted.dot)} />
|
|
<span className="text-xs text-purple-700 truncate">
|
|
{deadlinesByStatus.submitted} inskickad
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Completed/Confirmed deadlines */}
|
|
{(deadlinesByStatus.completed > 0 || deadlinesByStatus.confirmed > 0) && (
|
|
<div className="flex items-center gap-1">
|
|
<div className={cn('w-2 h-2 rounded-sm', STATUS_COLORS.confirmed.dot)} />
|
|
<span className="text-xs text-muted-foreground truncate">
|
|
{(deadlinesByStatus.completed || 0) + (deadlinesByStatus.confirmed || 0)} klar
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</button>
|
|
)
|
|
}
|