const { useState, useEffect, useRef, useMemo } = React; // ─── Icons (inline SVGs) ───────────────────────────────────────────────────── const Icons = { search: ( ), mic: ( ), micOff: ( ), check: ( ), checkCircle: ( ), phone: ( ), mail: ( ), file: ( ), clock: ( ), arrowUp: ( ), arrowDown: ( ), plus: ( ), flag: ( ), zap: ( ), calendar: ( ), reply: ( ), snooze: ( ), notes: ( ), target: ( ), chevronRight: ( ), trendUp: ( ), trendDown: ( ), activity: ( ), circle: ( ), }; // ─── Utility Helpers ───────────────────────────────────────────────────────── function getGreeting() { const h = new Date().getHours(); if (h < 12) return "Good morning"; if (h < 17) return "Good afternoon"; return "Good evening"; } function formatDate(d) { return new Date(d).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" }); } function formatTime(d) { return new Date(d).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); } function formatDuration(seconds) { if (!seconds || seconds <= 0) return "0m"; const totalSec = Math.round(seconds); const h = Math.floor(totalSec / 3600); const m = Math.floor((totalSec % 3600) / 60); const s = totalSec % 60; if (h > 0) return m > 0 ? `${h}h ${m}m` : `${h}h`; if (m > 0) return s > 0 ? `${m}m ${s}s` : `${m}m`; return `${s}s`; } function timeAgo(dateStr) { const now = new Date(); const d = new Date(dateStr); const diffMs = now - d; const diffMins = Math.floor(diffMs / 60000); const diffHrs = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHrs / 24); if (diffMins < 60) return `${diffMins}m ago`; if (diffHrs < 24) return `${diffHrs}h ago`; if (diffDays === 1) return "Yesterday"; return `${diffDays}d ago`; } function ageLabel(dateStr) { const now = new Date(); const d = new Date(dateStr); const diffMs = now - d; const diffHrs = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffHrs / 24); if (diffHrs < 24) return { text: `${diffHrs} hours`, urgent: false }; if (diffDays <= 1) return { text: "1 day", urgent: false }; if (diffDays >= 3) return { text: `${diffDays} days waiting`, urgent: true }; return { text: `${diffDays} days`, urgent: false }; } function isOverdue(dateStr) { if (!dateStr) return false; const d = new Date(dateStr); const today = new Date(); today.setHours(0,0,0,0); return d < today; } function isDueToday(dateStr) { if (!dateStr) return false; const d = new Date(dateStr); const today = new Date(); return d.toDateString() === today.toDateString(); } function dueDateDisplay(dateStr) { if (!dateStr) return null; const d = new Date(dateStr); const today = new Date(); today.setHours(0,0,0,0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); if (d.toDateString() === today.toDateString()) return "Today"; if (d.toDateString() === tomorrow.toDateString()) return "Tomorrow"; if (d < today) { const diffDays = Math.floor((today - d) / 86400000); return `${diffDays}d overdue`; } return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } // ─── Priority Pill ─────────────────────────────────────────────────────────── function PriorityPill({ priority }) { const colors = { high: "bg-red-100 text-red-700", medium: "bg-amber-100 text-amber-700", low: "bg-emerald-100 text-emerald-700", }; return ( {priority} ); } // ─── Category Badge ────────────────────────────────────────────────────────── function CategoryBadge({ type }) { const config = { call: { icon: Icons.phone, label: "Call", cls: "bg-violet-100 text-violet-700" }, email: { icon: Icons.mail, label: "Email", cls: "bg-sky-100 text-sky-700" }, document: { icon: Icons.file, label: "Document", cls: "bg-orange-100 text-orange-700" }, other: { icon: Icons.flag, label: "Other", cls: "bg-slate-100 text-slate-600" }, }; const c = config[type] || config.other; return ( {c.icon} {c.label} ); } // ─── Due Date Label ────────────────────────────────────────────────────────── function DueDateLabel({ dateStr }) { if (!dateStr) return null; const label = dueDateDisplay(dateStr); const over = isOverdue(dateStr); const today = isDueToday(dateStr); let cls = "text-slate-500"; if (over) cls = "text-red-600 font-semibold"; else if (today) cls = "text-amber-600 font-semibold"; return ( {Icons.calendar} {label} ); } // ─── Card Wrapper ──────────────────────────────────────────────────────────── function Card({ children, className = "", ...props }) { return (
{children}
); } function CardHeader({ title, icon, action, count }) { return (
{icon && {icon}}

{title}

{count !== undefined && ( {count} )}
{action}
); } // ═══════════════════════════════════════════════════════════════════════════════ // MAIN DASHBOARD COMPONENT // ═══════════════════════════════════════════════════════════════════════════════ function Dashboard() { const [viewMode, setViewMode] = useState("today"); const [searchQuery, setSearchQuery] = useState(""); const [recording, setRecording] = useState(false); const [recordingSeconds, setRecordingSeconds] = useState(0); const timerRef = useRef(null); // Browser recording const [recordingMode, setRecordingMode] = useState(null); // "wasapi" | "browser" | null const [browserRecording, setBrowserRecording] = useState(false); const [uploading, setUploading] = useState(false); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); // ── Live data from backend ─────────────────────────────────────────────── const [liveData, setLiveData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetchDashboardData(); // Poll recording status const statusInterval = setInterval(checkRecordingStatus, 3000); return () => clearInterval(statusInterval); }, []); async function fetchDashboardData() { try { const resp = await fetch("/api/dashboard/data"); if (resp.ok) { const data = await resp.json(); setLiveData(data); } } catch (e) { console.warn("Failed to fetch dashboard data, using sample data", e); } finally { setLoading(false); } } async function checkRecordingStatus() { try { const resp = await fetch("/api/recording/status"); if (resp.ok) { const data = await resp.json(); setRecording(data.state === "recording"); } } catch (e) { /* ignore */ } } // ── Detect recording mode (WASAPI vs browser) ───────────────────────── useEffect(() => { fetch("/api/recording/status") .then(r => r.json()) .then(data => { setRecordingMode(data.state === "unavailable" ? "browser" : "wasapi"); }) .catch(() => setRecordingMode("browser")); }, []); // ── Recording timer ──────────────────────────────────────────────────── useEffect(() => { if (recording || browserRecording) { timerRef.current = setInterval(() => { setRecordingSeconds(s => s + 1); }, 1000); } else { clearInterval(timerRef.current); setRecordingSeconds(0); } return () => clearInterval(timerRef.current); }, [recording, browserRecording]); async function toggleRecording() { const endpoint = recording ? "/api/recording/stop" : "/api/recording/start"; try { const resp = await fetch(endpoint, { method: "POST" }); if (resp.ok) { setRecording(!recording); if (!recording) setRecordingSeconds(0); } } catch (e) { console.error("Recording toggle failed", e); } } // ── Browser recording functions ──────────────────────────────────────── async function startBrowserRecording() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Detect supported MIME type (Safari only supports audio/mp4) const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : MediaRecorder.isTypeSupported('audio/mp4') ? 'audio/mp4' : 'audio/webm'; const recorder = new MediaRecorder(stream, { mimeType }); chunksRef.current = []; recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; recorder.onstop = async () => { stream.getTracks().forEach(t => t.stop()); const blob = new Blob(chunksRef.current, { type: mimeType }); await uploadBrowserRecording(blob, mimeType); }; recorder.start(1000); // Collect data every second mediaRecorderRef.current = recorder; setBrowserRecording(true); setRecordingSeconds(0); } catch (e) { console.error("Mic access denied or unavailable:", e); alert("Could not access microphone. Please allow mic access and try again."); } } function stopBrowserRecording() { if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { mediaRecorderRef.current.stop(); } setBrowserRecording(false); } async function uploadBrowserRecording(blob, mimeType) { setUploading(true); try { const ext = mimeType.includes("mp4") ? ".m4a" : ".webm"; const formData = new FormData(); formData.append("audio", blob, `recording${ext}`); const resp = await fetch("/api/calls/upload", { method: "POST", body: formData }); if (resp.ok) { const data = await resp.json(); // Refresh dashboard data after a delay to allow processing setTimeout(fetchDashboardData, 3000); setTimeout(fetchDashboardData, 10000); } else { const err = await resp.json(); alert(`Upload failed: ${err.error || "Unknown error"}`); } } catch (e) { console.error("Upload failed:", e); alert("Failed to upload recording. Check your connection."); } finally { setUploading(false); } } // ── Task actions ─────────────────────────────────────────────────────── async function handleCompleteTask(taskId) { try { await fetch(`/tasks/${taskId}/complete`, { method: "POST" }); fetchDashboardData(); } catch (e) { console.error(e); } } async function handleAcceptInsight(insightId) { try { await fetch(`/api/email/insights/${insightId}/accept`, { method: "POST" }); fetchDashboardData(); } catch (e) { console.error(e); } } async function handleDismissInsight(insightId) { try { await fetch(`/api/email/insights/${insightId}/dismiss`, { method: "POST" }); fetchDashboardData(); } catch (e) { console.error(e); } } // ── Quick Add ────────────────────────────────────────────────────────── const [quickTask, setQuickTask] = useState(""); const [quickPriority, setQuickPriority] = useState("medium"); const [quickDue, setQuickDue] = useState(""); const [quickCategory, setQuickCategory] = useState("other"); async function handleQuickAdd(e) { e.preventDefault(); if (!quickTask.trim()) return; try { const resp = await fetch("/api/tasks/quick-add", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: quickTask.trim(), priority: quickPriority, due_date: quickDue || null, source_type: quickCategory, }), }); if (resp.ok) { setQuickTask(""); setQuickDue(""); setQuickPriority("medium"); setQuickCategory("other"); fetchDashboardData(); } } catch (e) { console.error(e); } } // ── Email Sync ───────────────────────────────────────────────────────── async function handleEmailSync() { try { await fetch("/api/email/sync", { method: "POST" }); setTimeout(fetchDashboardData, 5000); } catch (e) { console.error(e); } } // ── Derive display data from liveData or use sample ──────────────────── const data = useMemo(() => { if (liveData) return transformLiveData(liveData); return getSampleData(); }, [liveData]); // ── Render ───────────────────────────────────────────────────────────── const recordTimerStr = `${Math.floor(recordingSeconds/60)}:${String(recordingSeconds%60).padStart(2,"0")}`; return (
{/* ── SIDEBAR (desktop only — hidden on mobile) ────────────────── */} {/* ── MAIN CONTENT ──────────────────────────────────────────────── */}
{/* ── 1. HEADER ─────────────────────────────────────────────────── */}
{/* Row 1: Greeting + controls */}

