"use client"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Combobox } from "@/components/ui/combobox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { placeStructure } from "@/app/(frontend)/logistics/map/actions"; import { validatePlacement, type NodeInput, type StructureInput, type ZoneInput, } from "@/lib/map/placement"; import type { MapPoint } from "@/lib/map/points"; import type { MapConfig, NodeFeature, StructureFeature, ZoneFeature } from "@/lib/map/types"; interface BlueprintOption { id: number; name: string; terrainType: "land" | "water" | null; constructionDurationMinutes: number | null; requiredTech: string[]; materials: { resource: number; resourceName: string; amount: number | null }[]; resourcesInRange: { resource: number | null; range: number | null }[]; structuresInRange: { structure: number | null; range: number | null }[]; } interface FactionOption { id: number; name: string; } interface PlacementDialogProps { open: boolean; onOpenChange: (open: boolean) => void; mapConfig: MapConfig | null; point: MapPoint; zones: ZoneFeature[]; nodes: NodeFeature[]; structures: StructureFeature[]; onPlaced: () => void; } export function PlacementDialog({ open, onOpenChange, mapConfig, point, zones, nodes, structures, onPlaced, }: PlacementDialogProps) { const [blueprints, setBlueprints] = useState([]); const [factions, setFactions] = useState([]); const [blueprintId, setBlueprintId] = useState(""); const [name, setName] = useState(""); const [factionId, setFactionId] = useState("none"); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (!open) return; let cancelled = false; void (async () => { const [blueprintRes, factionRes] = await Promise.all([ fetch("/api/structures?limit=500&depth=1&sort=name", { cache: "no-store" }), fetch("/api/factions?limit=200&depth=0&sort=name", { cache: "no-store" }), ]); if (cancelled) return; if (blueprintRes.ok) { const body = (await blueprintRes.json()) as { docs: { id: number; name: string; terrainType?: string | null; constructionDurationMinutes?: number | null; requiredTech?: { name?: string }[] | number[]; materials?: { resource?: { id: number; name?: string } | number; amount?: number | null; }[]; resourcesInRange?: { resource?: { id: number } | number | null; range?: number | null; }[]; structuresInRange?: { structure?: { id: number } | number | null; range?: number | null; }[]; }[]; }; setBlueprints( body.docs.map((doc) => ({ id: doc.id, name: doc.name, terrainType: (doc.terrainType as "land" | "water" | null | undefined) ?? "land", constructionDurationMinutes: doc.constructionDurationMinutes ?? null, requiredTech: (doc.requiredTech ?? []) .filter( (tech): tech is { name?: string } => typeof tech === "object" && tech !== null, ) .map((tech) => tech.name ?? "Unknown"), materials: (doc.materials ?? []).map((material) => ({ resource: typeof material.resource === "object" && material.resource !== null ? material.resource.id : 0, resourceName: typeof material.resource === "object" && material.resource !== null ? (material.resource.name ?? "Resource") : "Resource", amount: material.amount ?? 0, })), resourcesInRange: (doc.resourcesInRange ?? []).map((requirement) => ({ resource: typeof requirement.resource === "object" && requirement.resource !== null ? requirement.resource.id : null, range: requirement.range ?? null, })), structuresInRange: (doc.structuresInRange ?? []).map((requirement) => ({ structure: typeof requirement.structure === "object" && requirement.structure !== null ? requirement.structure.id : null, range: requirement.range ?? null, })), })), ); } if (factionRes.ok) { const body = (await factionRes.json()) as { docs: { id: number; name: string }[] }; setFactions(body.docs); } })(); return () => { cancelled = true; }; }, [open]); const blueprint = useMemo( () => blueprints.find((option) => option.id === Number(blueprintId)) ?? null, [blueprints, blueprintId], ); const validationError = useMemo(() => { if (!mapConfig || !blueprint) return null; if ( point[0] < 0 || point[0] > mapConfig.worldSizeWidth || point[1] < 0 || point[1] > mapConfig.worldSizeHeight ) { return "Site is outside the map bounds."; } const zoneInputs: ZoneInput[] = zones.map((zone) => ({ id: zone.id, name: zone.name, type: zone.type, resources: zone.resources.map((entry) => entry.id), points: zone.points, })); const nodeInputs: NodeInput[] = nodes.map((node) => ({ id: node.id, resources: node.resources.map((entry) => entry.id), position: node.point, })); const structureInputs: StructureInput[] = structures.map((structure) => ({ id: structure.id, type: structure.blueprintId ?? 0, coordinates: structure.point, })); const validation = validatePlacement({ zones: zoneInputs, nodes: nodeInputs, structures: structureInputs, blueprintTerrain: blueprint.terrainType ?? "land", resourcesInRange: blueprint.resourcesInRange, structuresInRange: blueprint.structuresInRange, point, }); return validation.allowed ? null : (validation.error ?? "Site is not valid."); }, [mapConfig, blueprint, point, zones, nodes, structures]); const reset = () => { setBlueprintId(""); setName(""); setFactionId("none"); }; const submit = async () => { if (!mapConfig || !blueprint) return; setSubmitting(true); try { const result = await placeStructure({ mapId: mapConfig.id, blueprintId: blueprint.id, name, factionId: factionId === "none" ? null : Number(factionId), x: point[0], y: point[1], }); if (result.success) { toast.success(`Placement of "${name}" submitted. Ship materials to start construction.`); reset(); onOpenChange(false); onPlaced(); } else { toast.error(result.error ?? "Placement failed."); } } finally { setSubmitting(false); } }; const canSubmit = Boolean(blueprint) && name.trim().length > 0 && !validationError && !submitting; return ( { if (!nextOpen) reset(); onOpenChange(nextOpen); }} > Place structure Site at {Math.round(point[0])} m east, {Math.round(point[1])} m south {mapConfig ? ` on ${mapConfig.name}` : ""}.
({ value: String(option.id), label: option.name, }))} />
setName(event.target.value)} placeholder="e.g. FOB Wolfpack" />
({ value: String(faction.id), label: faction.name, })), ]} />
{blueprint && (
Terrain: {blueprint.terrainType ?? "land"} {blueprint.constructionDurationMinutes != null && ( <> {" \u00b7 "}Build time: {blueprint.constructionDurationMinutes} min )}
{blueprint.materials.length > 0 && (
Materials:{" "} {blueprint.materials .map((material) => `${material.resourceName} x${material.amount ?? 0}`) .join(", ")}
)} {blueprint.requiredTech.length > 0 && (
Requires tech (informational): {blueprint.requiredTech.join(", ")}
)}
)} {validationError && (

{validationError}

)}
); }