foamking/components/dashboard/quote-card.tsx
mikl0s 3ebb63dc6c Add admin dashboard, authentication, step wizard, and quote management
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>
2026-02-22 20:59:11 +00:00

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>
)
}