feat(ui): team builder — lead + helpers with live duration preview, wired into the action panels (#44)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
'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).
|
||||
const inTransit = tasks.find((t) => t.type === 'JUMP' && t.status === 'ACTIVE')
|
||||
const blocked = inTransit ?? (type === 'JUMP' ? tasks.find((t) => t.type === 'JUMP') : null)
|
||||
|
||||
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 t = tasks.find((x) => x.leadCrewId === c.id && x.status === 'PENDING')
|
||||
return t ? `${t.type} ALREADY QUEUED` : 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user