From 9c1e0cce01a7613ab88d0065102fe69a050d323f Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Wed, 6 May 2026 14:52:57 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20inbox=20UX=20polish=20=E2=80=94=20auto-?= =?UTF-8?q?save=20routing,=20search=20filter,=20email=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three improvements to the Operations Inbox: 1. Auto-save email routing: typing in the routing config auto-saves after 800ms with inline "Saving…" / "✓ Saved" indicator. No more manual Save button per route. 2. Search filter: instant client-side search across name, company, email, and ticket ID. Composes with existing type/status filters. 3. No-email badge: signals without email delivery now show a dim MailQuestion icon in the list view instead of nothing, making it clear at a glance which tickets need attention. Co-Authored-By: Claude Opus 4.6 --- src/app/hq-command/dashboard/inbox/page.tsx | 69 ++++++++++++++++----- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/src/app/hq-command/dashboard/inbox/page.tsx b/src/app/hq-command/dashboard/inbox/page.tsx index 7828052..bc2b4db 100644 --- a/src/app/hq-command/dashboard/inbox/page.tsx +++ b/src/app/hq-command/dashboard/inbox/page.tsx @@ -1,11 +1,12 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { ArrowLeft, Radar, Inbox, CheckCircle2, Clock, AlertCircle, Package, Video, Sparkles, Building2, Mail, Phone, ShieldAlert, Trash2, Loader2, Settings, X, MailCheck, MailX, Users, Key, Check, + Search, MailQuestion, } from "lucide-react"; import { getSignals, updateSignalStatus, resolveAndCleanSignal, getNotificationRoutes, updateNotificationRoute, deleteSignal, resendSignalEmail, getClients, approveAccessRequest, deleteClient } from "./actions"; import { useHqUi } from "@/components/hq/Toast"; @@ -25,6 +26,11 @@ export default function OperationsInbox() { const [filterType, setFilterType] = useState("ALL"); const [filterStatus, setFilterStatus] = useState("ALL"); + const [searchQuery, setSearchQuery] = useState(""); + + // Auto-save routing: track per-route save state + const [routeSaveStatus, setRouteSaveStatus] = useState>({}); + const debounceTimers = useRef>({}); const fetchInitialData = async () => { setIsLoading(true); @@ -41,10 +47,26 @@ export default function OperationsInbox() { useEffect(() => { fetchInitialData(); }, []); - const handleSaveRoute = async (type: string, emails: string) => { - const res = await updateNotificationRoute(type, emails); - if ((res as any)?.error) ui.toast(`Failed to save ${type} routing: ${(res as any).error}`, "error"); - else ui.toast(`${type} routing saved.`, "success"); + // Auto-save a route after 800ms of inactivity + const autoSaveRoute = useCallback((type: string, emails: string) => { + if (debounceTimers.current[type]) clearTimeout(debounceTimers.current[type]); + setRouteSaveStatus(prev => ({ ...prev, [type]: "idle" })); + debounceTimers.current[type] = setTimeout(async () => { + setRouteSaveStatus(prev => ({ ...prev, [type]: "saving" })); + const res = await updateNotificationRoute(type, emails); + if ((res as any)?.error) { + setRouteSaveStatus(prev => ({ ...prev, [type]: "error" })); + ui.toast(`Failed to save ${type} routing: ${(res as any).error}`, "error"); + } else { + setRouteSaveStatus(prev => ({ ...prev, [type]: "saved" })); + setTimeout(() => setRouteSaveStatus(prev => ({ ...prev, [type]: "idle" })), 2000); + } + }, 800); + }, [ui]); + + const handleRouteChange = (type: string, emails: string) => { + setRoutes(prev => ({ ...prev, [type]: emails })); + autoSaveRoute(type, emails); }; const handleStatusChange = async (id: string, status: string) => { @@ -156,6 +178,12 @@ export default function OperationsInbox() { const filtered = signals.filter(s => { if (filterType !== "ALL" && s.type !== filterType) return false; if (filterStatus !== "ALL" && s.status !== filterStatus) return false; + if (searchQuery) { + const q = searchQuery.toLowerCase(); + const match = [s.clientName, s.clientCompany, s.clientEmail, s.ticketId] + .some(field => field?.toLowerCase().includes(q)); + if (!match) return false; + } return true; }); @@ -198,6 +226,10 @@ export default function OperationsInbox() { ))} +
+ + setSearchQuery(e.target.value)} placeholder="Search name, company, email, ticket…" className="w-full bg-white/5 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-xs text-white placeholder-[#86868B]/60 outline-none focus:border-white/30 transition-colors" /> +
{isLoading ?
@@ -216,7 +248,11 @@ export default function OperationsInbox() { - ) : null} + ) : ( + + + + )} {getStatusIcon(signal.status)}
@@ -363,15 +399,20 @@ export default function OperationsInbox() {

Email Routing

Configure which team members receive alerts. Separate emails with commas.

- {[{ id: "ORDER", label: "Spare Part Orders", color: "text-amber-500" }, { id: "DIAGNOSTIC", label: "Tech Support / Diagnostics", color: "text-rose-500" }, { id: "CONSULTATION", label: "Engineering Consultations", color: "text-[#00F0FF]" }].map(route => ( -
- -
- setRoutes({...routes, [route.id]: e.target.value})} className="flex-1 bg-transparent border-b border-white/20 text-white text-sm pb-1 outline-none focus:border-white font-mono" placeholder="e.g. sales@flux.com, ceo@flux.com" /> - + {[{ id: "ORDER", label: "Spare Part Orders", color: "text-amber-500" }, { id: "DIAGNOSTIC", label: "Tech Support / Diagnostics", color: "text-rose-500" }, { id: "CONSULTATION", label: "Engineering Consultations", color: "text-[#00F0FF]" }].map(route => { + const status = routeSaveStatus[route.id] || "idle"; + return ( +
+
+ + + {status === "saving" ? "Saving…" : status === "saved" ? "✓ Saved" : status === "error" ? "✗ Failed" : ""} + +
+ handleRouteChange(route.id, e.target.value)} className="w-full bg-transparent border-b border-white/20 text-white text-sm pb-1 outline-none focus:border-white font-mono" placeholder="e.g. sales@flux.com, ceo@flux.com" />
-
- ))} + ); + })}