diff --git a/app/admin/integrations/page.tsx b/app/admin/integrations/page.tsx index 11502a7..e60293c 100644 --- a/app/admin/integrations/page.tsx +++ b/app/admin/integrations/page.tsx @@ -49,6 +49,27 @@ interface MergedRow { setting: IntegrationSetting; } +const CATEGORY_LABELS: Record = { + psa: 'PSA', + rmm: 'RMM', + docs: 'Documentation', + security: 'Security', + backup: 'Backup', + network: 'Network', + identity: 'Identity', + mdm: 'MDM', + mail: 'Mail', + finance: 'Finance', + productivity: 'Productivity', + llm: 'LLM', +}; + +const CATEGORY_ORDER = [ + 'psa', 'rmm', 'docs', 'security', 'backup', + 'network', 'identity', 'mdm', 'mail', + 'finance', 'productivity', 'llm', +]; + function liveStatusLight(s: IntegrationHealthItem['status']): StatusLightState { if (s === 'ok') return 'ok'; if (s === 'auth_failed' || s === 'unreachable') return 'error'; @@ -227,20 +248,49 @@ export default function IntegrationTogglesPage() { /> ) : ( - + (() => { + // Bucket rows by category, preserving the canonical order. + const byCategory = new Map(); + for (const r of rows) { + const arr = byCategory.get(r.category) ?? []; + arr.push(r); + byCategory.set(r.category, arr); + } + const visibleCategories = CATEGORY_ORDER.filter((c) => byCategory.has(c)); + // Catch any unexpected category not in the canonical order. + for (const c of byCategory.keys()) { + if (!visibleCategories.includes(c)) visibleCategories.push(c); + } + + return ( +
+ {visibleCategories.map((category) => ( +
+

+ {CATEGORY_LABELS[category] ?? category} +

+
    + {byCategory.get(category)! + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((row) => ( + + setReasons((prev) => ({ ...prev, [row.key]: v })) + } + onToggle={(next) => void toggle(row, next)} + /> + ))} +
+
+ ))} +
+ ); + })() )}