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>
276 lines
12 KiB
TypeScript
276 lines
12 KiB
TypeScript
import { AlertTriangle, CheckCircle, Check, X } from "lucide-react"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { formatPrice, type CalculationDetails } from "@/lib/calculations"
|
||
import { PRICES, CONSTRAINTS, FLOORING_TYPES } from "@/lib/constants"
|
||
|
||
interface CalculationDetailsProps {
|
||
details: CalculationDetails
|
||
distanceSource?: "openrouteservice" | "table" | null
|
||
}
|
||
|
||
export function CalculationDetailsView({ details, distanceSource }: CalculationDetailsProps) {
|
||
return (
|
||
<Card className="w-full">
|
||
<CardHeader>
|
||
<CardTitle>Detaljeret Prisberegning</CardTitle>
|
||
<CardDescription>Komplet oversigt over alle delpriser og beregninger</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
{/* Input Values */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Indtastede værdier</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Gulvareal:</span>
|
||
<span>{details.area} m²</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Gulvhøjde:</span>
|
||
<span>{details.height} cm</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Postnummer:</span>
|
||
<span>{details.postalCode}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Afstand (tur-retur):</span>
|
||
<span>{details.distance} km</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Selected Components */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Valgte komponenter</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="flex items-center gap-2 text-muted-foreground">
|
||
{details.includeInsulation ? (
|
||
<Check className="h-4 w-4 text-green-600" />
|
||
) : (
|
||
<X className="h-4 w-4 text-red-500" />
|
||
)}
|
||
Isolering:
|
||
</span>
|
||
<span className={details.includeInsulation ? "" : "text-muted-foreground"}>
|
||
{details.includeInsulation ? "Inkluderet" : "Fravalgt"}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="flex items-center gap-2 text-muted-foreground">
|
||
{details.includeFloorHeating ? (
|
||
<Check className="h-4 w-4 text-green-600" />
|
||
) : (
|
||
<X className="h-4 w-4 text-red-500" />
|
||
)}
|
||
Gulvvarme:
|
||
</span>
|
||
<span className={details.includeFloorHeating ? "" : "text-muted-foreground"}>
|
||
{details.includeFloorHeating ? "Inkluderet" : "Fravalgt"}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="flex items-center gap-2 text-muted-foreground">
|
||
{details.includeCompound ? (
|
||
<Check className="h-4 w-4 text-green-600" />
|
||
) : (
|
||
<X className="h-4 w-4 text-red-500" />
|
||
)}
|
||
Gulvstøbning:
|
||
</span>
|
||
<span className={details.includeCompound ? "" : "text-muted-foreground"}>
|
||
{details.includeCompound ? "Inkluderet" : "Fravalgt"}
|
||
</span>
|
||
</div>
|
||
{details.includeCompound && (
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Gulvbelægning:</span>
|
||
<span>{FLOORING_TYPES[details.flooringType]?.name || details.flooringType}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Calculated Values */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Beregnede værdier</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Isoleringstykkelse:</span>
|
||
<span>
|
||
{details.insulationThickness} cm ({details.height} -{" "}
|
||
{CONSTRAINTS.CONCRETE_THICKNESS} cm beton)
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Isoleringsvolumen:</span>
|
||
<span>{details.insulationVolume.toFixed(2)} m³</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Spartelvægt:</span>
|
||
<span>
|
||
{details.compoundWeight.toLocaleString("da-DK")} kg ({details.area} m² ×{" "}
|
||
{PRICES.COMPOUND_WEIGHT_PER_M2} kg/m²)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Component Prices */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Komponent priser</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div
|
||
className={`flex justify-between ${!details.includeInsulation ? "opacity-50" : ""}`}
|
||
>
|
||
<span className="text-muted-foreground">
|
||
Isolering{" "}
|
||
{details.includeInsulation
|
||
? details.insulationThickness > 0
|
||
? `(${details.insulationVolume.toFixed(2)} m³ × ${formatPrice(PRICES.INSULATION_TOTAL_PER_M3)}/m³)`
|
||
: "(simpel arbejdsløn)"
|
||
: "(fravalgt)"}
|
||
:
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.insulation)}</span>
|
||
</div>
|
||
<div
|
||
className={`flex justify-between ${!details.includeFloorHeating ? "opacity-50" : ""}`}
|
||
>
|
||
<span className="text-muted-foreground">
|
||
Gulvvarme{" "}
|
||
{details.includeFloorHeating
|
||
? `(${details.area} m² × ${formatPrice(PRICES.FLOOR_HEATING_TOTAL)}/m²)`
|
||
: "(fravalgt)"}
|
||
:
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.floorHeating)}</span>
|
||
</div>
|
||
<div
|
||
className={`flex justify-between ${!details.includeFloorHeating ? "opacity-50" : ""}`}
|
||
>
|
||
<span className="text-muted-foreground">
|
||
Syntetisk net{" "}
|
||
{details.includeFloorHeating
|
||
? `(${details.area} m² × ${formatPrice(PRICES.SYNTHETIC_NET_TOTAL)}/m²)`
|
||
: "(fravalgt)"}
|
||
:
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.syntheticNet)}</span>
|
||
</div>
|
||
<div className={`flex justify-between ${!details.includeCompound ? "opacity-50" : ""}`}>
|
||
<span className="text-muted-foreground">
|
||
Flydespartel{" "}
|
||
{details.includeCompound
|
||
? `(${details.area} m² × ${formatPrice(PRICES.SELF_LEVELING_COMPOUND)}/m²${FLOORING_TYPES[details.flooringType]?.compoundMultiplier > 1 ? " +28%" : ""})`
|
||
: "(fravalgt)"}
|
||
:
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.selfLevelingCompound)}</span>
|
||
</div>
|
||
<div className={`flex justify-between ${!details.includeCompound ? "opacity-50" : ""}`}>
|
||
<span className="text-muted-foreground">
|
||
Pumpebil-tillæg{" "}
|
||
{details.includeCompound
|
||
? `(${details.compoundWeight.toLocaleString("da-DK")} kg)`
|
||
: "(fravalgt)"}
|
||
:
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.pumpTruckFee)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Startgebyr:</span>
|
||
<span className="font-medium">{formatPrice(details.startFee)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Subtotal and Fees */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Subtotal og tillæg</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex justify-between border-t pt-2">
|
||
<span className="text-muted-foreground">Subtotal:</span>
|
||
<span className="font-semibold">{formatPrice(details.subtotal)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Afdækning ({(PRICES.COVERING_PERCENTAGE * 100).toFixed(1)}%):
|
||
</span>
|
||
<span>{formatPrice(details.coveringFee)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Affald ({(PRICES.WASTE_PERCENTAGE * 100).toFixed(2)}%):
|
||
</span>
|
||
<span>{formatPrice(details.wasteFee)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Tillæg i alt:</span>
|
||
<span className="font-medium">{formatPrice(details.totalFees)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Transport */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Transport</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Kørsel ({details.distance} km × {formatPrice(PRICES.TRANSPORT_PER_KM)}/km):
|
||
</span>
|
||
<span>{formatPrice(details.transport)}</span>
|
||
</div>
|
||
{details.bridgeFee > 0 && (
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Storebælt-tillæg:</span>
|
||
<span>{formatPrice(details.bridgeFee)}</span>
|
||
</div>
|
||
)}
|
||
{distanceSource && (
|
||
<div
|
||
className={`mt-2 flex items-center gap-2 rounded-md p-2 text-xs ${
|
||
distanceSource === "openrouteservice"
|
||
? "bg-green-50 text-green-700"
|
||
: "bg-amber-50 text-amber-700"
|
||
}`}
|
||
>
|
||
{distanceSource === "openrouteservice" ? (
|
||
<>
|
||
<CheckCircle className="h-3 w-3" />
|
||
<span>Præcis afstand via OpenRouteService</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<AlertTriangle className="h-3 w-3" />
|
||
<span>Præcis afstandsberegning ikke mulig - overslag brugt</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Final Total */}
|
||
<div>
|
||
<h3 className="mb-2 font-semibold">Total</h3>
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex justify-between border-t pt-2">
|
||
<span className="text-muted-foreground">Total ekskl. moms:</span>
|
||
<span className="font-semibold">{formatPrice(details.totalExclVat)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">Moms (25%):</span>
|
||
<span>{formatPrice(details.vat)}</span>
|
||
</div>
|
||
<div className="flex justify-between border-t pt-2 text-lg">
|
||
<span className="font-semibold">Total inkl. moms:</span>
|
||
<span className="font-bold text-primary">{formatPrice(details.totalInclVat)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|