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>
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
import { formatPrice } from "@/lib/calculations"
|
|
import { type StoredQuote, type QuoteStatus } from "@/lib/db"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Phone, Check, X, ExternalLink } from "lucide-react"
|
|
|
|
interface QuoteCardProps {
|
|
quote: StoredQuote
|
|
onStatusChange: (id: number, status: QuoteStatus) => void
|
|
onReject: (quote: StoredQuote) => void
|
|
}
|
|
|
|
function formatRelativeDate(dateString: string): string {
|
|
const date = new Date(dateString)
|
|
const now = new Date()
|
|
const diffMs = now.getTime() - date.getTime()
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
|
|
if (diffDays === 0) return "I dag"
|
|
if (diffDays === 1) return "I går"
|
|
if (diffDays < 7) return `${diffDays}d`
|
|
if (diffDays < 30) return `${Math.floor(diffDays / 7)}u`
|
|
return `${Math.floor(diffDays / 30)}m`
|
|
}
|
|
|
|
export function QuoteCard({ quote, onStatusChange, onReject }: QuoteCardProps) {
|
|
const slug = `${quote.postalCode}-${quote.id}`
|
|
const detailUrl = `/tilbud/${slug}`
|
|
|
|
return (
|
|
<div className="group flex items-center gap-2 rounded-lg border bg-white p-2 shadow-sm transition-shadow hover:shadow-md">
|
|
{/* Main content - clickable */}
|
|
<a href={detailUrl} target="_blank" rel="noopener noreferrer" className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="truncate text-sm font-medium">{quote.customerName}</span>
|
|
<ExternalLink className="h-3 w-3 flex-shrink-0 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
<span>{quote.postalCode}</span>
|
|
<span>·</span>
|
|
<span>{quote.area}m²</span>
|
|
<span>·</span>
|
|
<span>{formatRelativeDate(quote.createdAt)}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex-shrink-0 text-right">
|
|
<div className="text-sm font-semibold text-primary">
|
|
{quote.totalInclVat ? formatPrice(Math.round(quote.totalInclVat)) : "—"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
{/* Action buttons - always visible */}
|
|
<div className="flex flex-shrink-0 items-center gap-0.5 border-l pl-2">
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className={`h-7 w-7 ${
|
|
quote.status === "contacted"
|
|
? "text-blue-300"
|
|
: "text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
|
}`}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
if (quote.status !== "contacted") {
|
|
onStatusChange(quote.id, "contacted")
|
|
}
|
|
}}
|
|
title="Marker som kontaktet"
|
|
disabled={quote.status === "contacted"}
|
|
>
|
|
<Phone className="h-3.5 w-3.5" />
|
|
</Button>
|
|
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className={`h-7 w-7 ${
|
|
quote.status === "accepted"
|
|
? "text-green-300"
|
|
: "text-green-600 hover:bg-green-50 hover:text-green-700"
|
|
}`}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
if (quote.status !== "accepted") {
|
|
onStatusChange(quote.id, "accepted")
|
|
}
|
|
}}
|
|
title="Accepter"
|
|
disabled={quote.status === "accepted"}
|
|
>
|
|
<Check className="h-3.5 w-3.5" />
|
|
</Button>
|
|
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-7 w-7 text-red-600 hover:bg-red-50 hover:text-red-700"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onReject(quote)
|
|
}}
|
|
title="Afvis"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|