"use client"; import * as React from "react"; import Link from "next/link"; import { cn, pluralize } from "@/lib/utils"; import type { RosterAssignment, RosterMember } from "@/app/(frontend)/roster/page"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Building2Icon, CrownIcon, ListIcon, NetworkIcon, ShieldIcon } from "lucide-react"; const RosterDisplayContext = React.createContext<"lead" | "actual">("lead"); const TYPE_ORDER: Record = { division: 0, squad: 1, team: 2, special: 3, }; const TYPE_LABELS: Record = { division: "Division", squad: "Squad", team: "Team", special: "Special", }; type TreeNode = RosterAssignment & { children: TreeNode[]; members: RosterMember[] }; function buildTree(assignments: RosterAssignment[], members: RosterMember[]): TreeNode[] { const nodeMap = new Map(); for (const a of assignments) { nodeMap.set(a.id, { ...a, children: [], members: [] }); } // Wire the explicit hierarchy from subAssignments. Drop any edge that // would create a cycle (a unit cannot contain itself) or that points at // an assignment that wasn't fetched. const childIds = new Set(); const visited = new Set(); const inStack = new Set(); const wire = (node: TreeNode): void => { if (visited.has(node.id) || inStack.has(node.id)) return; inStack.add(node.id); for (const subId of node.subAssignmentIds ?? []) { const child = nodeMap.get(subId); if (!child || inStack.has(child.id)) continue; node.children.push(child); childIds.add(child.id); wire(child); } inStack.delete(node.id); visited.add(node.id); }; for (const node of nodeMap.values()) wire(node); const roots = assignments.filter((a) => !childIds.has(a.id)).map((a) => nodeMap.get(a.id)!); // Each node only holds the members/leader it claims directly — anyone a // descendant also claims renders at the deepest unit only. const descendantClaimed = (node: TreeNode): Set => { const ids = new Set(); for (const child of node.children) { for (const id of [...child.memberIds, child.leaderId]) { if (id != null) ids.add(id); } for (const id of descendantClaimed(child)) ids.add(id); } return ids; }; for (const a of assignments) { const node = nodeMap.get(a.id)!; const claimed = descendantClaimed(node); node.members = members.filter( (m) => (a.memberIds.includes(m.user.id) || a.leaderId === m.user.id) && !claimed.has(m.user.id), ); } const byOrder = (x: TreeNode, y: TreeNode) => (TYPE_ORDER[x.type] ?? 99) - (TYPE_ORDER[y.type] ?? 99) || x.name.localeCompare(y.name); for (const node of nodeMap.values()) node.children.sort(byOrder); return roots.sort(byOrder); } function collectAllMembers(node: TreeNode): RosterMember[] { const result = [...node.members]; for (const child of node.children) { result.push(...collectAllMembers(child)); } return result; } // ─── Org Chart (top-down ORBAT tree) ──────────────────────────────── function OrgChartView({ assignments, members, }: { assignments: RosterAssignment[]; members: RosterMember[]; }) { const roots = buildTree(assignments, members); const assignedIds = new Set( assignments.flatMap((a) => [...a.memberIds, a.leaderId]).filter(Boolean), ); const unassigned = members.filter((m) => !assignedIds.has(m.user.id)); return (
{roots.length > 0 ? (
{roots.map((root) => ( ))}
) : (

No assignments configured.

)} {unassigned.length > 0 && (

Unassigned

{unassigned.map((m) => ( ))}
)}
); } function OrgChartNode({ node, depth }: { node: TreeNode; depth: number }) { const allMembers = collectAllMembers(node); const leader = node.leaderId ? allMembers.find((m) => m.user.id === node.leaderId) : null; const directMembers = node.members.filter((m) => m.user.id !== node.leaderId); return (
{node.children.length > 0 && ( <>
{node.children.map((child) => (
))}
)}
); } function UnitBox({ node, depth, leader, directMembers, memberCount, }: { node: TreeNode; depth: number; leader: RosterMember | null | undefined; directMembers: RosterMember[]; memberCount: number; }) { const depthBorders = ["border-l-blue-600", "border-l-amber-600", "border-l-emerald-600"]; const borderClass = depthBorders[depth] ?? "border-l-zinc-600"; return (
{node.name} {TYPE_LABELS[node.type] ?? node.type} {node.callsign && ( [{node.callsign}] )} {memberCount} {pluralize(memberCount, "member")}
{leader && (
Commanding
)} {directMembers.length > 0 && (
{directMembers.map((m) => ( ))}
)}
); } // ─── By Assignment ────────────────────────────────────────────────── function ByAssignmentView({ assignments, members, }: { assignments: RosterAssignment[]; members: RosterMember[]; }) { const grouped = new Map(); for (const m of members) { const key = m.assignmentName ?? "__unassigned"; const list = grouped.get(key) ?? []; list.push(m); grouped.set(key, list); } const sorted = [...grouped.entries()].sort(([a], [b]) => { if (a === "__unassigned") return 1; if (b === "__unassigned") return -1; const aType = assignments.find((asgn) => asgn.name === a)?.type ?? ""; const bType = assignments.find((asgn) => asgn.name === b)?.type ?? ""; return (TYPE_ORDER[aType] ?? 99) - (TYPE_ORDER[bType] ?? 99) || a.localeCompare(b); }); return (
{sorted.map(([key, groupMembers]) => { const assignment = assignments.find((a) => a.name === key); return (

{key === "__unassigned" ? "Unassigned" : key}

{assignment && ( {TYPE_LABELS[assignment.type] ?? assignment.type} )} {assignment?.callsign && ( [{assignment.callsign}] )} {groupMembers.length}
{groupMembers.map((m) => ( ))}
); })}
); } // ─── By Rank ──────────────────────────────────────────────────────── function ByRankView({ members }: { members: RosterMember[] }) { const grouped = new Map(); for (const m of members) { const list = grouped.get(m.rank.name) ?? []; list.push(m); grouped.set(m.rank.name, list); } const sorted = [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)); return (
{sorted.map(([rankName, groupMembers]) => { const first = groupMembers[0]; return (
{first.rank.insigniaUrl && ( {rankName} )}

{rankName}

({first.rank.abbreviation}) {groupMembers.length}
{groupMembers.map((m) => ( ))}
); })}
); } // ─── Alphabetical ─────────────────────────────────────────────────── function AlphabeticalView({ members }: { members: RosterMember[] }) { const sorted = [...members].sort((a, b) => a.user.displayName.localeCompare(b.user.displayName)); return (
{sorted.map((m) => ( ))}
); } // ─── Member Card ──────────────────────────────────────────────────── function MemberCard({ member }: { member: RosterMember }) { const leadOrActual = React.useContext(RosterDisplayContext); const leadLabel = leadOrActual === "actual" ? "Actual" : "Lead"; return ( {member.rank.insigniaUrl && ( {member.rank.name} )}
{member.user.displayName} {member.rank.abbreviation} · @{member.user.username}
{member.isLeader && ( {leadLabel} )} {member.assignmentName && ( {member.assignmentCallsign ? `[${member.assignmentCallsign}]` : member.assignmentName} )}
); } // ─── Main RosterView ──────────────────────────────────────────────── export function RosterView({ members, assignments, leadOrActual = "lead", }: { members: RosterMember[]; assignments: RosterAssignment[]; leadOrActual?: "lead" | "actual"; }) { return ( Org Chart By Assignment By Rank A–Z ); }