091d043c85
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>
241 lines
9.0 KiB
TypeScript
241 lines
9.0 KiB
TypeScript
'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 } 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>
|
|
)
|
|
}
|