import config from "@payload-config"; import { getPayload } from "payload"; import { notFound } from "next/navigation"; import { headers as nextHeaders } from "next/headers"; import Link from "next/link"; import { Faction, GameStructure, Resource, Structure } from "@/payload-types"; import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext"; import { CapacityBar } from "@/components/frontend/storage/CapacityBar"; import { StorageRules } from "@/components/frontend/storage/StorageRules"; import { StructureStorageView } from "@/components/frontend/storage/StructureStorageView"; import { StaffRoster } from "@/components/frontend/baseManagement/StaffRoster"; import { UpgradeCard } from "@/components/frontend/baseManagement/UpgradeCard"; import { calculateStoredMass, calculateUtilization, getCategoryLabel, getFunctionTypeLabel, getGridSlots, } from "@/lib/logistics"; import { pluralize } from "@/lib/utils"; import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base"; import { currencyConfigFromGameRules } from "@/lib/banking"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { ArrowLeftIcon, BoxIcon, Building2Icon, CoinsIcon, MapPinIcon, WrenchIcon, } from "lucide-react"; import { Item, ItemContent, ItemDescription, ItemHeader, ItemMedia, ItemTitle, } from "@/components/ui/item"; import { ManageStorageButton } from "@/components/frontend/storage/ManageStorageButton"; interface StructureDetailPageProps { params: Promise<{ id: string }>; } export default async function StructureDetailPage({ params }: StructureDetailPageProps) { const { id } = await params; const payloadConfig = await config; const payload = await getPayload({ config: payloadConfig }); const headers = await nextHeaders(); const { user } = await payload.auth({ headers, canSetHeaders: false }); const structureRes = await payload .findByID({ collection: "game-structures", id: Number(id), depth: 2, }) .catch(() => null); if (!structureRes) { notFound(); } const structure = structureRes as unknown as GameStructure & { type: Structure; faction?: Faction | null; }; const structureType = structure.type; const storedResources = structure.storedResources ?? []; const currentMass = calculateStoredMass(storedResources); const utilization = calculateUtilization(structureType, storedResources); const gridSlots = getGridSlots(structureType); const functionLabel = getFunctionTypeLabel(structureType.functionType); const description = structureType.description ? convertLexicalToPlaintext({ data: structureType.description }) : null; const image = typeof structureType.image === "object" && structureType.image !== null ? structureType.image : null; const imageUrl = image?.url ?? null; const upgradeTarget = structureType.upgradesInto; const upgradeTargetId = typeof upgradeTarget === "object" && upgradeTarget !== null ? upgradeTarget.id : upgradeTarget; let upgrade: { targetName: string; materials: { name: string; needed: number; have: number }[]; } | null = null; if (typeof upgradeTargetId === "number") { const targetRes = await payload .findByID({ collection: "structures", id: upgradeTargetId, depth: 1 }) .catch(() => null); if (targetRes) { const target = targetRes as unknown as Structure; const resourceIds = target.materials .map((material) => typeof material.resource === "object" && material.resource !== null ? material.resource.id : material.resource, ) .filter((resourceId): resourceId is number => typeof resourceId === "number"); const resourceRes = resourceIds.length > 0 ? await payload.find({ collection: "resources", where: { id: { in: resourceIds } }, limit: resourceIds.length, depth: 0, }) : null; const resourceNames = new Map( (resourceRes?.docs ?? []).map((resource) => [resource.id, resource.name]), ); const inventory = [...(structure.storedResources ?? []), ...(structure.voidStorage ?? [])]; upgrade = { targetName: target.name, materials: target.materials.map((material) => { const resourceId = typeof material.resource === "object" && material.resource !== null ? material.resource.id : material.resource; const resourceName = typeof material.resource === "object" && material.resource !== null ? material.resource.name : (resourceNames.get(resourceId) ?? `Resource #${resourceId}`); const have = inventory.reduce((total, entry) => { const entryResourceId = typeof entry.resource === "object" && entry.resource !== null ? entry.resource.id : entry.resource; return entryResourceId === resourceId ? total + entry.amount : total; }, 0); return { name: resourceName, needed: Math.ceil((material.amount ?? 0) * (structureType.upgradeCostMultiplier ?? 1)), have, }; }), }; } } const staffRes = await payload.find({ collection: "npc-staffing", where: { and: [{ structure: { equals: structure.id } }, { status: { equals: "active" } }], }, limit: 50, depth: 1, }); const staff = staffRes.docs.map((entry) => { const npc = entry.npc; const npcName = typeof npc === "object" && npc !== null ? npc.name : typeof npc === "number" ? `NPC #${npc}` : null; const laborClassificationId = typeof entry.laborClassification === "object" && entry.laborClassification !== null ? entry.laborClassification.id : typeof entry.laborClassification === "number" ? entry.laborClassification : null; return { id: entry.id, npcName, positionType: entry.positionType, positionTitle: entry.positionTitle, salary: entry.salary ?? 0, headcount: entry.headcount ?? 1, laborClassificationId, hiredAt: new Date(entry.hiredAt ?? entry.createdAt).toISOString(), }; }); const canManage = await hasLogisticsQualification(payload, user); const classificationsRes = await payload.find({ collection: "labor-classifications", limit: 100, depth: 0, }); const classifications: LaborClassificationOption[] = classificationsRes.docs.map((doc) => ({ id: doc.id, name: doc.name, key: doc.key, defaultUnitSalary: doc.defaultUnitSalary ?? null, availableHeadcount: doc.availableHeadcount ?? 0, })); const rules = await payload.findGlobal({ slug: "game-rules", depth: 1, overrideAccess: true, }); const currencyConfig = currencyConfigFromGameRules(rules); const structureStaffing: StructureStaffingOption = { id: structure.id, name: structure.name, maintenanceCost: structure.maintenanceCost ?? 0, laborRequirements: (structure.laborRequirements ?? []).map((requirement) => { const classificationId = typeof requirement.classification === "object" ? requirement.classification.id : requirement.classification; return { classificationId, requiredHeadcount: requirement.requiredHeadcount, maxHeadcount: requirement.maxHeadcount ?? 0, enforcedSalary: requirement.enforcedSalary ?? null, currentHeadcount: staff .filter((member) => member.laborClassificationId === classificationId) .reduce((total, member) => total + member.headcount, 0), }; }), staffPositions: (structure.staffPositions ?? []).map((position) => ({ positionType: position.positionType, max: position.max ?? null, enforcedSalary: position.enforcedSalary ?? null, currentHeadcount: staff.filter( (member) => member.npcName !== null && member.positionType === position.positionType, ).length, })), }; const npcsRes = await payload.find({ collection: "game-npcs", limit: 200, depth: 0, }); const npcs = npcsRes.docs.map((npc) => ({ id: npc.id, name: npc.name })); const totalValue = storedResources.reduce((sum, entry) => { const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return sum; return sum + (resource.baseValue ?? 0) * entry.amount; }, 0); return (
{description}
}