Add collapsible sections to Ivanti Queue page

Group queue items into a hybrid layout: Inventory section (CARD/GRANITE/DECOM)
at top, then vendor-grouped sections for FP/Archer items sorted alphabetically.
Each section header is clickable to collapse/expand with chevron indicators.

- Extract grouping logic into reusable utility (queueGrouping.js)
- Add collapse state management (all sections expanded by default)
- Preserve cross-section multi-select, floating action bar, ticket badges
- Add 5 property-based tests covering grouping correctness, ordering,
  empty section omission, count accuracy, and selection independence
This commit is contained in:
Jordan Ramos
2026-05-27 11:07:32 -06:00
parent d081961341
commit fabf98790c
7 changed files with 941 additions and 149 deletions

View File

@@ -0,0 +1,63 @@
/**
* Queue grouping utility — extracts the hybrid Inventory + vendor grouping logic
* from IvantiTodoQueuePage into a testable pure function.
*
* Spec: .kiro/specs/queue-collapsible-sections
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7
*/
const INVENTORY_TYPES = new Set(['CARD', 'GRANITE', 'DECOM']);
/**
* Groups visible queue items into the hybrid section layout.
*
* @param {Array} visibleItems - Queue items with status 'pending'
* @returns {Array<{key: string, label: string, type: string, items: Array}>}
*
* Rules:
* - Items with workflow_type CARD, GRANITE, or DECOM → Inventory section
* - Items with workflow_type FP or Archer → grouped by vendor field
* - Items with null/undefined/empty vendor → placed in "Unknown" vendor section
* - Inventory section appears first (if non-empty)
* - Vendor sections sorted alphabetically by label
* - Sections with zero items are omitted from output
*/
export function groupQueueItems(visibleItems) {
const inventoryItems = [];
const vendorMap = new Map();
for (const item of visibleItems) {
if (INVENTORY_TYPES.has(item.workflow_type)) {
inventoryItems.push(item);
} else {
const vendor = item.vendor?.trim() || 'Unknown';
if (!vendorMap.has(vendor)) vendorMap.set(vendor, []);
vendorMap.get(vendor).push(item);
}
}
const sections = [];
if (inventoryItems.length > 0) {
sections.push({
key: 'inventory',
label: 'Inventory',
type: 'inventory',
items: inventoryItems,
});
}
const sortedVendors = [...vendorMap.entries()]
.sort((a, b) => a[0].localeCompare(b[0]));
for (const [vendor, items] of sortedVendors) {
sections.push({
key: `vendor:${vendor}`,
label: vendor,
type: 'vendor',
items,
});
}
return sections;
}