Add VCL vertical metadata: inline-editable team fields, JSDoc on compliance routes, stats query rewrite

This commit is contained in:
Jordan Ramos
2026-05-13 07:57:41 -06:00
parent 0d29a1b84e
commit 9eec63ea42
4 changed files with 549 additions and 104 deletions

View File

@@ -183,10 +183,89 @@ function NonCompliantDonutChart({ donut }) {
);
}
// ---------------------------------------------------------------------------
// Inline Editable Cell Component
// ---------------------------------------------------------------------------
function EditableCell({ value, type, onSave, style, placeholder }) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(value || '');
useEffect(() => {
setDraft(value || '');
}, [value]);
const handleSave = () => {
setEditing(false);
const newValue = type === 'number' ? (draft === '' ? 0 : parseInt(draft, 10)) : draft;
if (newValue !== value) {
onSave(newValue);
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
setDraft(value || '');
setEditing(false);
}
};
if (editing) {
const inputStyle = {
background: 'rgba(15,23,42,0.9)',
border: `1px solid ${TEAL}`,
borderRadius: '0.25rem',
color: '#CBD5E1',
fontFamily: 'monospace',
fontSize: '0.7rem',
padding: '0.25rem 0.4rem',
width: '100%',
outline: 'none',
boxShadow: `0 0 0 1px ${TEAL}40`,
};
return (
<input
type={type || 'text'}
value={draft}
onChange={e => setDraft(e.target.value)}
onBlur={handleSave}
onKeyDown={handleKeyDown}
style={{ ...inputStyle, ...style }}
autoFocus
placeholder={placeholder}
/>
);
}
const displayValue = type === 'number' ? (value || 0) : (value || '');
return (
<span
onClick={() => setEditing(true)}
style={{
cursor: 'pointer',
display: 'inline-block',
minWidth: '30px',
minHeight: '1.2em',
padding: '0.1rem 0.25rem',
borderRadius: '0.2rem',
transition: 'background 0.15s',
...(style || {}),
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(20,184,166,0.08)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
title="Click to edit"
>
{displayValue || <span style={{ color: '#475569', fontStyle: 'italic' }}>{placeholder || '—'}</span>}
</span>
);
}
// ---------------------------------------------------------------------------
// Heavy Hitters Table (Task 14)
// ---------------------------------------------------------------------------
function HeavyHittersTable({ heavyHitters }) {
function HeavyHittersTable({ heavyHitters, onMetadataUpdate }) {
if (!heavyHitters || heavyHitters.length === 0) {
return (
<div style={{ padding: '1.5rem', textAlign: 'center', color: '#475569', fontFamily: 'monospace', fontSize: '0.8rem' }}>
@@ -244,10 +323,20 @@ function HeavyHittersTable({ heavyHitters }) {
{row.non_compliant}
</td>
<td style={cellStyle}>
{row.compliance_date || ''}
<EditableCell
value={row.compliance_date || ''}
type="text"
placeholder="e.g. Q3 2026"
onSave={(val) => onMetadataUpdate && onMetadataUpdate(row.vertical || row.team, { compliance_date: val || null })}
/>
</td>
<td style={{ ...cellStyle, color: '#94A3B8', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{row.notes || ''}
<EditableCell
value={row.notes || ''}
type="text"
placeholder="Add notes"
onSave={(val) => onMetadataUpdate && onMetadataUpdate(row.vertical || row.team, { notes: val })}
/>
</td>
</tr>
))}
@@ -260,7 +349,7 @@ function HeavyHittersTable({ heavyHitters }) {
// ---------------------------------------------------------------------------
// Vertical Breakdown Table (Task 15)
// ---------------------------------------------------------------------------
function VerticalBreakdownTable({ verticalBreakdown }) {
function VerticalBreakdownTable({ verticalBreakdown, onMetadataUpdate }) {
if (!verticalBreakdown || verticalBreakdown.length === 0) {
return (
<div style={{ padding: '1.5rem', textAlign: 'center', color: '#475569', fontFamily: 'monospace', fontSize: '0.8rem' }}>
@@ -357,10 +446,21 @@ function VerticalBreakdownTable({ verticalBreakdown }) {
{row.blockers || 0}
</td>
<td style={{ ...cellStyle, textAlign: 'right', color: '#94A3B8' }}>
{row.risk_acceptances || 0}
<EditableCell
value={row.risk_acceptances || 0}
type="number"
placeholder="0"
onSave={(val) => onMetadataUpdate && onMetadataUpdate(row.vertical || row.team, { risk_acceptances: val })}
style={{ textAlign: 'right', width: '50px' }}
/>
</td>
<td style={{ ...cellStyle, color: '#94A3B8', maxWidth: '150px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{row.notes || ''}
<EditableCell
value={row.notes || ''}
type="text"
placeholder="Add notes"
onSave={(val) => onMetadataUpdate && onMetadataUpdate(row.vertical || row.team, { notes: val })}
/>
</td>
</tr>
))}
@@ -381,6 +481,40 @@ export default function VCLReportPage() {
const [error, setError] = useState(null);
const [showBulkUpload, setShowBulkUpload] = useState(false);
const handleMetadataUpdate = async (team, fields) => {
try {
const res = await fetch(`${API_BASE}/compliance/vcl/vertical-metadata/${encodeURIComponent(team)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(fields),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
console.error('Failed to update metadata:', data.error || res.statusText);
return;
}
// Optimistically update local state
setStats(prev => {
if (!prev) return prev;
const updated = { ...prev };
if (updated.heavy_hitters) {
updated.heavy_hitters = updated.heavy_hitters.map(hh =>
(hh.vertical === team || hh.team === team) ? { ...hh, ...fields } : hh
);
}
if (updated.vertical_breakdown) {
updated.vertical_breakdown = updated.vertical_breakdown.map(vb =>
(vb.vertical === team || vb.team === team) ? { ...vb, ...fields } : vb
);
}
return updated;
});
} catch (err) {
console.error('Metadata update error:', err);
}
};
useEffect(() => {
const fetchData = async () => {
setLoading(true);
@@ -467,10 +601,10 @@ export default function VCLReportPage() {
</div>
{/* Heavy Hitters */}
<HeavyHittersTable heavyHitters={stats?.heavy_hitters} />
<HeavyHittersTable heavyHitters={stats?.heavy_hitters} onMetadataUpdate={handleMetadataUpdate} />
{/* Vertical Breakdown */}
<VerticalBreakdownTable verticalBreakdown={stats?.vertical_breakdown} />
<VerticalBreakdownTable verticalBreakdown={stats?.vertical_breakdown} onMetadataUpdate={handleMetadataUpdate} />
{/* Bulk Upload Modal */}
{showBulkUpload && (