Expand the calculator with a multi-step wizard flow, admin dashboard with quote tracking, login/auth system, distance API integration, and history page. Add new UI components (dialog, progress, select, slider, switch), update pricing logic, and improve the overall design with new assets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
"use client"
|
|
|
|
import { formatPrice } from "@/lib/calculations"
|
|
import { type StoredQuote, type QuoteStatus } from "@/lib/db"
|
|
import { QuoteCard } from "./quote-card"
|
|
|
|
interface KanbanColumnProps {
|
|
title: string
|
|
status: QuoteStatus
|
|
quotes: StoredQuote[]
|
|
showTotal?: boolean
|
|
onStatusChange: (id: number, status: QuoteStatus) => void
|
|
onReject: (quote: StoredQuote) => void
|
|
}
|
|
|
|
function KanbanColumn({
|
|
title,
|
|
status,
|
|
quotes,
|
|
showTotal = true,
|
|
onStatusChange,
|
|
onReject,
|
|
}: KanbanColumnProps) {
|
|
const total = quotes.reduce((sum, q) => sum + (q.totalInclVat || 0), 0)
|
|
|
|
const bgColor = {
|
|
new: "bg-blue-50/50 border-blue-200",
|
|
contacted: "bg-amber-50/50 border-amber-200",
|
|
accepted: "bg-green-50/50 border-green-200",
|
|
rejected: "bg-gray-50/50 border-gray-200",
|
|
}[status]
|
|
|
|
const headerColor = {
|
|
new: "text-blue-700",
|
|
contacted: "text-amber-700",
|
|
accepted: "text-green-700",
|
|
rejected: "text-gray-500",
|
|
}[status]
|
|
|
|
return (
|
|
<div className={`flex flex-col rounded-xl border ${bgColor}`}>
|
|
{/* Fixed header */}
|
|
<div className="flex-shrink-0 border-b border-inherit p-3">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className={`font-semibold ${headerColor}`}>{title}</h2>
|
|
<span className={`text-sm font-medium ${headerColor}`}>{quotes.length}</span>
|
|
</div>
|
|
{showTotal && quotes.length > 0 && (
|
|
<div className="mt-1 text-sm text-muted-foreground">
|
|
Værdi: <span className="font-medium">{formatPrice(Math.round(total))}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Scrollable content */}
|
|
<div
|
|
className="flex-1 space-y-2 overflow-y-auto p-2"
|
|
style={{ maxHeight: "calc(100vh - 220px)" }}
|
|
>
|
|
{quotes.length === 0 ? (
|
|
<p className="py-6 text-center text-xs text-muted-foreground">Ingen tilbud</p>
|
|
) : (
|
|
quotes.map((quote) => (
|
|
<QuoteCard
|
|
key={quote.id}
|
|
quote={quote}
|
|
onStatusChange={onStatusChange}
|
|
onReject={onReject}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface KanbanBoardProps {
|
|
quotes: StoredQuote[]
|
|
onStatusChange: (id: number, status: QuoteStatus) => void
|
|
onReject: (quote: StoredQuote) => void
|
|
rejectedCount: number
|
|
onShowRejected: () => void
|
|
}
|
|
|
|
export function KanbanBoard({
|
|
quotes,
|
|
onStatusChange,
|
|
onReject,
|
|
rejectedCount,
|
|
onShowRejected,
|
|
}: KanbanBoardProps) {
|
|
// Only show 3 main columns
|
|
const columns: { title: string; status: QuoteStatus; showTotal: boolean }[] = [
|
|
{ title: "Nye tilbud", status: "new", showTotal: true },
|
|
{ title: "Kunde kontaktet", status: "contacted", showTotal: true },
|
|
{ title: "Tilbud accepteret", status: "accepted", showTotal: true },
|
|
]
|
|
|
|
const groupedQuotes = columns.reduce(
|
|
(acc, col) => {
|
|
acc[col.status] = quotes.filter((q) => q.status === col.status)
|
|
return acc
|
|
},
|
|
{} as Record<QuoteStatus, StoredQuote[]>
|
|
)
|
|
|
|
return (
|
|
<div>
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
{columns.map((col) => (
|
|
<KanbanColumn
|
|
key={col.status}
|
|
title={col.title}
|
|
status={col.status}
|
|
quotes={groupedQuotes[col.status] || []}
|
|
showTotal={col.showTotal}
|
|
onStatusChange={onStatusChange}
|
|
onReject={onReject}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Rejected quotes link */}
|
|
{rejectedCount > 0 && (
|
|
<button
|
|
onClick={onShowRejected}
|
|
className="mt-4 text-sm text-muted-foreground hover:text-foreground hover:underline"
|
|
>
|
|
Se {rejectedCount} afviste tilbud →
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|