{getGreeting()}, JD

{formatDate(new Date())}

{/* View Toggle */}
{/* Record Button */} {recordingMode === "wasapi" ? ( ) : recordingMode === "browser" ? ( ) : null}
{/* Row 2: Always-visible search bar */}
{Icons.search} setSearchQuery(e.target.value)} onKeyDown={e => { if (e.key === "Enter" && searchQuery.trim()) { window.location.href = `/search?q=${encodeURIComponent(searchQuery.trim())}`; } if (e.key === "Escape") { setSearchQuery(""); e.target.blur(); } }} className="w-full h-10 pr-3 text-base md:text-sm bg-transparent border-none outline-none placeholder-slate-400 text-slate-800" />
{/* ── QUICK-ADD TASK BAR ──────────────────────────────────────── */}
{Icons.plus} setQuickTask(e.target.value)} className="flex-1 h-10 text-sm bg-transparent border-none outline-none placeholder-slate-400 text-slate-800" />
setQuickDue(e.target.value)} className="h-10 px-3 text-sm rounded-lg border border-slate-200 bg-white text-slate-700 outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400" />
{/* ── TWO COLUMN LAYOUT ───────────────────────────────────────── */}
{/* ── LEFT COLUMN ──────────────────────────────────────────────── */}
{/* ── My Tasks (grouped by category) ────────────────────────── */} View All → } />
{groupTasksByCategory(data.tasks).map(group => (
{group.label}
{group.items.map(task => ( handleCompleteTask(task.id)} /> ))}
))} {data.tasks.length === 0 && (
No open tasks — nice work! 🎉
)}
{/* ── Completed Today ────────────────────────────────────────── */}
{data.completedToday.length > 0 ? (
{data.completedToday.map(item => (
{Icons.checkCircle} {item.title} {item.completedTime}
))}
) : (
Nothing completed yet — let's get started!
)}
{/* ── RIGHT COLUMN ─────────────────────────────────────────────── */}
{/* ── Email Insights ──────────────────────────────────────────── */} View All →
} />
{data.emailInsights.map(insight => ( handleAcceptInsight(insight.id)} onDismiss={() => handleDismissInsight(insight.id)} /> ))} {data.emailInsights.length === 0 && (
{data.gmailConnected ? "No new insights" : ( Connect Gmail → )}
)}
{/* ── Recent Calls ────────────────────────────────────────────── */}
{Icons.phone}

