diff --git a/backend/routes/ivantiTodoQueue.js b/backend/routes/ivantiTodoQueue.js index 6b83f18..e18395b 100644 --- a/backend/routes/ivantiTodoQueue.js +++ b/backend/routes/ivantiTodoQueue.js @@ -42,15 +42,20 @@ function createIvantiTodoQueueRouter(db, requireAuth) { if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) { return res.status(400).json({ error: 'finding_id is required.' }); } - if (!isValidVendor(vendor)) { - return res.status(400).json({ error: 'vendor is required (max 200 chars).' }); - } if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) { - return res.status(400).json({ error: 'workflow_type must be FP or Archer.' }); + return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' }); + } + // Vendor is required for FP and Archer, optional for CARD + if (workflow_type !== 'CARD' && !isValidVendor(vendor)) { + return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' }); + } + if (vendor !== undefined && vendor !== '' && !isValidVendor(vendor)) { + return res.status(400).json({ error: 'vendor must be under 200 chars.' }); } - const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null; - const title = finding_title && typeof finding_title === 'string' + const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim(); + const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null; + const title = finding_title && typeof finding_title === 'string' ? finding_title.slice(0, 500) : null; @@ -58,7 +63,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) { `INSERT INTO ivanti_todo_queue (user_id, finding_id, finding_title, cves_json, vendor, workflow_type) VALUES (?, ?, ?, ?, ?, ?)`, - [req.user.id, finding_id.trim(), title, cvesJson, vendor.trim(), workflow_type], + [req.user.id, finding_id.trim(), title, cvesJson, vendorVal, workflow_type], function (err) { if (err) { console.error('Error adding to queue:', err); diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index d1904ee..0b32740 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -1111,7 +1111,8 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd return () => document.removeEventListener('keydown', handler); }, [onCancel]); - const canSubmit = queueForm.vendor.trim().length > 0; + const isCard = queueForm.workflowType === 'CARD'; + const canSubmit = isCard || queueForm.vendor.trim().length > 0; return ReactDOM.createPortal(
- {/* Vendor input */} - + {/* Vendor input — hidden for CARD */} + {isCard ? ( +
+ No vendor required — disposition handled in CARD +
+ ) : ( + + )} {/* Workflow type toggle */}
@@ -1232,15 +1245,24 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted const pendingCount = items.filter((i) => i.status === 'pending').length; const completedCount = items.filter((i) => i.status === 'complete').length; - // Group by vendor, sorted alphabetically + // CARD items are their own top section; everything else groups by vendor const grouped = useMemo(() => { + const cardItems = items.filter((i) => i.workflow_type === 'CARD'); + const otherItems = items.filter((i) => i.workflow_type !== 'CARD'); + const map = {}; - items.forEach((item) => { + otherItems.forEach((item) => { const v = item.vendor || 'Unknown'; if (!map[v]) map[v] = []; map[v].push(item); }); - return Object.keys(map).sort().map((vendor) => ({ vendor, items: map[vendor] })); + const vendorGroups = Object.keys(map).sort().map((vendor) => ({ + key: vendor, label: vendor, items: map[vendor], isCard: false, + })); + + return cardItems.length > 0 + ? [{ key: '__CARD__', label: 'CARD', items: cardItems, isCard: true }, ...vendorGroups] + : vendorGroups; }, [items]); return ( @@ -1313,24 +1335,24 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted Check a row in the findings table to add it.
- ) : grouped.map(({ vendor, items: vendorItems }) => ( -
- {/* Vendor group header */} + ) : grouped.map(({ key, label, items: groupItems, isCard }) => ( +
+ {/* Group header */}
- - {vendor} + + {label} - {vendorItems.length} + {groupItems.length}
{/* Items */} - {vendorItems.map((item) => { + {groupItems.map((item) => { const done = item.status === 'complete'; const wfColor = item.workflow_type === 'FP' ? { col: '#F59E0B', rgb: '245,158,11' } : item.workflow_type === 'Archer' ? { col: '#0EA5E9', rgb: '14,165,233' }