246 lines
9.2 KiB
TypeScript
246 lines
9.2 KiB
TypeScript
'use client'
|
|
|
|
import { Invoice, Deadline } from '@/types'
|
|
import { cn } from '@/lib/utils'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Plus } from 'lucide-react'
|
|
import {
|
|
formatDateISO,
|
|
isSameDay,
|
|
isInvoiceOverdue,
|
|
isDeadlineOverdue,
|
|
getTimeSlots,
|
|
formatDayViewHeader,
|
|
DEADLINE_TYPE_LABELS,
|
|
PRIORITY_LABELS,
|
|
} from '@/lib/calendar/utils'
|
|
|
|
interface CalendarDayViewProps {
|
|
date: Date
|
|
invoices: Invoice[]
|
|
deadlines: Deadline[]
|
|
onAddDeadline: (date: Date) => void
|
|
}
|
|
|
|
export function CalendarDayView({
|
|
date,
|
|
invoices,
|
|
deadlines,
|
|
onAddDeadline,
|
|
}: CalendarDayViewProps) {
|
|
const timeSlots = getTimeSlots(8, 20)
|
|
const today = new Date()
|
|
const isToday = isSameDay(date, today)
|
|
const dateStr = formatDateISO(date)
|
|
|
|
// Filter items for this day
|
|
const dayInvoices = invoices.filter(inv => inv.due_date === dateStr)
|
|
const dayDeadlines = deadlines.filter(d => d.due_date === dateStr)
|
|
|
|
// Categorize items
|
|
const activeInvoices = dayInvoices.filter(inv =>
|
|
inv.status !== 'paid' && inv.status !== 'cancelled' && inv.status !== 'credited'
|
|
)
|
|
const paidInvoices = dayInvoices.filter(inv => inv.status === 'paid')
|
|
const activeDeadlines = dayDeadlines.filter(d => !d.is_completed)
|
|
const completedDeadlines = dayDeadlines.filter(d => d.is_completed)
|
|
|
|
return (
|
|
<div className="border rounded-lg overflow-hidden">
|
|
{/* Header */}
|
|
<div className={cn(
|
|
'p-4 border-b flex items-center justify-between',
|
|
isToday && 'bg-primary/10'
|
|
)}>
|
|
<h3 className={cn(
|
|
'text-lg font-semibold capitalize',
|
|
isToday && 'text-primary'
|
|
)}>
|
|
{formatDayViewHeader(date)}
|
|
{isToday && <span className="ml-2 text-sm font-normal text-muted-foreground">(Idag)</span>}
|
|
</h3>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onAddDeadline(date)}
|
|
>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
Lägg till deadline
|
|
</Button>
|
|
</div>
|
|
|
|
{/* All-day events section */}
|
|
<div className="border-b">
|
|
<div className="p-2 bg-muted/30">
|
|
<div className="text-xs font-medium text-muted-foreground mb-2">Hela dagen</div>
|
|
|
|
{/* Active invoices */}
|
|
{activeInvoices.length > 0 && (
|
|
<div className="space-y-1 mb-2">
|
|
{activeInvoices.map((invoice) => (
|
|
<div
|
|
key={invoice.id}
|
|
className={cn(
|
|
'p-2 rounded-md',
|
|
isInvoiceOverdue(invoice)
|
|
? 'bg-destructive/10 border border-destructive/30'
|
|
: 'bg-primary/10 border border-primary/30'
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn(
|
|
'w-3 h-3 rounded-full flex-shrink-0',
|
|
isInvoiceOverdue(invoice) ? 'bg-destructive' : 'bg-primary'
|
|
)} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={cn(
|
|
'text-sm font-medium',
|
|
isInvoiceOverdue(invoice) ? 'text-destructive' : 'text-primary'
|
|
)}>
|
|
Faktura {invoice.invoice_number}
|
|
{isInvoiceOverdue(invoice) && (
|
|
<span className="ml-2 text-xs bg-destructive/20 px-1.5 py-0.5 rounded">
|
|
Förfallen
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{invoice.customer?.name} • {(invoice.total_sek || invoice.total).toLocaleString('sv-SE')} kr
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Paid invoices */}
|
|
{paidInvoices.length > 0 && (
|
|
<div className="space-y-1 mb-2">
|
|
{paidInvoices.map((invoice) => (
|
|
<div
|
|
key={invoice.id}
|
|
className="p-2 rounded-md bg-success/10 border border-success/30"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-success flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-success">
|
|
Faktura {invoice.invoice_number}
|
|
<span className="ml-2 text-xs bg-success/20 px-1.5 py-0.5 rounded">
|
|
Betald
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{invoice.customer?.name} • {(invoice.total_sek || invoice.total).toLocaleString('sv-SE')} kr
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Active deadlines */}
|
|
{activeDeadlines.length > 0 && (
|
|
<div className="space-y-1 mb-2">
|
|
{activeDeadlines.map((deadline) => (
|
|
<div
|
|
key={deadline.id}
|
|
className={cn(
|
|
'p-2 rounded-md',
|
|
isDeadlineOverdue(deadline)
|
|
? 'bg-destructive/10 border border-destructive/30'
|
|
: 'bg-warning/10 border border-warning/30'
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn(
|
|
'w-3 h-3 rounded-sm flex-shrink-0',
|
|
isDeadlineOverdue(deadline) ? 'bg-destructive' : 'bg-warning'
|
|
)} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={cn(
|
|
'text-sm font-medium',
|
|
isDeadlineOverdue(deadline) ? 'text-destructive' : 'text-warning-foreground'
|
|
)}>
|
|
{deadline.title}
|
|
{isDeadlineOverdue(deadline) && (
|
|
<span className="ml-2 text-xs bg-destructive/20 px-1.5 py-0.5 rounded">
|
|
Försenad
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{DEADLINE_TYPE_LABELS[deadline.deadline_type] || deadline.deadline_type}
|
|
{deadline.priority !== 'normal' && (
|
|
<span className={cn(
|
|
'ml-2 px-1.5 py-0.5 rounded',
|
|
deadline.priority === 'critical' && 'bg-red-100 text-red-700',
|
|
deadline.priority === 'important' && 'bg-orange-100 text-orange-700'
|
|
)}>
|
|
{PRIORITY_LABELS[deadline.priority]}
|
|
</span>
|
|
)}
|
|
{deadline.customer?.name && ` • ${deadline.customer.name}`}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Completed deadlines */}
|
|
{completedDeadlines.length > 0 && (
|
|
<div className="space-y-1">
|
|
{completedDeadlines.map((deadline) => (
|
|
<div
|
|
key={deadline.id}
|
|
className="p-2 rounded-md bg-success/10 border border-success/30 opacity-60"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-sm bg-success flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-success line-through">
|
|
{deadline.title}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{DEADLINE_TYPE_LABELS[deadline.deadline_type] || deadline.deadline_type}
|
|
<span className="ml-2 bg-success/20 px-1.5 py-0.5 rounded">Klar</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{dayInvoices.length === 0 && dayDeadlines.length === 0 && (
|
|
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
Inga händelser för denna dag
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time grid */}
|
|
<div className="max-h-[400px] overflow-y-auto">
|
|
{timeSlots.map((time) => (
|
|
<button
|
|
key={time}
|
|
onClick={() => onAddDeadline(date)}
|
|
className="w-full flex border-b last:border-b-0 hover:bg-muted/50 transition-colors"
|
|
>
|
|
<div className="w-16 p-2 border-r text-xs text-muted-foreground text-right pr-2 flex-shrink-0">
|
|
{time}
|
|
</div>
|
|
<div className="flex-1 min-h-[40px]" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|