Recent Calls

{data.recentCalls.length}
{data.callStats.callsMade} of {data.callStats.callTarget} · {data.callStats.totalCallTime} · View All →
{data.recentCalls.map(call => ( ))} {data.recentCalls.length === 0 && (
No calls recorded yet
)}
{/* ── Today's Timeline ────────────────────────────────────────── */} {data.timeline.length > 0 && (
{data.timeline.map((event, i) => (
{i < data.timeline.length - 1 && (
)}

{event.title}

{event.time}

))}
)}
{/* ── BOTTOM NAV BAR (mobile only) ────────────────────────────── */}
); } // ─── Sidebar Component ─────────────────────────────────────────────────────── function Sidebar() { const currentPath = window.location.pathname; const navItems = [ { href: "/", label: "Dashboard", icon: ( )}, { href: "/tasks", label: "Tasks", icon: ( )}, { href: "/calls", label: "Calls", icon: ( )}, { href: "/email/setup", label: "Email", icon: ( )}, { href: "/digests", label: "Digests", icon: ( )}, { href: "/search", label: "Search", icon: ( )}, ]; function isActive(href) { if (href === "/") return currentPath === "/"; return currentPath.startsWith(href); } return ( ); } // ─── Bottom Nav Bar (mobile only) ───────────────────────────────────────────── function BottomNav() { const currentPath = window.location.pathname; const tabs = [ { href: "/", label: "Home", icon: ( )}, { href: "/tasks", label: "Tasks", icon: ( )}, { href: "/calls", label: "Calls", icon: ( )}, { href: "/email/setup", label: "Email", icon: ( )}, { href: "/digests", label: "Digests", icon: ( )}, ]; function isActive(href) { if (href === "/") return currentPath === "/"; return currentPath.startsWith(href); } return ( ); } // ─── Sub-Components ────────────────────────────────────────────────────────── function TaskRow({ task, onComplete }) { return (
{task.title}
{task.contact && {task.contact}}
); } function EmailInsightRow({ insight, onAccept, onDismiss }) { const age = ageLabel(insight.emailDate); return (

{insight.subject}

{insight.sender}

{age.text}

{insight.suggestedTask}

); } function CallRow({ call }) { return (
{Icons.phone}

{call.title}

{call.hasFollowUp && {Icons.flag}}

{call.time} · {call.duration}

{call.summary && (

{call.summary}

)}
); } // ─── Data Transformation ───────────────────────────────────────────────────── function groupTasksByCategory(tasks) { const groups = { call: { label: "Calls to Make", items: [] }, email: { label: "Emails to Send", items: [] }, document: { label: "Documents to Prepare", items: [] }, other: { label: "Other Tasks", items: [] }, }; tasks.forEach(t => { const cat = t.sourceType || "other"; if (groups[cat]) groups[cat].items.push(t); else groups.other.items.push(t); }); return Object.entries(groups) .filter(([_, g]) => g.items.length > 0) .map(([category, g]) => ({ category, ...g })); } function transformLiveData(d) { // Transform API data to component format const tasks = (d.tasks || []).map(t => ({ id: t.id, title: t.title, priority: t.priority || "medium", dueDate: t.due_date, sourceType: t.source_type || "other", contact: t.assignee || null, })); const completedToday = (d.completed_today || []).map(t => ({ id: t.id, title: t.title, completedTime: t.completed_at ? formatTime(t.completed_at) : "", })); const emailInsights = (d.email_insights || []).map(i => ({ id: i.id, subject: i.email_subject || "(no subject)", sender: i.email_sender || "unknown", emailDate: i.email_date || i.created_at, suggestedTask: i.suggested_task, priority: i.suggested_priority || "medium", })); const recentCalls = (d.recent_calls || []).map(c => ({ id: c.id, title: d.call_titles?.[c.id] || "Untitled Call", time: c.started_at ? formatTime(c.started_at) : "", duration: formatDuration(c.duration_seconds), summary: c.notes || null, hasFollowUp: false, })); const timeline = (d.timeline || []).map(e => ({ title: e.title, time: e.time, completed: e.completed, current: e.current || false, })); return { callStats: { callsMade: d.stats?.call_count || 0, callTarget: 8, totalCallTime: d.stats?.total_duration ? formatDuration(d.stats.total_duration) : "0m", }, tasks, completedToday, emailInsights, recentCalls, timeline, gmailConnected: d.gmail_connected || false, }; } function getSampleData() { return { callStats: { callsMade: 2, callTarget: 8, totalCallTime: "47m", }, tasks: [ { id: "1", title: "Call Marcus Greene — finalize flatbed trailer quote", priority: "high", dueDate: new Date().toISOString().slice(0,10), sourceType: "call", contact: "Greene Construction" }, { id: "2", title: "Call Hank's Excavation — schedule equipment return", priority: "high", dueDate: new Date(Date.now() - 86400000).toISOString().slice(0,10), sourceType: "call", contact: "Hank's Excavation" }, { id: "3", title: "Return follow-up call to Riverside Paving", priority: "medium", dueDate: new Date(Date.now() + 86400000).toISOString().slice(0,10), sourceType: "call", contact: "Riverside Paving" }, { id: "4", title: "Reply to Jenny at BlueLine about insurance cert", priority: "high", dueDate: new Date().toISOString().slice(0,10), sourceType: "email", contact: "BlueLine Logistics" }, { id: "5", title: "Send updated rental agreement to Metro Builders", priority: "medium", dueDate: new Date(Date.now() + 172800000).toISOString().slice(0,10), sourceType: "email", contact: "Metro Builders" }, { id: "6", title: "Prepare Q1 fleet utilization report", priority: "medium", dueDate: new Date(Date.now() + 432000000).toISOString().slice(0,10), sourceType: "document", contact: null }, { id: "7", title: "Update maintenance log for Unit #TR-4422", priority: "low", dueDate: null, sourceType: "document", contact: null }, { id: "8", title: "Review vendor pricing from AllState Equipment", priority: "low", dueDate: new Date(Date.now() + 604800000).toISOString().slice(0,10), sourceType: "other", contact: "AllState Equipment" }, ], completedToday: [ { id: "c1", title: "Called Dave at Summit Cranes — confirmed delivery", completedTime: "9:15 AM" }, { id: "c2", title: "Sent invoice #4455 to Tri-County Excavating", completedTime: "8:30 AM" }, { id: "c3", title: "Updated fleet availability spreadsheet", completedTime: "8:05 AM" }, ], emailInsights: [ { id: "e1", subject: "RE: Flatbed Availability for March", sender: "Marcus Greene ", emailDate: new Date(Date.now() - 7200000).toISOString(), suggestedTask: "Reply to Marcus with updated flatbed availability and 6-month pricing", priority: "high", }, { id: "e2", subject: "Insurance Certificate Renewal", sender: "Jenny Park ", emailDate: new Date(Date.now() - 259200000).toISOString(), suggestedTask: "Send renewed insurance certificate to BlueLine — their current one expires Friday", priority: "high", }, { id: "e3", subject: "Equipment Return Scheduling", sender: "accounting@hanksexcavation.com", emailDate: new Date(Date.now() - 86400000).toISOString(), suggestedTask: "Schedule pickup for 2 backhoes from Hank's Excavation job site", priority: "medium", }, { id: "e4", subject: "2025 Vendor Pricing Update", sender: "sales@allstateequipment.com", emailDate: new Date(Date.now() - 172800000).toISOString(), suggestedTask: "Review updated vendor pricing and compare against current fleet costs", priority: "low", }, ], recentCalls: [ { id: "r1", title: "Dave Mitchell — Summit Cranes", time: "9:12 AM", duration: "12m 34s", summary: "Confirmed delivery of 60-ton crane for Monday. Access route cleared.", hasFollowUp: true }, { id: "r2", title: "Sarah Chen — Riverside Paving", time: "Yesterday", duration: "8m 22s", summary: "Left voicemail — follow up Thursday about roller rental extension", hasFollowUp: true }, { id: "r3", title: "Tom Barrett — Metro Builders", time: "Yesterday", duration: "22m 15s", summary: "Discussed Q2 equipment needs. Wants proposal for 3 excavators + 2 dump trucks.", hasFollowUp: false }, ], timeline: [ { title: "Updated fleet availability", time: "8:05 AM", completed: true }, { title: "Sent invoice to Tri-County", time: "8:30 AM", completed: true }, { title: "Called Dave at Summit Cranes", time: "9:12 AM", completed: true }, { title: "Follow up with Marcus Greene", time: "10:30 AM", completed: false, current: true }, { title: "Lunch with regional sales team", time: "12:00 PM", completed: false }, { title: "Review vendor proposals", time: "2:00 PM", completed: false }, { title: "Daily digest review", time: "6:00 PM", completed: false }, ], gmailConnected: true, }; } // ─── Mount ─────────────────────────────────────────────────────────────────── const root = ReactDOM.createRoot(document.getElementById("dashboard-root")); root.render(React.createElement(Dashboard));