foamking/app/historik/page.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

222 lines
8.3 KiB
TypeScript

"use client"
import { useEffect, useState, useMemo } from "react"
import { useRouter } from "next/navigation"
import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { SearchFilter } from "@/components/dashboard/search-filter"
import { formatPrice } from "@/lib/calculations"
import { type StoredQuote, type QuoteStatus } from "@/lib/db"
import { LogOut, LayoutDashboard, Loader2, ExternalLink } from "lucide-react"
const STATUS_LABELS: Record<QuoteStatus, { label: string; className: string }> = {
new: { label: "Ny", className: "bg-blue-100 text-blue-700" },
contacted: { label: "Kontaktet", className: "bg-amber-100 text-amber-700" },
accepted: { label: "Accepteret", className: "bg-green-100 text-green-700" },
rejected: { label: "Afvist", className: "bg-gray-100 text-gray-500" },
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString("da-DK", {
day: "numeric",
month: "short",
year: "numeric",
})
}
export default function HistorikPage() {
const router = useRouter()
const [quotes, setQuotes] = useState<StoredQuote[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
useEffect(() => {
fetchQuotes()
}, [])
async function fetchQuotes() {
try {
const res = await fetch("/api/quotes")
if (!res.ok) {
if (res.status === 401) {
router.push("/login")
return
}
throw new Error("Failed to fetch quotes")
}
const data = await res.json()
setQuotes(data.quotes)
} catch (error) {
console.error("Failed to fetch quotes:", error)
} finally {
setLoading(false)
}
}
async function handleLogout() {
try {
await fetch("/api/auth/logout", { method: "POST" })
router.push("/login")
router.refresh()
} catch (error) {
console.error("Logout failed:", error)
}
}
const filteredQuotes = useMemo(() => {
if (!search.trim()) return quotes
const term = search.toLowerCase()
return quotes.filter(
(q) =>
q.customerName.toLowerCase().includes(term) ||
q.customerEmail.toLowerCase().includes(term) ||
q.postalCode.includes(term) ||
q.id.toString().includes(term)
)
}, [quotes, search])
if (loading) {
return (
<main className="flex min-h-screen items-center justify-center bg-muted/30">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</main>
)
}
return (
<main className="min-h-screen bg-muted/30">
{/* Header */}
<header className="sticky top-0 z-10 border-b bg-white">
<div className="container mx-auto flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-4">
<Link href="/">
<Image
src="/foam-king-logo.png"
alt="Foam King"
width={120}
height={48}
className="h-10 w-auto"
/>
</Link>
<h1 className="hidden text-lg font-semibold sm:block">Historik</h1>
</div>
<div className="flex items-center gap-2">
<Link href="/dashboard">
<Button variant="ghost" size="sm">
<LayoutDashboard className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">Dashboard</span>
</Button>
</Link>
<Button variant="ghost" size="sm" onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">Log ud</span>
</Button>
</div>
</div>
</header>
{/* Main content */}
<div className="container mx-auto px-4 py-6">
{/* Search and stats */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="max-w-md flex-1">
<SearchFilter value={search} onChange={setSearch} />
</div>
<div className="text-sm text-muted-foreground">
{filteredQuotes.length} tilbud
{search && ` (af ${quotes.length})`}
</div>
</div>
{/* Table */}
<div className="overflow-hidden rounded-xl border bg-white shadow-sm">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b bg-muted/50">
<th className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
#
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
Kunde
</th>
<th className="hidden px-4 py-3 text-left text-sm font-medium text-muted-foreground md:table-cell">
Lokation
</th>
<th className="hidden px-4 py-3 text-left text-sm font-medium text-muted-foreground sm:table-cell">
Areal
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
Pris
</th>
<th className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
Status
</th>
<th className="hidden px-4 py-3 text-left text-sm font-medium text-muted-foreground lg:table-cell">
Dato
</th>
<th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground"></th>
</tr>
</thead>
<tbody>
{filteredQuotes.map((quote) => {
const slug = `${quote.postalCode}-${quote.id}`
const status = STATUS_LABELS[quote.status]
return (
<tr key={quote.id} className="border-b last:border-0 hover:bg-muted/30">
<td className="px-4 py-3 text-sm text-muted-foreground">{quote.id}</td>
<td className="px-4 py-3">
<div className="font-medium">{quote.customerName}</div>
<div className="text-sm text-muted-foreground">{quote.customerEmail}</div>
</td>
<td className="hidden px-4 py-3 text-sm md:table-cell">
{quote.postalCode}
{quote.address && (
<span className="text-muted-foreground">, {quote.address}</span>
)}
</td>
<td className="hidden px-4 py-3 text-sm sm:table-cell">{quote.area} m²</td>
<td className="px-4 py-3 font-medium">
{quote.totalInclVat ? formatPrice(Math.round(quote.totalInclVat)) : "—"}
</td>
<td className="px-4 py-3">
<span
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${status.className}`}
>
{status.label}
</span>
</td>
<td className="hidden px-4 py-3 text-sm text-muted-foreground lg:table-cell">
{formatDate(quote.createdAt)}
</td>
<td className="px-4 py-3 text-right">
<a
href={`/tilbud/${slug}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
>
Åbn
<ExternalLink className="h-3 w-3" />
</a>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{filteredQuotes.length === 0 && (
<div className="py-12 text-center text-muted-foreground">
{search ? "Ingen tilbud matcher søgningen" : "Ingen tilbud endnu"}
</div>
)}
</div>
</div>
</main>
)
}