88 lines
4.6 KiB
TypeScript
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>
|
|
)
|
|
}
|