foamking/components/calculator/calculation-details.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

276 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)}× ${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}× ${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}× ${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}× ${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>
)
}