const EMPTY_TQ = () => ({ rArm:{active:false,type:'',time:'',position:''}, lArm:{active:false,type:'',time:'',position:''}, rLeg:{active:false,type:'',time:'',position:''}, lLeg:{active:false,type:'',time:'',position:''} });
const BLANK_MEMBER = () => ({
status:'auto', ammo:'green', equip:'green',
service:'USA', unit:'ODA 0312', allergies:'NKDA', evac:'', triage:'', gender:'M', weight:85,
mechanism:[], acuteFindings:[], injuries:[], burnedRegions:[], tourniquets:EMPTY_TQ(),
vitals:[], draftVitals:{hr:'',sbp:'',dbp:'',rr:'',spo2:'',pain:''}, avpu:'A',
interventions:[], medsGiven:[], fluidsGiven:[], notes:'',
burn:{ formula:'parkland', burnTime:'', inhal:false, circ:false, uop:[], care:[] },
pain:{ rung:'', notes:'' }, abx:{}, tbi:{}, hypo:{},
});
const m = (o) => ({ ...BLANK_MEMBER(), ...o });
const ODA_ROSTER = [
m({ id:'18A',  role:'18A',  title:'Detachment Cdr',  name:'HALVORSEN, M.', last4:'2210', bloodType:'O+',  pos:6,  weight:88 }),
m({ id:'180A', role:'180A', title:'Asst Det Cdr',    name:'REYES, D.',     last4:'7731', bloodType:'A-',  pos:7,  weight:82, ammo:'amber' }),
m({ id:'18Z',  role:'18Z',  title:'Team Sergeant',   name:'BOUDREAUX, T.', last4:'0954', bloodType:'O-',  pos:5,  weight:94 }),
m({ id:'18F',  role:'18F',  title:'Intel Sgt',       name:'NAKAMURA, K.',  last4:'3318', bloodType:'B+',  pos:8,  weight:79 }),
m({ id:'18B1', role:'18B',  title:'Weapons Sgt',     name:'OKAFOR, J.',    last4:'4521', bloodType:'A+',  pos:1,  weight:90, evac:'urgent', triage:'immediate',
mechanism:['GSW','Blast'], acuteFindings:[], allergies:'PCN',
injuries:[{view:'front',x:78,y:295,type:'gsw'},{view:'front',x:130,y:200,type:'gsw'}],
tourniquets:{ ...EMPTY_TQ(), rLeg:{active:true,type:'CAT',time:'1342',position:'Proximal'} },
vitals:[{time:'1338',hr:'128',sbp:'92',dbp:'60',rr:'22',spo2:'94',pain:'8'},{time:'1345',hr:'118',sbp:'98',dbp:'64',rr:'20',spo2:'96',pain:'6'},{time:'1352',hr:'110',sbp:'104',dbp:'68',rr:'18',spo2:'97',pain:'4'}],
draftVitals:{hr:'110',sbp:'104',dbp:'68',rr:'18',spo2:'97',pain:'4'}, avpu:'V',
interventions:['M:Tourniquet applied','M:TXA 2g IV/IO','C:IV 18g established','C:Whole blood started','H:HPMK applied','P:Ketamine IV/IO 20–30 mg','A:Ertapenem 1 g IV/IM'],
medsGiven:[{id:1,time:'1343',name:'Fentanyl OTFC',dose:'800 mcg'},{id:2,time:'1346',name:'TXA',dose:'2 g IV'},{id:3,time:'1350',name:'Ketamine IV',dose:'30 mg'}],
fluidsGiven:[{id:1,time:'1348',type:'Whole Blood',vol:'500'}],
pain:{rung:'severe'}, abx:{erta:true},
notes:'1340: GSW R thigh + L flank. CAT prox R thigh; bleeding controlled. IV L AC 18g. TXA 2g. Permissive hypotension SBP 90+. HPMK on.' }),
m({ id:'18B2', role:'18B',  title:'Weapons Sgt',     name:'STRAND, E.',    last4:'6602', bloodType:'O+',  pos:12, weight:86, ammo:'red' }),
m({ id:'18C1', role:'18C',  title:'Engineer Sgt',    name:'DELACROIX, P.', last4:'1187', bloodType:'AB+', pos:2,  weight:84, evac:'priority', triage:'delayed',
mechanism:['IED','Burn','Blast'], injuries:[{view:'front',x:100,y:180,type:'shrap'}], burnedRegions:['f-rArm','f-chest','f-head'],
vitals:[{time:'1341',hr:'112',sbp:'118',dbp:'76',rr:'24',spo2:'95',pain:'9'},{time:'1355',hr:'104',sbp:'122',dbp:'78',rr:'22',spo2:'96',pain:'6'}],
draftVitals:{hr:'104',sbp:'122',dbp:'78',rr:'22',spo2:'96',pain:'6'},
interventions:['A:Sit-up & lean-forward','R:SpO₂ monitored','C:IV 18g established','H:Remove wet / burned clothing','H:HPMK applied','W:Burns covered dry sterile','P:Fentanyl OTFC 800 mcg'],
medsGiven:[{id:1,time:'1344',name:'Fentanyl OTFC',dose:'800 mcg'}], fluidsGiven:[{id:1,time:'1350',type:'LR',vol:'500'}],
burn:{ formula:'rule10', burnTime:'1336', inhal:true, circ:false, uop:[], care:['Stop','Airway','Assess','Wounds','Hypothermia'] },
pain:{rung:'moderate'}, equip:'amber',
notes:'1338: IED strike lead vic. Flash/flame burns face, R arm, ant chest. Singed nasal hair — airway watch.' }),
m({ id:'18C2', role:'18C',  title:'Engineer Sgt',    name:'MBEKI, S.',     last4:'8840', bloodType:'B-',  pos:11, weight:91 }),
m({ id:'18D1', role:'18D',  title:'Medical Sgt',     name:'PHAM, R.',      last4:'5573', bloodType:'O-',  pos:4,  weight:80 }),
m({ id:'18D2', role:'18D',  title:'Medical Sgt',     name:'WHITLOCK, A.',  last4:'2296', bloodType:'A+',  pos:9,  weight:83 }),
m({ id:'18E1', role:'18E',  title:'Commo Sgt',       name:'CASTILLO, R.',  last4:'9015', bloodType:'O+',  pos:3,  weight:78, evac:'routine', triage:'minimal',
mechanism:['Blast'], avpu:'A',
vitals:[{time:'1350',hr:'96',sbp:'128',dbp:'82',rr:'16',spo2:'98',pain:'5'}], draftVitals:{hr:'96',sbp:'128',dbp:'82',rr:'16',spo2:'98',pain:'5'},
tbi:{ loc:true, aoc:true, pta:false, headache:true }, interventions:['H:TBI screen (MACE 2)','P:Acetaminophen 1 g PO + Meloxicam 15 mg PO'], pain:{rung:'mild'},
notes:'1339: ~3 m from IED blast, knocked down, dazed ~1 min. HA 5/10, no vomiting. Pupils equal.' }),
m({ id:'18E2', role:'18E',  title:'Commo Sgt',       name:'GRANT, L.',     last4:'4478', bloodType:'AB-', pos:10, weight:81, ammo:'amber' }),
];
function memberStatus(mb) {
if (mb.status && mb.status !== 'auto') return mb.status;
if (mb.triage === 'expectant') return 'black';
const tq = Object.values(mb.tourniquets||{}).some(t=>t.active);
if (mb.triage === 'immediate' || mb.evac === 'urgent' || tq || (mb.acuteFindings||[]).length) return 'red';
if ((mb.injuries||[]).length || (mb.burnedRegions||[]).length || mb.evac || mb.triage || (mb.tbi && Object.values(mb.tbi).some(Boolean))) return 'amber';
return 'green';
}
const STATUS_LABEL = { green:'Up', amber:'Wounded', red:'Urgent', black:'KIA/Exp' };
const STATUS_ORDER = ['green','amber','red','black'];
const RAG = ['green','amber','red'];
const cycle = (v, arr=RAG) => arr[(arr.indexOf(v)+1) % arr.length];
function Formation({ team, selectedId, onSelect }) {
const sorted = [...team].sort((a,b)=>a.pos-b.pos);
return (
<div className="formation" title="Order of movement — position 1 leads">
<svg viewBox="0 0 240 26">
<line x1="6" y1="13" x2="234" y2="13" stroke="var(--line-2)" strokeWidth="1" strokeDasharray="2 3"/>
{sorted.map((mb,i)=>{ const st=memberStatus(mb); const x=12+i*19.6; const sel=mb.id===selectedId;
return <g key={mb.id} className={'st-'+st} onClick={()=>onSelect(mb.id)} style={{cursor:'pointer'}}>
<circle cx={x} cy="13" r={sel?6.5:5} fill="var(--st)" stroke={sel?'var(--fg-0)':'none'} strokeWidth="1.5"/>
<text x={x} y="24.5" textAnchor="middle" fontSize="6" fill="var(--fg-3)" fontFamily="var(--mono)">{mb.pos}</text>
</g>; })}
</svg>
</div>
);
}
function ManTile({ mb, sel, onSelect, opts = {}, due }) {
const st = memberStatus(mb);
const tqN = Object.values(mb.tourniquets||{}).filter(t=>t.active).length;
const lv = (mb.vitals||[])[mb.vitals.length-1];
const evacC = mb.evac==='urgent'?'var(--crit)':mb.evac==='priority'?'var(--amber)':mb.evac==='routine'?'var(--op)':null;
const triC = mb.triage==='immediate'?'var(--crit)':mb.triage==='delayed'?'var(--amber)':mb.triage==='minimal'?'var(--op)':mb.triage==='expectant'?'#555':null;
return (
<div className={'man st-'+st+(sel?' sel':'')} onClick={()=>onSelect(mb.id)} data-screen-label={mb.role+' '+mb.name}>
{st==='red' && <span className="alert"></span>}
<div className="role">{mb.role}<span className="pos">P{mb.pos}</span></div>
<div className="nm">{mb.name || <span style={{color:'var(--fg-3)'}}>— unassigned —</span>}{opts.showLast4 !== false && mb.last4 && <span style={{color:'var(--fg-3)'}}> /{mb.last4}</span>}</div>
<div className="meta">
<span className="pill blood">{mb.bloodType}</span>
{mb.triage ? <span className="pill hot" style={{background:triC}}>{mb.triage.slice(0,3)}</span> : <span className="pill">{STATUS_LABEL[st]}</span>}
{mb.evac && <span className="pill hot" style={{background:evacC, color: mb.evac==='priority'?'#1a1410':mb.evac==='routine'?'#0c1410':'#fff'}}>{mb.evac.slice(0,3)}</span>}
{tqN>0 && <span className="pill hot" style={{background:'var(--crit)'}}>TQ×{tqN}</span>}
</div>
<div className="foot">
{opts.showAmmo !== false && <><span className={'st-'+mb.ammo} title="Ammo">AMMO</span><span className={'st-'+mb.equip} title="Equipment">EQP</span></>}
<span className={due?'st-red':lv?'st-green':'st-black'} style={{flex:1.3}} title="Last vitals">{lv?lv.time:'— —'}</span>
</div>
{due && <span className="due">{due}</span>}
</div>
);
}
function TeamStrip({ team, selectedId, onSelect, opts = {}, dueMap = {} }) {
const counts = STATUS_ORDER.reduce((a,k)=>({...a,[k]:0}),{});
team.forEach(mb=>counts[memberStatus(mb)]++);
const cas = team.filter(mb=>memberStatus(mb)!=='green').length;
const rank = { red:0, black:1, amber:2, green:3 };
const sorted = [...team].sort((a,b)=> opts.sortBy==='status' ? (rank[memberStatus(a)]-rank[memberStatus(b)]) || a.pos-b.pos : opts.sortBy==='role' ? a.id.localeCompare(b.id) : a.pos-b.pos);
return (
<div className="team" data-screen-label="Team strip">
<div className="team-sum">
<div className="lbl">ODA · 12 PAX · <b style={{color:cas?'var(--crit)':'var(--op)'}}>{cas} CAS</b></div>
<div className="counts">
{STATUS_ORDER.map(k=><div key={k} className={'cnt st-'+k}><b>{counts[k]}</b><span>{k==='black'?'KIA':k.toUpperCase()}</span></div>)}
</div>
{opts.showFormation !== false && <><div className="lbl" style={{marginTop:2}}>Order of Movement</div><Formation team={team} selectedId={selectedId} onSelect={onSelect}/></>}
</div>
<div className={'team-grid'+(opts.compactStrip?' compact':'')}>
{sorted.map(mb=><ManTile key={mb.id} mb={mb} sel={mb.id===selectedId} onSelect={onSelect} opts={opts} due={dueMap[mb.id]}/>)}
</div>
</div>
);
}
Object.assign(window, { ODA_ROSTER, BLANK_MEMBER, EMPTY_TQ, memberStatus, STATUS_LABEL, STATUS_ORDER, RAG, cycle, TeamStrip });