Files
SpaceWanderer/components/team-builder.tsx
T

88 lines
4.6 KiB
TypeScript

'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'
// 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 default powerOf — the same call
// 5a's route makes, so it equals the stored duration. 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<number | null>(null)
const [helperIds, setHelperIds] = useState<number[]>([])
// 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
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) : 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 (
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<span style={{ fontSize: '0.6rem', color: 'var(--text-muted)', letterSpacing: '0.12em' }}>PICK TEAM // {type}</span>
{blocked ? (
<span style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>
{inTransit ? `IN TRANSIT // ${inTransit.targetLabel}` : 'A JUMP IS ALREADY QUEUED'}
</span>
) : (
roster.map((c) => (
<label key={c.id} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<input
type="radio"
name="lead"
checked={leadId === c.id}
disabled={leadReason(c) !== null}
onChange={() => {
setLeadId(c.id)
setHelperIds((h) => h.filter((x) => x !== c.id)) // a lead is never also a helper of the same task
}}
/>
<input type="checkbox" checked={helperIds.includes(c.id)} disabled={leadId === c.id || helperReason(c) !== null} onChange={() => setHelperIds((h) => (h.includes(c.id) ? h.filter((x) => x !== c.id) : [...h, c.id]))} />
<span style={{ fontSize: '0.65rem', color: leadId === c.id ? 'var(--text-secondary)' : 'var(--text-muted)', letterSpacing: '0.08em' }}>
{c.name}
{leadId === c.id ? ' // LEAD' : ''} {leadReason(c) ?? helperReason(c) ?? ''}
</span>
</label>
))
)}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
{duration !== null ? `WILL TAKE ${duration} TURN(S)` : 'PICK A LEAD'}
</span>
<Button variant="EXEC" size="SM" className="ml-auto" disabled={!lead || blocked !== null || leadReason(lead) !== null} onClick={() => lead && onQueue(lead.id, helperIds)}>
DISPATCH
</Button>
</div>
</div>
)
}