'use client' import { useState } from 'react' import { Button } from '@/components/ui/button' import type { CrewDto, QueueDto } from '@/lib/game/save' import { computeDuration, type TaskType } from '@/lib/game/tasks' import { makePowerOf, type SkillRow } from '@/lib/game/skills' // Shared crew picker for the four action panels — PR A: an action is queued as a lead + // helpers task, not a one-click. Self-contained: the panel passes the /api/crew CrewState // it fetched (roster + queue, Task 6) and onQueue hands back the picked team; the panel // performs its own fetch with the team fields in the body (Task 5a). The live preview is // Task 1's computeDuration (pure, no DB import) with the skill-band powerOf — Task 14's // makePowerOf over the roster's { skill, level } rows (R-P3: the same computation the // queueing routes make, so the preview equals the stored duration); each member row shows // that member's power contribution for the armed action's skill. The rows mirror Task 3's // queueTask so the UI never offers a 409 (JC-11's fields make the member↔row check exact). export function TeamBuilder({ type, hopCount, roster, tasks, onQueue }: { type: TaskType hopCount?: number roster: CrewDto[] tasks: QueueDto[] onQueue: (leadCrewId: number, helperCrewIds: number[]) => void }) { const [leadId, setLeadId] = useState(null) const [helperIds, setHelperIds] = useState([]) // Whole-builder blocks: an ACTIVE jump is in transit (all types); in the jump panel any // queued jump is — the second-jump 409 (JC-12). The JUMP branch coalesces find()'s // undefined to null so blocked is strictly QueueDto | null: the roster ternary below tests // truthiness, but the button guard compares to null — undefined slipped past it and // disabled every empty-queue JUMP dispatch (v0.10.1 e2e jump regression). const inTransit = tasks.find((t) => t.type === 'JUMP' && t.status === 'ACTIVE') const queuedJump = type === 'JUMP' ? (tasks.find((t) => t.type === 'JUMP') ?? null) : null const blocked = inTransit ?? queuedJump // R-P3: the preview's powerOf is the same makePowerOf the queueing routes call (Task 14) — // built from the roster's { skill, level } rows (JC-26), so the estimate equals the stored // duration instead of assuming every member is power 1. const skillRows: SkillRow[] = roster.flatMap((c) => c.skills.map((s) => ({ saveId: '', crewId: String(c.id), skill: s.skill, level: s.level, xp: 0 }))) const powerOf = makePowerOf(roster, skillRows, type) const lead = roster.find((c) => c.id === leadId) ?? null const helpers = roster.filter((c) => helperIds.includes(c.id)) const duration = lead ? computeDuration(type, hopCount ?? 0, lead, helpers, powerOf) : null // CrewDto ⊇ CrewMember const leadReason = (c: CrewDto) => { const leads = tasks.find((x) => x.leadCrewId === c.id && x.status === 'PENDING') if (leads) return `${leads.type} ALREADY QUEUED` const helps = tasks.find((x) => x.helperCrewIds.includes(c.id) && x.status === 'PENDING') return helps ? `COMMITTED: ${helps.type}` : null } const helperReason = (c: CrewDto) => { const t = tasks.find((x) => (x.leadCrewId === c.id || x.helperCrewIds.includes(c.id)) && (x.status === 'ACTIVE' || x.status === 'PENDING')) return t ? `BUSY: ${t.type}` : null } return (
PICK TEAM // {type} {blocked ? ( {inTransit ? `IN TRANSIT // ${inTransit.targetLabel}` : 'A JUMP IS ALREADY QUEUED'} ) : ( roster.map((c) => ( )) )}
{duration !== null ? `WILL TAKE ${duration} TURN(S)` : 'PICK A LEAD'}
) }