Features: - Complete Next.js 16 app with TypeScript and Tailwind CSS - Customer-facing price calculator form with validation - Admin mode showing detailed price breakdowns - Accurate price calculations matching business requirements - Responsive design with custom shadcn/ui theme - API endpoint for quote requests - Danish postal code distance calculations - Complete test coverage against documentation examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
169 lines
No EOL
7.5 KiB
TypeScript
169 lines
No EOL
7.5 KiB
TypeScript
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { formatPrice, type CalculationDetails } from "@/lib/calculations"
|
||
import { PRICES, CONSTRAINTS } from "@/lib/constants"
|
||
|
||
interface CalculationDetailsProps {
|
||
details: CalculationDetails
|
||
}
|
||
|
||
export function CalculationDetailsView({ details }: 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>
|
||
|
||
{/* 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">
|
||
<span className="text-muted-foreground">
|
||
Isolering {details.insulationThickness > 0 ? `(${details.insulationVolume.toFixed(2)} m³ × ${formatPrice(PRICES.INSULATION_TOTAL)}/m³)` : "(simpel arbejdsløn)"}:
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.insulation)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Gulvvarme ({details.area} m² × {formatPrice(PRICES.FLOOR_HEATING_TOTAL)}/m²):
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.floorHeating)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Syntetisk net ({details.area} m² × {formatPrice(PRICES.SYNTHETIC_NET_TOTAL)}/m²):
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.syntheticNet)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Flydespartel ({details.area} m² × {formatPrice(PRICES.SELF_LEVELING_COMPOUND)}/m²):
|
||
</span>
|
||
<span className="font-medium">{formatPrice(details.selfLevelingCompound)}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">
|
||
Pumpebil-tillæg ({details.compoundWeight.toLocaleString("da-DK")} kg):
|
||
</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>
|
||
)}
|
||
</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>
|
||
)
|
||
} |