/* ============================================================
REQUEST DETAIL — role-aware, governance-enforced
============================================================ */
(function(){
const {Icon,Btn,StatusBadge,PriorityBadge,RoleBadge,Avatar,Field,Textarea,Select,Banner,Modal,PermDenied}=window;
const {useState}=React;
const G=()=>window.GOV;
const TARGET_LABEL={under_review:'Move to Under Review',needs_info:'Request Information',approved:'Approve',rejected:'Reject',closed:'Close case'};
const TARGET_VARIANT={approved:'primary',rejected:'danger',needs_info:'accent'};
function RequestDetail(){
const store=window.useStore();
const {state,me,route,go,sel,dispatch,toast,helpers}=store;
const req=sel.request(route.id);
const [reason,setReason]=useState(null); // {to}
const [reasonText,setReasonText]=useState('');
const [assignOpen,setAssignOpen]=useState(false);
const [noteScope,setNoteScope]=useState('partner');
const [noteText,setNoteText]=useState('');
const [respondText,setRespondText]=useState('');
const [respondFiles,setRespondFiles]=useState([]);
if(!req) return ;
if(!G().canViewRequest(me,req)) return ;
const isPartner=me.role==='partner';
const isTeam=!isPartner;
const settings=state.settings;
const allowed=G().transitionsFor(req.status, me, settings);
const canDecide=G().can(me,'change_status',settings);
const canRecommend=G().can(me,'recommend')&&!canDecide;
const canAssign=G().can(me,'assign_reviewer');
const canInternal=G().can(me,'add_internal_note');
const readOnly=['rejected','closed'].includes(req.status);
const notes=G().visibleNotes(me,req);
const files=G().visibleFiles(me,req);
const reviewer=req.reviewerId?sel.user(req.reviewerId):null;
const reviewers=state.users.filter(u=>u.role==='reviewer'&&u.status==='active');
function doStatus(to,reasonVal){
dispatch({type:'STATUS',reqId:req.id,to,reason:reasonVal||''});
toast({kind:'audit',icon:'shield',title:G().STATUS[to].label,desc:'Status changed · recorded to audit trail.'});
}
function clickTransition(to){
if(G().REASON_REQUIRED.includes(to)){ setReason({to}); setReasonText(''); }
else doStatus(to);
}
function confirmReason(){
if(!reasonText.trim()) return;
doStatus(reason.to,reasonText.trim());
setReason(null);
}
function assign(reviewerId){
dispatch({type:'ASSIGN',reqId:req.id,reviewerId});
setAssignOpen(false);
toast({kind:'audit',icon:'userCheck',title:'Reviewer assigned',desc:sel.userName(reviewerId)+' · recorded to audit.'});
}
function recommend(rec){
dispatch({type:'RECOMMEND',reqId:req.id,rec});
toast({kind:'audit',icon:'shield',title:'Recommendation sent',desc:'An admin will make the final decision.'});
}
function addNote(){
if(!noteText.trim()) return;
dispatch({type:'NOTE',reqId:req.id,scope:noteScope,body:noteText.trim()});
toast({title:noteScope==='internal'?'Internal note added':'Note added',desc:noteScope==='internal'?'Hidden from the partner.':'Visible to the partner.'});
setNoteText('');
}
function respond(){
if(!respondText.trim()) return;
dispatch({type:'RESPOND',reqId:req.id,body:respondText.trim(),files:respondFiles});
toast({kind:'success',title:'Response sent',desc:'Request moved back to Under Review.'});
setRespondText(''); setRespondFiles([]);
}
function download(file){
// real, scope-checked download (server enforces visibility + logs the audit)
window.open('/api/files/'+file.id,'_blank');
toast({title:'Download started',desc:file.name+(isTeam?' · logged to audit':'')});
}
function addRespondFile(){
const n=respondFiles.length+1;
setRespondFiles(x=>[...x,{name:'follow-up-'+n+'.pdf',type:'Additional report'}]);
}
const backTo=isPartner?'requests':(me.role==='reviewer'?'queue':'requests');
return
{/* header */}
go(backTo,{status:'all'})}>Back
{isTeam?sel.hospitalName(req.hospitalId):'Request'}
Submitted {helpers.fmtDate(req.createdAt)} by {sel.userName(req.submittedById)} · updated {helpers.timeAgo(req.updatedAt)}
{/* partner needs-info action */}
{isPartner && req.status==='needs_info' &&
A reviewer has asked for more details. Add a note (and any files) below to continue — this moves your request back to Under Review.
{respondFiles.length>0 &&
{respondFiles.map((f,i)=>
{f.name}setRespondFiles(x=>x.filter((_,j)=>j!==i))}> )}
}
Send response
Attach file
}
{/* ---------------- LEFT: case content ---------------- */}
{req.patient.ref}],
['Initials', req.patient.initials||'—'],
['Age', req.patient.age?req.patient.age+' yrs':'—'],
['Gender', req.patient.gender||'—'],
['Diagnosis', req.patient.diagnosis],
['Condition', req.patient.condition||'—'],
]}/>
Unassigned],
req.request.partnerNote?['Partner note', req.request.partnerNote]:null,
].filter(Boolean)}/>
{/* files */}
Files {files.length} file{files.length!==1?'s':''}
{files.length===0 &&
No files attached.
}
{files.map(file=>
{file.name}
{file.scope==='internal'&&Internal }
{file.type} · {sel.userName(file.by)} · {helpers.timeAgo(file.at)}
download(file)}>Download
)}
{/* notes */}
Notes
{isTeam&&You see internal notes }
{notes.length===0 &&
No notes yet.
}
{notes.map(n=>{const au=sel.user(n.authorId);return
{au?au.name:'—'}
{au&&}
{n.scope==='internal'&&Internal — hidden from partner }
{helpers.timeAgo(n.at)}
{n.body}
;})}
{/* composer */}
{isTeam && !readOnly &&
{canInternal &&
setNoteScope('partner')}>Partner-visible
setNoteScope('internal')}>Internal-only
}
}
{isPartner && !readOnly && req.status!=='needs_info' &&
}
{readOnly &&
This request is {G().STATUS[req.status].label.toLowerCase()} — notes are read-only.
}
{/* history */}
Status history
{[...req.history].reverse().map((h,i)=>
{h.from && <> >}
{sel.userName(h.byId)} · {helpers.fmtDate(h.at)}{h.reason?' · “'+h.reason+'”':''}
)}
{/* ---------------- RIGHT: action rail ---------------- */}
{isTeam &&
Case actions
{/* assignment */}
Assigned reviewer
{reviewer?
{reviewer.name} :
Unassigned }
{canAssign &&
setAssignOpen(true)}>{reviewer?'Reassign':'Assign'} }
{!canAssign &&
Only admins assign reviewers
}
{/* decision */}
{canDecide?'Change status':'Decision'}
{readOnly &&
{G().STATUS[req.status].label} — no further transitions{me.role!=='reviewer'?' except reopen':''}.
}
{!readOnly && canDecide &&
{allowed.map(to=>
clickTransition(to)}>{TARGET_LABEL[to]} )}
{allowed.length===0 &&
No transitions available.
}
}
{canDecide && allowed.includes('closed') && false}
{req.status==='closed' && canDecide &&
}
{/* reviewer recommend mode */}
{!readOnly && canRecommend &&
Reviewers recommend — an admin makes the final call
{req.recommendation &&
You recommended {req.recommendation.rec.toUpperCase()}
}
recommend('approve')}>Recommend approve
recommend('needs info')}>Recommend more info
recommend('reject')}>Recommend reject
}
{/* show reviewer recommendation to deciders */}
{canDecide && req.recommendation && <>
{sel.userName(req.recommendation.byId)} recommended {req.recommendation.rec.toUpperCase()}
>}
}
{/* who/meta for partner */}
{isPartner &&
Case summary
}/>
}/>
Pending assignment}/>
}
{/* governance: permissions on this case */}
{/* reason modal */}
setReason(null)} lg
title={reason&&reason.to==='rejected'?'Reject request':'Request more information'}
icon={reason&&reason.to==='rejected'?'ban':'message'}
iconColor={reason&&reason.to==='rejected'?'var(--st-rejected-fg)':'var(--bhf-orange)'}
footer={<>
setReason(null)}>Cancel
{reason&&reason.to==='rejected'?'Reject request':'Send to partner'}
>}>
A reason is required for this action. It will be {reason&&reason.to==='needs_info'?'shown to the partner and ':''}recorded in the status history and audit log.
{/* assign modal */}
setAssignOpen(false)} title="Assign reviewer" icon="userCheck">
{reviewers.map(r=>
assign(r.id)}>
{r.name}
{state.requests.filter(x=>x.reviewerId===r.id).length} active case(s)
{req.reviewerId===r.id && }
)}
;
}
function InfoCard({title,icon,rows}){
return ;
}
function Meta({k,v}){ return {k} {v}
; }
/* governance summary card */
function PermissionsCard({me,req,settings}){
const G=window.GOV;
const can=(a)=>G.can(me,a,settings);
const list=[
['View this case', true],
['Download files', true],
['Add partner-visible note', !['rejected','closed'].includes(req.status)],
['Add internal-only note', can('add_internal_note')],
['View internal notes', can('view_internal')],
['Assign reviewer', can('assign_reviewer')],
['Change status', can('change_status')],
me.role==='reviewer'?['Recommend a decision', true]:null,
].filter(Boolean);
return
Your permissions here
{me.role==='reviewer'&&{settings.reviewersCanDecide?'can decide (admin setting on)':'recommend-only'} }
{list.map((p,i)=>
{p[0]}
)}
;
}
window.RequestDetail=RequestDetail;
})();