Files
accounted/extensions/general/calendar/components/CalendarDayCell.tsx
T
Jakob WennbergandClaude Opus 4.6 091d043c85 feat: UI polish, lint fixes, onboarding redesign, help page expansion, and test improvements
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>
2026-03-05 23:05:49 +01:00

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>
)
}