New classification logic etc
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Invoice, Deadline, DeadlineStatus } 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { Invoice, Deadline } from '@/types'
|
||||
import { SWEDISH_DAYS, getMonthGrid } from '@/lib/calendar/utils'
|
||||
import { CalendarDayCell } from './CalendarDayCell'
|
||||
|
||||
interface CalendarGridProps {
|
||||
year: number
|
||||
month: number
|
||||
invoices: Invoice[]
|
||||
deadlines: Deadline[]
|
||||
onDayClick: (date: Date) => void
|
||||
}
|
||||
|
||||
export function CalendarGrid({
|
||||
year,
|
||||
month,
|
||||
invoices,
|
||||
deadlines,
|
||||
onDayClick,
|
||||
}: CalendarGridProps) {
|
||||
const weeks = getMonthGrid(year, month)
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{/* Day headers */}
|
||||
<div className="grid grid-cols-7 bg-muted">
|
||||
{SWEDISH_DAYS.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="py-2 text-center text-sm font-medium text-muted-foreground border-r last:border-r-0"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Week rows */}
|
||||
{weeks.map((week, weekIndex) => (
|
||||
<div key={weekIndex} className="grid grid-cols-7">
|
||||
{week.map((date, dayIndex) => (
|
||||
<CalendarDayCell
|
||||
key={dayIndex}
|
||||
date={date}
|
||||
currentMonth={month}
|
||||
invoices={invoices}
|
||||
deadlines={deadlines}
|
||||
onDayClick={onDayClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CalendarViewMode } from '@/types'
|
||||
import {
|
||||
SWEDISH_MONTHS,
|
||||
getWeekNumber,
|
||||
formatDayViewHeader,
|
||||
} from '@/lib/calendar/utils'
|
||||
import { ViewModeSelector } from './ViewModeSelector'
|
||||
|
||||
interface CalendarHeaderProps {
|
||||
year: number
|
||||
month: number
|
||||
currentDate?: Date
|
||||
viewMode?: CalendarViewMode
|
||||
onPrevious: () => void
|
||||
onNext: () => void
|
||||
onToday: () => void
|
||||
onViewModeChange?: (mode: CalendarViewMode) => void
|
||||
}
|
||||
|
||||
export function CalendarHeader({
|
||||
year,
|
||||
month,
|
||||
currentDate,
|
||||
viewMode = 'month',
|
||||
onPrevious,
|
||||
onNext,
|
||||
onToday,
|
||||
onViewModeChange,
|
||||
}: CalendarHeaderProps) {
|
||||
// Generate title based on view mode
|
||||
const getTitle = () => {
|
||||
switch (viewMode) {
|
||||
case 'week':
|
||||
if (currentDate) {
|
||||
const weekNum = getWeekNumber(currentDate)
|
||||
return `Vecka ${weekNum}, ${currentDate.getFullYear()}`
|
||||
}
|
||||
return `${SWEDISH_MONTHS[month]} ${year}`
|
||||
|
||||
case 'day':
|
||||
if (currentDate) {
|
||||
return formatDayViewHeader(currentDate)
|
||||
}
|
||||
return `${SWEDISH_MONTHS[month]} ${year}`
|
||||
|
||||
case 'month':
|
||||
default:
|
||||
return `${SWEDISH_MONTHS[month]} ${year}`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onClick={onPrevious}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={onNext}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onToday}>
|
||||
Idag
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold capitalize">
|
||||
{getTitle()}
|
||||
</h2>
|
||||
|
||||
{onViewModeChange && (
|
||||
<ViewModeSelector
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={onViewModeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Spacer when no view mode selector */}
|
||||
{!onViewModeChange && <div />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
'use client'
|
||||
|
||||
import { Invoice, Deadline } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
getWeekDays,
|
||||
formatWeekDayHeader,
|
||||
formatDateISO,
|
||||
isSameDay,
|
||||
isInvoiceOverdue,
|
||||
isDeadlineOverdue,
|
||||
getTimeSlots,
|
||||
} from '@/lib/calendar/utils'
|
||||
|
||||
interface CalendarWeekViewProps {
|
||||
currentDate: Date
|
||||
invoices: Invoice[]
|
||||
deadlines: Deadline[]
|
||||
onDayClick: (date: Date) => void
|
||||
}
|
||||
|
||||
export function CalendarWeekView({
|
||||
currentDate,
|
||||
invoices,
|
||||
deadlines,
|
||||
onDayClick,
|
||||
}: CalendarWeekViewProps) {
|
||||
const weekDays = getWeekDays(currentDate)
|
||||
const timeSlots = getTimeSlots(8, 20)
|
||||
const today = new Date()
|
||||
|
||||
// Group invoices and deadlines by date for quick lookup
|
||||
const getItemsForDate = (date: Date) => {
|
||||
const dateStr = formatDateISO(date)
|
||||
const dayInvoices = invoices.filter(inv => inv.due_date === dateStr)
|
||||
const dayDeadlines = deadlines.filter(d => d.due_date === dateStr)
|
||||
return { invoices: dayInvoices, deadlines: dayDeadlines }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{/* Header row with day names and dates */}
|
||||
<div className="grid grid-cols-[60px_repeat(7,1fr)] bg-muted border-b">
|
||||
<div className="p-2 border-r" /> {/* Empty corner cell */}
|
||||
{weekDays.map((date, index) => {
|
||||
const isToday = isSameDay(date, today)
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onDayClick(date)}
|
||||
className={cn(
|
||||
'p-2 text-center border-r last:border-r-0 hover:bg-muted/80 transition-colors',
|
||||
isToday && 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'text-sm font-medium',
|
||||
isToday && 'text-primary font-bold'
|
||||
)}>
|
||||
{formatWeekDayHeader(date)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* All-day events section */}
|
||||
<div className="grid grid-cols-[60px_repeat(7,1fr)] border-b bg-muted/30">
|
||||
<div className="p-2 border-r text-xs text-muted-foreground">
|
||||
Heldag
|
||||
</div>
|
||||
{weekDays.map((date, index) => {
|
||||
const items = getItemsForDate(date)
|
||||
const isToday = isSameDay(date, today)
|
||||
|
||||
// Filter to only show non-timed items (all invoices and deadlines without times are "all-day")
|
||||
const allDayInvoices = items.invoices.filter(inv =>
|
||||
inv.status !== 'paid' && inv.status !== 'cancelled' && inv.status !== 'credited'
|
||||
)
|
||||
const allDayDeadlines = items.deadlines.filter(d => !d.is_completed)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'min-h-[60px] p-1 border-r last:border-r-0',
|
||||
isToday && 'bg-primary/5'
|
||||
)}
|
||||
>
|
||||
{/* Invoice indicators */}
|
||||
{allDayInvoices.map((invoice) => (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className={cn(
|
||||
'text-xs p-1 mb-1 rounded truncate',
|
||||
isInvoiceOverdue(invoice)
|
||||
? 'bg-destructive/20 text-destructive'
|
||||
: 'bg-primary/20 text-primary'
|
||||
)}
|
||||
title={`Faktura ${invoice.invoice_number} - ${invoice.customer?.name || ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-current flex-shrink-0" />
|
||||
<span className="truncate">F#{invoice.invoice_number}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Deadline indicators */}
|
||||
{allDayDeadlines.map((deadline) => (
|
||||
<div
|
||||
key={deadline.id}
|
||||
className={cn(
|
||||
'text-xs p-1 mb-1 rounded truncate',
|
||||
isDeadlineOverdue(deadline)
|
||||
? 'bg-destructive/20 text-destructive'
|
||||
: 'bg-warning/20 text-warning-foreground'
|
||||
)}
|
||||
title={deadline.title}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-sm bg-current flex-shrink-0" />
|
||||
<span className="truncate">{deadline.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Time grid */}
|
||||
<div className="max-h-[500px] overflow-y-auto">
|
||||
{timeSlots.map((time, timeIndex) => (
|
||||
<div key={time} className="grid grid-cols-[60px_repeat(7,1fr)] border-b last:border-b-0">
|
||||
<div className="p-2 border-r text-xs text-muted-foreground text-right pr-2">
|
||||
{time}
|
||||
</div>
|
||||
{weekDays.map((date, dayIndex) => {
|
||||
const isToday = isSameDay(date, today)
|
||||
return (
|
||||
<button
|
||||
key={dayIndex}
|
||||
onClick={() => onDayClick(date)}
|
||||
className={cn(
|
||||
'min-h-[40px] border-r last:border-r-0 hover:bg-muted/50 transition-colors',
|
||||
isToday && 'bg-primary/5'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Invoice, Deadline } from '@/types'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import {
|
||||
isInvoiceOverdue,
|
||||
isDeadlineOverdue,
|
||||
formatDateISO,
|
||||
DEADLINE_TYPE_LABELS,
|
||||
PRIORITY_LABELS,
|
||||
PRIORITY_COLORS,
|
||||
} from '@/lib/calendar/utils'
|
||||
import { FileText, CheckCircle, Clock, AlertTriangle, Plus } from 'lucide-react'
|
||||
|
||||
interface DayDetailModalProps {
|
||||
date: Date | null
|
||||
invoices: Invoice[]
|
||||
deadlines: Deadline[]
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onAddDeadline: (date: Date) => void
|
||||
onToggleDeadline: (deadline: Deadline) => void
|
||||
}
|
||||
|
||||
export function DayDetailModal({
|
||||
date,
|
||||
invoices,
|
||||
deadlines,
|
||||
open,
|
||||
onOpenChange,
|
||||
onAddDeadline,
|
||||
onToggleDeadline,
|
||||
}: DayDetailModalProps) {
|
||||
if (!date) return null
|
||||
|
||||
const dateStr = formatDateISO(date)
|
||||
const dayInvoices = invoices.filter(inv => inv.due_date === dateStr)
|
||||
const dayDeadlines = deadlines.filter(d => d.due_date === dateStr)
|
||||
|
||||
const formattedDate = date.toLocaleDateString('sv-SE', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="capitalize">{formattedDate}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Invoices section */}
|
||||
{dayInvoices.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Fakturor ({dayInvoices.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{dayInvoices.map((invoice) => {
|
||||
const overdue = isInvoiceOverdue(invoice)
|
||||
const paid = invoice.status === 'paid'
|
||||
return (
|
||||
<Link
|
||||
key={invoice.id}
|
||||
href={`/invoices/${invoice.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div
|
||||
className={`p-3 rounded-lg border transition-colors hover:bg-muted/50 ${
|
||||
overdue ? 'border-destructive/50 bg-destructive/5' :
|
||||
paid ? 'border-success/50 bg-success/5' :
|
||||
'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
{overdue ? (
|
||||
<AlertTriangle className="h-4 w-4 text-destructive mt-0.5" />
|
||||
) : paid ? (
|
||||
<CheckCircle className="h-4 w-4 text-success mt-0.5" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-sm">
|
||||
{invoice.invoice_number}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(invoice.customer as { name: string })?.name || 'Okänd kund'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-sm">
|
||||
{formatCurrency(invoice.total, invoice.currency)}
|
||||
</p>
|
||||
<Badge
|
||||
variant={
|
||||
overdue ? 'destructive' :
|
||||
paid ? 'success' :
|
||||
'secondary'
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{overdue ? 'Förfallen' : paid ? 'Betald' : 'Väntande'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deadlines section */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Deadlines ({dayDeadlines.length})
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onAddDeadline(date)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Lägg till
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{dayDeadlines.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
Inga deadlines denna dag
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{dayDeadlines.map((deadline) => {
|
||||
const overdue = isDeadlineOverdue(deadline)
|
||||
const completed = deadline.is_completed
|
||||
const priorityStyle = PRIORITY_COLORS[deadline.priority]
|
||||
|
||||
return (
|
||||
<div
|
||||
key={deadline.id}
|
||||
className={`p-3 rounded-lg border transition-colors ${
|
||||
overdue && !completed
|
||||
? 'border-destructive/50 bg-destructive/5'
|
||||
: completed
|
||||
? 'border-success/50 bg-success/5'
|
||||
: `${priorityStyle.border} ${priorityStyle.bg}`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<button
|
||||
onClick={() => onToggleDeadline(deadline)}
|
||||
className="mt-0.5"
|
||||
>
|
||||
{completed ? (
|
||||
<CheckCircle className="h-4 w-4 text-success" />
|
||||
) : overdue ? (
|
||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<div>
|
||||
<p className={`font-medium text-sm ${completed ? 'line-through text-muted-foreground' : ''}`}>
|
||||
{deadline.title}
|
||||
</p>
|
||||
{deadline.due_time && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
kl. {deadline.due_time.slice(0, 5)}
|
||||
</p>
|
||||
)}
|
||||
{deadline.customer && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{deadline.customer.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{DEADLINE_TYPE_LABELS[deadline.deadline_type]}
|
||||
</Badge>
|
||||
{!completed && deadline.priority !== 'normal' && (
|
||||
<Badge
|
||||
variant={deadline.priority === 'critical' ? 'destructive' : 'warning'}
|
||||
className="text-xs"
|
||||
>
|
||||
{PRIORITY_LABELS[deadline.priority]}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{dayInvoices.length === 0 && dayDeadlines.length === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<Clock className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Inga händelser denna dag
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={() => onAddDeadline(date)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Lägg till deadline
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Invoice, Deadline, CalendarViewMode } from '@/types'
|
||||
import {
|
||||
getWeekStart,
|
||||
} from '@/lib/calendar/utils'
|
||||
import { CalendarHeader } from './CalendarHeader'
|
||||
import { CalendarGrid } from './CalendarGrid'
|
||||
import { CalendarWeekView } from './CalendarWeekView'
|
||||
import { CalendarDayView } from './CalendarDayView'
|
||||
import { DayDetailModal } from './DayDetailModal'
|
||||
import { DeadlineForm } from '@/components/deadlines/DeadlineForm'
|
||||
|
||||
interface PaymentCalendarProps {
|
||||
invoices: Invoice[]
|
||||
deadlines: Deadline[]
|
||||
customers: { id: string; name: string }[]
|
||||
onDeadlineCreate: (data: Omit<Deadline, 'id' | 'user_id' | 'created_at' | 'updated_at'>) => Promise<void>
|
||||
onDeadlineToggle: (deadline: Deadline) => Promise<void>
|
||||
}
|
||||
|
||||
export function PaymentCalendar({
|
||||
invoices,
|
||||
deadlines,
|
||||
customers,
|
||||
onDeadlineCreate,
|
||||
onDeadlineToggle,
|
||||
}: PaymentCalendarProps) {
|
||||
const today = new Date()
|
||||
const [year, setYear] = useState(today.getFullYear())
|
||||
const [month, setMonth] = useState(today.getMonth())
|
||||
const [viewMode, setViewMode] = useState<CalendarViewMode>('month')
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(today)
|
||||
const [showDayModal, setShowDayModal] = useState(false)
|
||||
const [modalDate, setModalDate] = useState<Date | null>(null)
|
||||
const [showDeadlineForm, setShowDeadlineForm] = useState(false)
|
||||
const [deadlineFormDate, setDeadlineFormDate] = useState<Date | null>(null)
|
||||
|
||||
// Navigation handlers based on view mode
|
||||
const handlePrevious = useCallback(() => {
|
||||
if (viewMode === 'month') {
|
||||
if (month === 0) {
|
||||
setMonth(11)
|
||||
setYear(year - 1)
|
||||
} else {
|
||||
setMonth(month - 1)
|
||||
}
|
||||
} else if (viewMode === 'week') {
|
||||
const newDate = new Date(selectedDate)
|
||||
newDate.setDate(newDate.getDate() - 7)
|
||||
setSelectedDate(newDate)
|
||||
setYear(newDate.getFullYear())
|
||||
setMonth(newDate.getMonth())
|
||||
} else if (viewMode === 'day') {
|
||||
const newDate = new Date(selectedDate)
|
||||
newDate.setDate(newDate.getDate() - 1)
|
||||
setSelectedDate(newDate)
|
||||
setYear(newDate.getFullYear())
|
||||
setMonth(newDate.getMonth())
|
||||
}
|
||||
}, [viewMode, month, year, selectedDate])
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (viewMode === 'month') {
|
||||
if (month === 11) {
|
||||
setMonth(0)
|
||||
setYear(year + 1)
|
||||
} else {
|
||||
setMonth(month + 1)
|
||||
}
|
||||
} else if (viewMode === 'week') {
|
||||
const newDate = new Date(selectedDate)
|
||||
newDate.setDate(newDate.getDate() + 7)
|
||||
setSelectedDate(newDate)
|
||||
setYear(newDate.getFullYear())
|
||||
setMonth(newDate.getMonth())
|
||||
} else if (viewMode === 'day') {
|
||||
const newDate = new Date(selectedDate)
|
||||
newDate.setDate(newDate.getDate() + 1)
|
||||
setSelectedDate(newDate)
|
||||
setYear(newDate.getFullYear())
|
||||
setMonth(newDate.getMonth())
|
||||
}
|
||||
}, [viewMode, month, year, selectedDate])
|
||||
|
||||
const handleToday = useCallback(() => {
|
||||
const today = new Date()
|
||||
setYear(today.getFullYear())
|
||||
setMonth(today.getMonth())
|
||||
setSelectedDate(today)
|
||||
}, [])
|
||||
|
||||
const handleViewModeChange = useCallback((mode: CalendarViewMode) => {
|
||||
setViewMode(mode)
|
||||
// When switching to week view, ensure selectedDate is set properly
|
||||
if (mode === 'week') {
|
||||
const weekStart = getWeekStart(selectedDate)
|
||||
setSelectedDate(weekStart)
|
||||
}
|
||||
}, [selectedDate])
|
||||
|
||||
const handleDayClick = useCallback((date: Date) => {
|
||||
if (viewMode === 'month') {
|
||||
// In month view, clicking a day shows the modal
|
||||
setModalDate(date)
|
||||
setShowDayModal(true)
|
||||
} else if (viewMode === 'week') {
|
||||
// In week view, clicking a day switches to day view
|
||||
setSelectedDate(date)
|
||||
setViewMode('day')
|
||||
setYear(date.getFullYear())
|
||||
setMonth(date.getMonth())
|
||||
}
|
||||
}, [viewMode])
|
||||
|
||||
const handleAddDeadline = useCallback((date: Date) => {
|
||||
setDeadlineFormDate(date)
|
||||
setShowDayModal(false)
|
||||
setShowDeadlineForm(true)
|
||||
}, [])
|
||||
|
||||
const handleDeadlineFormClose = useCallback(() => {
|
||||
setShowDeadlineForm(false)
|
||||
setDeadlineFormDate(null)
|
||||
}, [])
|
||||
|
||||
const handleDeadlineSubmit = useCallback(async (data: Omit<Deadline, 'id' | 'user_id' | 'created_at' | 'updated_at'>) => {
|
||||
await onDeadlineCreate(data)
|
||||
handleDeadlineFormClose()
|
||||
}, [onDeadlineCreate])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<CalendarHeader
|
||||
year={year}
|
||||
month={month}
|
||||
currentDate={selectedDate}
|
||||
viewMode={viewMode}
|
||||
onPrevious={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onToday={handleToday}
|
||||
onViewModeChange={handleViewModeChange}
|
||||
/>
|
||||
|
||||
{/* Month View */}
|
||||
{viewMode === 'month' && (
|
||||
<CalendarGrid
|
||||
year={year}
|
||||
month={month}
|
||||
invoices={invoices}
|
||||
deadlines={deadlines}
|
||||
onDayClick={handleDayClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Week View */}
|
||||
{viewMode === 'week' && (
|
||||
<CalendarWeekView
|
||||
currentDate={selectedDate}
|
||||
invoices={invoices}
|
||||
deadlines={deadlines}
|
||||
onDayClick={handleDayClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Day View */}
|
||||
{viewMode === 'day' && (
|
||||
<CalendarDayView
|
||||
date={selectedDate}
|
||||
invoices={invoices}
|
||||
deadlines={deadlines}
|
||||
onAddDeadline={handleAddDeadline}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Day detail modal (for month view clicks) */}
|
||||
<DayDetailModal
|
||||
date={modalDate}
|
||||
invoices={invoices}
|
||||
deadlines={deadlines}
|
||||
open={showDayModal}
|
||||
onOpenChange={setShowDayModal}
|
||||
onAddDeadline={handleAddDeadline}
|
||||
onToggleDeadline={onDeadlineToggle}
|
||||
/>
|
||||
|
||||
{/* Deadline form dialog */}
|
||||
<DeadlineForm
|
||||
open={showDeadlineForm}
|
||||
onOpenChange={handleDeadlineFormClose}
|
||||
onSubmit={handleDeadlineSubmit}
|
||||
initialDate={deadlineFormDate}
|
||||
customers={customers}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Invoice } from '@/types'
|
||||
import { calculatePeriodSummary, SWEDISH_MONTHS } from '@/lib/calendar/utils'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { TrendingUp, Clock, CheckCircle, AlertTriangle } from 'lucide-react'
|
||||
|
||||
interface PaymentSummaryCardProps {
|
||||
invoices: Invoice[]
|
||||
year: number
|
||||
month: number
|
||||
}
|
||||
|
||||
export function PaymentSummaryCard({ invoices, year, month }: PaymentSummaryCardProps) {
|
||||
const summary = calculatePeriodSummary(invoices)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg">
|
||||
Sammanfattning {SWEDISH_MONTHS[month]} {year}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Expected */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center">
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Förväntat</p>
|
||||
<p className="text-sm font-medium">{summary.pendingCount} fakturor</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold">{formatCurrency(summary.totalExpected)}</p>
|
||||
</div>
|
||||
|
||||
{/* Overdue */}
|
||||
{summary.overdueCount > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center">
|
||||
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Förfallen</p>
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{summary.overdueCount} fakturor
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-destructive">
|
||||
{formatCurrency(summary.totalOverdue)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Paid */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center">
|
||||
<CheckCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Betald</p>
|
||||
<p className="text-sm font-medium">{summary.paidCount} fakturor</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-success">
|
||||
{formatCurrency(summary.totalPaid)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pending */}
|
||||
{summary.pendingCount > 0 && summary.overdueCount < summary.pendingCount && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Väntande</p>
|
||||
<p className="text-sm font-medium">
|
||||
{summary.pendingCount - summary.overdueCount} fakturor
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold">
|
||||
{formatCurrency(summary.totalExpected - summary.totalOverdue)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { CalendarViewMode } from '@/types'
|
||||
import { VIEW_MODE_LABELS } from '@/lib/calendar/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ViewModeSelectorProps {
|
||||
viewMode: CalendarViewMode
|
||||
onViewModeChange: (mode: CalendarViewMode) => void
|
||||
}
|
||||
|
||||
export function ViewModeSelector({ viewMode, onViewModeChange }: ViewModeSelectorProps) {
|
||||
const modes: CalendarViewMode[] = ['month', 'week', 'day']
|
||||
|
||||
return (
|
||||
<div className="inline-flex rounded-md border">
|
||||
{modes.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange(mode)}
|
||||
className={cn(
|
||||
'rounded-none border-r last:border-r-0 px-3',
|
||||
viewMode === mode && 'bg-muted'
|
||||
)}
|
||||
>
|
||||
{VIEW_MODE_LABELS[mode]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user