fix: bat uses dotnet directly, doctor frontend gets port 5174

This commit is contained in:
MingNian
2026-05-21 15:20:55 +08:00
parent 51c7c89ec5
commit 3ef25e734f
9 changed files with 358 additions and 83 deletions

View File

@@ -8,13 +8,26 @@ interface RawReport {
summary?: string; suggestions?: string;
patientName?: string; doctorName?: string;
createdAt: string; completedAt?: string;
items?: { id: string; itemName: string; resultValue: string; unit?: string; referenceRange?: string; isAbnormal: boolean }[];
items?: RawItem[];
}
interface RawItem {
id: string; itemName: string; resultValue: string;
unit?: string; referenceRange?: string; isAbnormal: boolean;
}
export function ReportDetailPage() {
const { id } = useParams<{ id: string }>();
const [report, setReport] = useState<RawReport | null>(null);
const [interpretation, setInterpretation] = useState('');
const [lightbox, setLightbox] = useState<string | null>(null);
// Interpretation form
const [summary, setSummary] = useState('');
const [riskLevel, setRiskLevel] = useState('normal');
const [suggestions, setSuggestions] = useState('');
const [items, setItems] = useState<{ itemName: string; resultValue: string; unit: string; referenceRange: string; isAbnormal: boolean }[]>([
{ itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false },
]);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
@@ -22,69 +35,130 @@ export function ReportDetailPage() {
api.get<RawReport>(`/api/reports/${id}`).then((r) => setReport(r.data)).catch(() => {});
}, [id]);
const addItem = () => {
setItems((prev) => [...prev, { itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false }]);
};
const updateItem = (index: number, field: string, value: string | boolean) => {
setItems((prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item));
};
const removeItem = (index: number) => {
if (items.length <= 1) return;
setItems((prev) => prev.filter((_, i) => i !== index));
};
const handleInterpret = async () => {
if (!interpretation.trim() || !id) return;
if (!summary.trim() || !id) return;
setSubmitting(true);
try {
await api.post(`/api/reports/${id}/interpret`, {
summary: interpretation,
items: [],
riskLevel: 'normal',
suggestions: null,
summary,
items: items.filter((it) => it.itemName.trim()),
riskLevel,
suggestions: suggestions || null,
});
// Refetch report to show updated status
const updated = await api.get<RawReport>(`/api/reports/${id}`);
setReport(updated.data);
setInterpretation('');
alert('interpretation submitted');
} catch { alert('submit failed'); }
finally { setSubmitting(false); }
};
if (!report) return <div style={{ padding: 24 }}>loading...</div>;
if (!report) return <div style={{ padding: 24 }}>Loading...</div>;
const isCompleted = report.status === 'completed';
return (
<div style={{ padding: 24 }}>
<Link to="/reports" style={{ fontSize: 13, color: '#1976d2' }}>&larr; back to reports</Link>
<Link to="/reports" style={{ fontSize: 13, color: '#1976d2' }}>&larr; Back to Reports</Link>
<div style={{ background: '#fff', marginTop: 16, padding: 24, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
<h2>{report.title}</h2>
<div style={{ marginTop: 12, fontSize: 14, color: '#666' }}>
<div>Patient: {report.patientName || 'unknown'}</div>
<div>Category: {report.category}</div>
<div>Status: {report.status}</div>
<div>Submitted: {report.createdAt?.split('T')[0]}</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
<div>
<h2 style={{ margin: 0 }}>{report.title}</h2>
<div style={{ marginTop: 8, fontSize: 13, color: '#888' }}>
Patient: {report.patientName || 'unknown'} &nbsp;|&nbsp;
Category: {report.category} &nbsp;|&nbsp;
Date: {report.createdAt?.split('T')[0]}
</div>
</div>
<span style={{
padding: '4px 12px', borderRadius: 12, fontSize: 12, fontWeight: 500,
background: isCompleted ? '#e8f5e9' : '#fff3e0',
color: isCompleted ? '#2e7d32' : '#f57c00',
}}>
{isCompleted ? 'Completed' : 'Pending Review'}
</span>
</div>
{/* Image gallery */}
{report.imageUrls && report.imageUrls.length > 0 && (
<div style={{ marginTop: 16 }}>
<h4 style={{ fontSize: 14, marginBottom: 8 }}>Report Images</h4>
{report.imageUrls.map((url, i) => (
<div key={i} style={{ padding: '8px 12px', background: '#f5f5f5', borderRadius: 4, marginBottom: 4, fontSize: 12 }}>
{url}
</div>
))}
<div style={{ marginTop: 20 }}>
<h4 style={{ fontSize: 14, marginBottom: 8 }}>Uploaded Images ({report.imageUrls.length})</h4>
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{report.imageUrls.map((url, i) => (
<div
key={i}
onClick={() => setLightbox(url)}
style={{
width: 120, height: 120, borderRadius: 8, overflow: 'hidden',
cursor: 'pointer', border: '2px solid #eee',
background: '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<img
src={`http://localhost:5000${url}`}
alt={`report-${i}`}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
</div>
))}
</div>
</div>
)}
{report.status === 'completed' && (
<div style={{ marginTop: 16, padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
<h4 style={{ fontSize: 14, marginBottom: 4 }}>Interpretation Result</h4>
<p style={{ fontSize: 13, color: '#555' }}>{report.summary || 'No summary'}</p>
{report.riskLevel && <p style={{ fontSize: 12, color: '#888' }}>Risk: {report.riskLevel}</p>}
{report.suggestions && <p style={{ fontSize: 12, color: '#888' }}>Suggestions: {report.suggestions}</p>}
{/* Lightbox */}
{lightbox && (
<div onClick={() => setLightbox(null)} style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999,
cursor: 'pointer',
}}>
<img src={`http://localhost:5000${lightbox}`} alt="preview" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} />
</div>
)}
{/* Completed interpretation display */}
{isCompleted && (
<div style={{ marginTop: 20, padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
<h4 style={{ fontSize: 14, marginBottom: 8 }}>Interpretation Result</h4>
<div style={{ fontSize: 13 }}>
<p><strong>Risk Level:</strong> <span style={{
color: report.riskLevel === 'normal' ? '#2e7d32' : report.riskLevel === 'abnormal' ? '#c62828' : '#f57c00',
fontWeight: 600,
}}>{report.riskLevel || '-'}</span></p>
<p><strong>Summary:</strong> {report.summary || '-'}</p>
{report.suggestions && <p><strong>Suggestions:</strong> {report.suggestions}</p>}
</div>
{report.items && report.items.length > 0 && (
<table style={{ width: '100%', marginTop: 8, borderCollapse: 'collapse', fontSize: 12 }}>
<thead><tr style={{ textAlign: 'left', borderBottom: '1px solid #ddd' }}>
<th style={{ padding: 4 }}>Item</th><th style={{ padding: 4 }}>Value</th><th style={{ padding: 4 }}>Range</th><th style={{ padding: 4 }}>Abnormal</th>
<table style={{ width: '100%', marginTop: 12, borderCollapse: 'collapse', fontSize: 12 }}>
<thead><tr style={{ textAlign: 'left', borderBottom: '2px solid #c8e6c9' }}>
<th style={{ padding: '6px 8px' }}>Item</th>
<th style={{ padding: '6px 8px' }}>Result</th>
<th style={{ padding: '6px 8px' }}>Reference</th>
<th style={{ padding: '6px 8px' }}>Abnormal</th>
</tr></thead>
<tbody>
{report.items.map((item) => (
<tr key={item.id} style={{ borderBottom: '1px solid #f0f0f0' }}>
<td style={{ padding: 4 }}>{item.itemName}</td>
<td style={{ padding: 4 }}>{item.resultValue} {item.unit || ''}</td>
<td style={{ padding: 4 }}>{item.referenceRange || '-'}</td>
<td style={{ padding: 4, color: item.isAbnormal ? '#c62828' : '#2e7d32' }}>{item.isAbnormal ? 'Yes' : 'No'}</td>
<tr key={item.id} style={{ borderBottom: '1px solid #e8f5e9' }}>
<td style={{ padding: '6px 8px' }}>{item.itemName}</td>
<td style={{ padding: '6px 8px' }}>{item.resultValue} {item.unit || ''}</td>
<td style={{ padding: '6px 8px' }}>{item.referenceRange || '-'}</td>
<td style={{ padding: '6px 8px', color: item.isAbnormal ? '#c62828' : '#2e7d32', fontWeight: 500 }}>
{item.isAbnormal ? 'Yes' : 'No'}
</td>
</tr>
))}
</tbody>
@@ -93,16 +167,72 @@ export function ReportDetailPage() {
</div>
)}
{report.status !== 'completed' && (
<div style={{ marginTop: 20 }}>
<h4 style={{ fontSize: 14, marginBottom: 8 }}>Doctor Interpretation</h4>
<textarea value={interpretation} onChange={(e) => setInterpretation(e.target.value)}
placeholder="Enter your interpretation..."
rows={5}
style={{ width: '100%', padding: 12, border: '1px solid #ddd', borderRadius: 4, fontSize: 14, resize: 'vertical' }} />
{/* Interpretation form (only for pending reports) */}
{!isCompleted && (
<div style={{ marginTop: 24, borderTop: '1px solid #eee', paddingTop: 20 }}>
<h3 style={{ fontSize: 15, marginBottom: 16 }}>Doctor Interpretation</h3>
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Summary</label>
<textarea
value={summary}
onChange={(e) => setSummary(e.target.value)}
placeholder="Enter your interpretation summary..."
rows={4}
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, resize: 'vertical', fontFamily: 'inherit', boxSizing: 'border-box' }}
/>
</div>
<div style={{ display: 'flex', gap: 16, marginBottom: 12 }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Risk Level</label>
<select value={riskLevel} onChange={(e) => setRiskLevel(e.target.value)}
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit' }}>
<option value="normal">Normal</option>
<option value="attention">Attention</option>
<option value="abnormal">Abnormal</option>
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Suggestions</label>
<input
value={suggestions}
onChange={(e) => setSuggestions(e.target.value)}
placeholder="e.g. Continue current medication"
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit', boxSizing: 'border-box' }}
/>
</div>
</div>
{/* Test items */}
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>Test Items</label>
{items.map((item, i) => (
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 6, alignItems: 'center' }}>
<input placeholder="Item name" value={item.itemName} onChange={(e) => updateItem(i, 'itemName', e.target.value)}
style={{ flex: 2, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
<input placeholder="Result" value={item.resultValue} onChange={(e) => updateItem(i, 'resultValue', e.target.value)}
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
<input placeholder="Unit" value={item.unit} onChange={(e) => updateItem(i, 'unit', e.target.value)}
style={{ width: 80, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
<input placeholder="Range" value={item.referenceRange} onChange={(e) => updateItem(i, 'referenceRange', e.target.value)}
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4 }}>
<input type="checkbox" checked={item.isAbnormal} onChange={(e) => updateItem(i, 'isAbnormal', e.target.checked)} />
Abnormal
</label>
<button onClick={() => removeItem(i)} style={{ background: 'none', border: 'none', color: '#c62828', cursor: 'pointer', fontSize: 16 }} disabled={items.length <= 1}></button>
</div>
))}
<button onClick={addItem} style={{ padding: '4px 12px', border: '1px dashed #1976d2', borderRadius: 4, background: 'none', color: '#1976d2', cursor: 'pointer', fontSize: 12 }}>
+ Add Item
</button>
</div>
<button onClick={handleInterpret} disabled={submitting} style={{
marginTop: 8, padding: '10px 24px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 4, fontSize: 14, opacity: submitting ? 0.7 : 1,
padding: '10px 28px', background: '#1976d2', color: '#fff',
border: 'none', borderRadius: 6, fontSize: 14, cursor: 'pointer',
opacity: submitting ? 0.7 : 1, marginTop: 8,
}}>
{submitting ? 'Submitting...' : 'Submit Interpretation'}
</button>