"use client"; import { useCallback, useEffect, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Combobox } from "@/components/ui/combobox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { addResource, removeResource, transferResource, } from "@/app/(frontend)/logistics/structures/actions"; import { createShipment } from "@/app/(frontend)/logistics/shipments/actions"; import { getResourceStorageCap, isResourceProhibited, isResourceWhitelisted, } from "@/lib/storageRules"; import { GameStructure, GameVehicle, Resource, Structure, Vehicle } from "@/payload-types"; import { ArrowDownToLineIcon, ArrowRightLeftIcon, ArrowUpFromLineIcon, Loader2Icon, TruckIcon, } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import { cn, pluralize } from "@/lib/utils"; interface ManageStorageDialogProps { structureId: number; open: boolean; onOpenChange: (open: boolean) => void; canManage: boolean; canTransfer: boolean; } export function ManageStorageDialog({ structureId, open, onOpenChange, canManage, canTransfer, }: ManageStorageDialogProps) { const [structure, setStructure] = useState<(GameStructure & { type: Structure }) | null>(null); const [allStructures, setAllStructures] = useState<(GameStructure & { type: Structure })[]>([]); const [allResources, setAllResources] = useState([]); const [availableVehicles, setAvailableVehicles] = useState<(GameVehicle & { type: Vehicle })[]>( [], ); const [loading, setLoading] = useState(false); const [isPending, startTransition] = useTransition(); const router = useRouter(); const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); // Shipment flow state const [shipmentMode, setShipmentMode] = useState(false); const [shipmentResourceId, setShipmentResourceId] = useState(""); const [shipmentAmount, setShipmentAmount] = useState("1"); const [shipmentDestId, setShipmentDestId] = useState(""); const [shipmentVehicleId, setShipmentVehicleId] = useState(""); // Form state const [depositResourceId, setDepositResourceId] = useState(""); const [depositAmount, setDepositAmount] = useState("1"); const [withdrawResourceId, setWithdrawResourceId] = useState(""); const [withdrawAmount, setWithdrawAmount] = useState("1"); const [transferToId, setTransferToId] = useState(""); const [transferResourceId, setTransferResourceId] = useState(""); const [transferAmount, setTransferAmount] = useState("1"); const fetchData = useCallback(async () => { setLoading(true); try { const [structRes, allRes, resourcesRes, vehiclesRes] = await Promise.all([ fetch(`/api/game-structures/${structureId}?depth=2`), fetch(`/api/game-structures?depth=2&limit=500`), fetch(`/api/resources?depth=0&limit=500`), fetch( `/api/game-vehicles?depth=2&limit=500&where[deployedAt][equals]=${structureId}&where[status][equals]=idle`, ), ]); const anyUnauthorized = [structRes, allRes, resourcesRes, vehiclesRes].some( (res) => res.status === 401 || res.status === 403, ); if (anyUnauthorized) { setMessage({ type: "error", text: "You must be logged in to manage storage.", }); return; } const structData = await structRes.json(); const allData = await allRes.json(); const resourcesData = await resourcesRes.json(); const vehiclesData = await vehiclesRes.json(); setStructure(structData); setAllStructures( allData.docs.filter((s: GameStructure & { type: Structure }) => s.id !== structureId), ); setAllResources(resourcesData.docs ?? []); setAvailableVehicles(vehiclesData.docs ?? []); } catch { setMessage({ type: "error", text: "Failed to load structure data." }); } finally { setLoading(false); } }, [structureId]); useEffect(() => { if (open) { fetchData(); setMessage(null); setDepositResourceId(""); setDepositAmount("1"); setWithdrawResourceId(""); setWithdrawAmount("1"); setTransferToId(""); setTransferResourceId(""); setTransferAmount("1"); setShipmentMode(false); setShipmentResourceId(""); setShipmentAmount("1"); setShipmentDestId(""); setShipmentVehicleId(""); } }, [open, fetchData]); const storedResources = structure?.storedResources ?? []; const structureType = structure?.type; const maxMass = structureType?.maxCapacityMass ?? null; const currentMass = storedResources.reduce((total, entry) => { const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return total; return total + (resource.massPerUnit ?? 0) * entry.amount; }, 0); const remainingMass = maxMass ? maxMass - currentMass : null; const findResource = (id: string): Resource | undefined => allResources.find((r) => r.id === Number(id)); const findStoredResource = (id: string): { resource: Resource; amount: number } | undefined => { const entry = storedResources.find((e) => { const res = e.resource; return typeof res === "object" && res !== null && "id" in res ? res.id === Number(id) : res === Number(id); }); if (!entry) return undefined; const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return undefined; return { resource, amount: entry.amount }; }; const getStoredAmountOf = ( s: GameStructure & { type: Structure }, resourceId: number, ): number => { const grid = (s.storedResources ?? []).reduce((sum, e) => { const res = e.resource; const rid = typeof res === "object" && res !== null && "id" in res ? res.id : (res as number); return sum + (rid === resourceId ? e.amount : 0); }, 0); const voidAmount = (s.voidStorage ?? []).reduce((sum, e) => { const res = e.resource; const rid = typeof res === "object" && res !== null && "id" in res ? res.id : (res as number); return sum + (rid === resourceId ? e.amount : 0); }, 0); return grid + voidAmount; }; const getMassOf = (s: GameStructure & { type: Structure }): number => { return (s.storedResources ?? []).reduce((total, entry) => { const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return total; return total + (resource.massPerUnit ?? 0) * entry.amount; }, 0); }; const getAllowedDepositCap = (resourceId: number): number | undefined => { if (!structureType) return undefined; const cap = getResourceStorageCap(structureType, resourceId); if (cap == null) return undefined; const stored = structure ? getStoredAmountOf(structure, resourceId) : 0; return Math.max(0, cap - stored); }; const getDepositLimitInfoFor = ( resourceId: string, ): { max: number; reason: string } | undefined => { const resource = findResource(resourceId); if (!resource) return undefined; const reasons: { value: number; label: string }[] = []; if (remainingMass !== null) { const massPerUnit = resource.massPerUnit ?? 0; if (massPerUnit > 0) { reasons.push({ value: Math.floor(remainingMass / massPerUnit), label: "mass capacity", }); } } const allowance = getAllowedDepositCap(resource.id); if (allowance !== undefined) { reasons.push({ value: allowance, label: "storage allowance" }); } if (reasons.length === 0) return undefined; const binding = reasons.reduce((min, r) => (r.value < min.value ? r : min), reasons[0]); return { max: binding.value, reason: binding.label }; }; const getDepositMax = (): number | undefined => getDepositLimitInfoFor(depositResourceId)?.max; const getWithdrawMax = (): number | undefined => { const stored = findStoredResource(withdrawResourceId); return stored?.amount; }; const getTransferMaxFor = (destId: string): number | undefined => { const stored = findStoredResource(transferResourceId); if (!stored) return undefined; const resId = stored.resource.id; const dest = allStructures.find((s) => s.id === Number(destId)); const destType = dest?.type; if (dest && destType) { if (isResourceProhibited(destType, resId)) return 0; if (!isResourceWhitelisted(destType, resId)) return 0; const caps: number[] = [stored.amount]; const cap = getResourceStorageCap(destType, resId); if (cap != null) { caps.push(Math.max(0, cap - getStoredAmountOf(dest, resId))); } const destMaxMass = destType.maxCapacityMass; if (destMaxMass != null) { const massPerUnit = stored.resource.massPerUnit ?? 0; if (massPerUnit > 0) { caps.push(Math.floor((destMaxMass - getMassOf(dest)) / massPerUnit)); } } return Math.min(...caps); } return stored.amount; }; const getTransferMax = (): number | undefined => getTransferMaxFor(transferToId); const clampAmount = (value: string, max: number | undefined): string => { const num = Number(value); if (isNaN(num) || num < 1) return "1"; if (max !== undefined && num > max) return String(max); return value; }; const getPreviewMass = (tab: "deposit" | "withdraw" | "transfer"): number | null => { if (!maxMass) return null; if (tab === "deposit") { const resource = findResource(depositResourceId); if (!resource) return null; const amt = Number(depositAmount) || 0; return currentMass + (resource.massPerUnit ?? 0) * amt; } if (tab === "withdraw") { const stored = findStoredResource(withdrawResourceId); if (!stored) return null; const amt = Number(withdrawAmount) || 0; return currentMass - (stored.resource.massPerUnit ?? 0) * amt; } if (tab === "transfer") { const stored = findStoredResource(transferResourceId); if (!stored) return null; const amt = Number(transferAmount) || 0; return currentMass - (stored.resource.massPerUnit ?? 0) * amt; } return null; }; const renderCapacityPreview = (tab: "deposit" | "withdraw" | "transfer") => { if (!maxMass) return null; const previewMass = getPreviewMass(tab); if (previewMass === null) return null; const previewPercent = Math.min((previewMass / maxMass) * 100, 100); const delta = previewMass - currentMass; const isOverCapacity = previewMass > maxMass; const getColor = (pct: number) => { if (pct > 85) return "text-red-400"; if (pct > 60) return "text-yellow-400"; return "text-green-400"; }; const getBarColor = (pct: number) => { if (pct > 85) return "[&>div]:bg-red-500"; if (pct > 60) return "[&>div]:bg-yellow-500"; return "[&>div]:bg-green-500"; }; return (
{tab === "deposit" ? "After deposit" : tab === "withdraw" ? "After withdrawal" : "After transfer"} {Math.round(previewPercent)}%
{currentMass.toLocaleString()} kg → {previewMass.toLocaleString()} kg {delta >= 0 ? "+" : ""} {delta.toLocaleString()} kg {isOverCapacity && " (over capacity!)"}
); }; const isResourceAllowed = (resourceId: number): boolean => { if (!structureType) return true; if (isResourceProhibited(structureType, resourceId)) return false; return isResourceWhitelisted(structureType, resourceId); }; const handleDeposit = () => { if (!depositResourceId) return; const resId = Number(depositResourceId); const amt = Number(depositAmount); if (isNaN(amt) || amt <= 0) { setMessage({ type: "error", text: "Amount must be a positive number." }); return; } startTransition(async () => { const result = await addResource(structureId, resId, amt); if (result.success) { setMessage({ type: "success", text: "Resource deposited successfully." }); setDepositAmount("1"); await fetchData(); router.refresh(); } else { setMessage({ type: "error", text: result.error ?? "Unknown error." }); } }); }; const handleWithdraw = () => { if (!withdrawResourceId) return; const resId = Number(withdrawResourceId); const amt = Number(withdrawAmount); if (isNaN(amt) || amt <= 0) { setMessage({ type: "error", text: "Amount must be a positive number." }); return; } startTransition(async () => { const result = await removeResource(structureId, resId, amt); if (result.success) { setMessage({ type: "success", text: "Resource withdrawn successfully." }); setWithdrawAmount("1"); await fetchData(); router.refresh(); } else { setMessage({ type: "error", text: result.error ?? "Unknown error." }); } }); }; const handleTransfer = () => { if (!transferToId || !transferResourceId) return; const destId = Number(transferToId); const resId = Number(transferResourceId); const amt = Number(transferAmount); if (isNaN(amt) || amt <= 0) { setMessage({ type: "error", text: "Amount must be a positive number." }); return; } startTransition(async () => { const result = await transferResource(structureId, destId, resId, amt); if (result.success) { setMessage({ type: "success", text: "Transfer completed successfully." }); setTransferAmount("1"); await fetchData(); router.refresh(); } else { const errorText = result.error ?? "Unknown error."; // Detect distance error — offer shipment flow if (errorText.includes("too far apart")) { setShipmentMode(true); setShipmentResourceId(transferResourceId); setShipmentAmount(transferAmount); setShipmentDestId(transferToId); setMessage({ type: "error", text: "These structures are too far for instant transfer. Create a shipment instead?", }); } else { setMessage({ type: "error", text: errorText }); } } }); }; const handleCreateShipment = () => { if (!shipmentDestId || !shipmentResourceId || !shipmentVehicleId) return; const destId = Number(shipmentDestId); const resId = Number(shipmentResourceId); const vehicleId = Number(shipmentVehicleId); const amt = Number(shipmentAmount); if (isNaN(amt) || amt <= 0) { setMessage({ type: "error", text: "Amount must be a positive number." }); return; } startTransition(async () => { const result = await createShipment({ originId: structureId, destinationId: destId, vehicleId, cargo: [{ relationTo: "resources", value: resId, amount: amt }], }); if (result.success) { setMessage({ type: "success", text: "Shipment created and dispatched!" }); setShipmentMode(false); setShipmentAmount("1"); await fetchData(); router.refresh(); } else { setMessage({ type: "error", text: result.error ?? "Failed to create shipment." }); } }); }; return ( Manage Storage {structure?.name ?? "Loading..."} {loading ? (
) : shipmentMode ? ( /* ── Shipment Creation Mode ─────────────────────── */
Create Shipment
From {structure?.name}
To {allStructures.find((s) => s.id === Number(shipmentDestId))?.name ?? "Unknown"}
Cargo {shipmentAmount}×{" "} {allResources.find((r) => r.id === Number(shipmentResourceId))?.name ?? "Unknown"}
{availableVehicles.length > 0 ? ( { const vType = typeof v.type === "object" ? v.type : null; return { value: String(v.id), label: `${v.name} (${vType?.name ?? "Unknown"}), ${v.currentFuel?.toFixed(0)} fuel`, }; })} /> ) : (
No idle vehicles stationed at this structure. Deploy a vehicle here first to create shipments.
)}
{shipmentVehicleId && (() => { const selectedVehicle = availableVehicles.find( (v) => v.id === Number(shipmentVehicleId), ); const vType = selectedVehicle && typeof selectedVehicle.type === "object" ? selectedVehicle.type : null; if (!selectedVehicle || !vType) return null; const destStructure = allStructures.find((s) => s.id === Number(shipmentDestId)); const originCoords = structure?.coordinates as [number, number] | null; const destCoords = destStructure?.coordinates as [number, number] | null; let distance = 0; if (originCoords && destCoords) { distance = Math.sqrt( (destCoords[0] - originCoords[0]) ** 2 + (destCoords[1] - originCoords[1]) ** 2, ); } const consumptionRate = vType.fuel?.fuelConsumptionRate ?? 1; const fuelCost = Math.ceil(distance * consumptionRate); const speed = vType.maxSpeedOnRoad ?? vType.maxSpeedOffRoad ?? vType.maxAirspeed ?? 50; const transitHours = distance / speed; const canAfford = (selectedVehicle.currentFuel ?? 0) >= fuelCost; return (
Distance {distance.toFixed(1)} units
Fuel Cost {fuelCost} units
Transit Time {transitHours < 1 ? `${Math.ceil(transitHours * 60)} min` : `${transitHours.toFixed(1)} hr`}
Vehicle Fuel {selectedVehicle.currentFuel?.toFixed(1)} / {vType.fuel?.fuelCapacity}
{!canAfford && (
Vehicle does not have enough fuel for this trip. Need {fuelCost} units, has {selectedVehicle.currentFuel?.toFixed(1)}.
)}
); })()}
) : ( <> {maxMass && (
Current capacity {Math.round(Math.min((currentMass / maxMass) * 100, 100))}%
{currentMass.toLocaleString()} kg used {maxMass.toLocaleString()} kg max
)} {canManage && ( Deposit )} {canManage && ( Withdraw )} {canTransfer && ( Transfer )} {canManage && (
{ setDepositResourceId(val); setDepositAmount( clampAmount(depositAmount, getDepositLimitInfoFor(val)?.max), ); }} placeholder="Select a resource..." searchPlaceholder="Search resources..." emptyMessage="Nothing may be deposited here." options={allResources .filter((r) => isResourceAllowed(r.id)) .map((resource) => { const cap = getAllowedDepositCap(resource.id); return { value: String(resource.id), label: resource.name, hint: cap !== undefined ? `${cap.toLocaleString()} max` : undefined, }; })} />
setDepositAmount(clampAmount(e.target.value, getDepositMax())) } /> {getDepositLimitInfoFor(depositResourceId) !== undefined && ( Max: {getDepositLimitInfoFor(depositResourceId)!.max.toLocaleString()}{" "} (limited by {getDepositLimitInfoFor(depositResourceId)!.reason}) )}
{renderCapacityPreview("deposit")}
)} {canManage && (
{ setWithdrawResourceId(val); const stored = findStoredResource(val); setWithdrawAmount(clampAmount(withdrawAmount, stored?.amount)); }} placeholder="Select a resource..." searchPlaceholder="Search resources..." emptyMessage="Nothing stored yet." options={storedResources .map((entry) => { const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return null; return { value: String(resource.id), label: `${resource.name} (${entry.amount.toLocaleString()} ${pluralize(entry.amount, resource.unitOfMeasure)})`, }; }) .filter((option) => option !== null)} />
setWithdrawAmount(clampAmount(e.target.value, getWithdrawMax())) } /> {getWithdrawMax() !== undefined && ( Max: {getWithdrawMax()!.toLocaleString()} stored )}
{renderCapacityPreview("withdraw")}
)} {canTransfer && (
{ setTransferToId(val); setTransferAmount(clampAmount(transferAmount, getTransferMaxFor(val))); }} placeholder="Select a structure..." searchPlaceholder="Search structures..." options={allStructures.map((s) => ({ value: String(s.id), label: s.name, }))} />
{ setTransferResourceId(val); const stored = findStoredResource(val); setTransferAmount(clampAmount(transferAmount, stored?.amount)); }} placeholder="Select a resource..." searchPlaceholder="Search resources..." emptyMessage="Nothing stored yet." options={storedResources .map((entry) => { const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return null; return { value: String(resource.id), label: `${resource.name} (${entry.amount.toLocaleString()} ${pluralize(entry.amount, resource.unitOfMeasure)})`, }; }) .filter((option) => option !== null)} />
setTransferAmount(clampAmount(e.target.value, getTransferMax())) } /> {getTransferMax() !== undefined && ( Max: {getTransferMax()!.toLocaleString()} stored )} {transferToId && transferResourceId && (() => { const dest = allStructures.find((s) => s.id === Number(transferToId)); const destType = dest?.type; if (!dest || !destType) return null; const stored = findStoredResource(transferResourceId); if (!stored) return null; const resId = stored.resource.id; if (isResourceProhibited(destType, resId)) { return ( Destination prohibits this resource. ); } if (!isResourceWhitelisted(destType, resId)) { return ( Destination only accepts items on its allowed storage list. This resource is not permitted. ); } const cap = getResourceStorageCap(destType, resId); const destStored = getStoredAmountOf(dest, resId); return ( {cap != null ? `Destination allowance: ${Math.max(0, cap - destStored).toLocaleString()} / ${cap.toLocaleString()}` : "Destination has no per-item storage limit"} ); })()}
{renderCapacityPreview("transfer")}
)}
)} {message && (
{message.text}
)}
); }