From c044d2d143dcfc73ecfdbddd118004bd8984fd57 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Sun, 12 Apr 2026 13:31:56 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20quick=20wins=20=E2=80=94=20global=20sea?= =?UTF-8?q?rch,=20P360=20actions,=20context=20panel,=20route=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wire GlobalSearch component into app shell top bar (US-10) - P360: Book Appointment button opens AppointmentForm (US-8) - P360: Add Note button creates leadActivity via GraphQL (US-8) - P360: Appointment rows clickable for edit (active statuses only) (US-8) - P360: Display lead status badge (was fetched but not rendered) (US-8) - Context panel: "View 360" link on linked patient → /patient/:id (US-6) - Context panel: Display campaign info from lead.utmCampaign (US-6) - Route guards: Admin-only routes wrapped in RequireAdmin (US-1, US-3) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/call-desk/context-panel.tsx | 18 ++++++ src/components/layout/app-shell.tsx | 6 +- src/main.tsx | 38 +++++++------ src/pages/patient-360.tsx | 66 ++++++++++++++++++++-- 4 files changed, 106 insertions(+), 22 deletions(-) diff --git a/src/components/call-desk/context-panel.tsx b/src/components/call-desk/context-panel.tsx index 000d68c..0d842ce 100644 --- a/src/components/call-desk/context-panel.tsx +++ b/src/components/call-desk/context-panel.tsx @@ -1,4 +1,5 @@ import { useState, useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSparkles, faPhone, faChevronDown, faChevronUp, @@ -58,6 +59,7 @@ const SectionHeader = ({ icon, label, count, expanded, onToggle }: { ); export const ContextPanel = ({ selectedLead, activities, calls, followUps, appointments, patients, callerPhone, isInCall }: ContextPanelProps) => { + const navigate = useNavigate(); const [contextExpanded, setContextExpanded] = useState(true); const [insightExpanded, setInsightExpanded] = useState(true); const [actionsExpanded, setActionsExpanded] = useState(true); @@ -163,6 +165,16 @@ export const ContextPanel = ({ selectedLead, activities, calls, followUps, appoi )} + {/* Campaign info */} + {(lead.utmCampaign || lead.campaignId) && ( +
+ Campaign + + {lead.utmCampaign ?? lead.campaignId} + +
+ )} + {/* Quick Actions — upcoming appointments + follow-ups + linked patient */} {(leadAppointments.length > 0 || leadFollowUps.length > 0 || linkedPatient) && (
@@ -223,6 +235,12 @@ export const ContextPanel = ({ selectedLead, activities, calls, followUps, appoi {linkedPatient.patientType && ( {linkedPatient.patientType} )} +
)} diff --git a/src/components/layout/app-shell.tsx b/src/components/layout/app-shell.tsx index 0a5b117..04305d5 100644 --- a/src/components/layout/app-shell.tsx +++ b/src/components/layout/app-shell.tsx @@ -15,6 +15,7 @@ import { useAuth } from '@/providers/auth-provider'; import { useData } from '@/providers/data-provider'; import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts'; import { useNetworkStatus } from '@/hooks/use-network-status'; +import { GlobalSearch } from '@/components/shared/global-search'; import { apiClient } from '@/lib/api-client'; import { cx } from '@/utils/cx'; @@ -119,7 +120,9 @@ export const AppShell = ({ children }: AppShellProps) => {
{/* Persistent top bar — visible on all pages */} {(hasAgentConfig || isAdmin) && ( -
+
+ +
{isAdmin && } {hasAgentConfig && ( <> @@ -140,6 +143,7 @@ export const AppShell = ({ children }: AppShellProps) => { )} +
)} diff --git a/src/main.tsx b/src/main.tsx index 76680e7..2e352e6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,6 +10,11 @@ const AdminSetupGuard = () => { const { isAdmin } = useAuth(); return isAdmin ? : ; }; + +const RequireAdmin = () => { + const { isAdmin } = useAuth(); + return isAdmin ? : ; +}; import { RoleRouter } from "@/components/layout/role-router"; import { NotFound } from "@/pages/not-found"; import { AllLeadsPage } from "@/pages/all-leads"; @@ -85,22 +90,23 @@ createRoot(document.getElementById("root")!).render( } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - {/* Settings hub + section pages */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* Admin-only routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/src/pages/patient-360.tsx b/src/pages/patient-360.tsx index 62642cf..251737c 100644 --- a/src/pages/patient-360.tsx +++ b/src/pages/patient-360.tsx @@ -15,8 +15,10 @@ import { Avatar } from '@/components/base/avatar/avatar'; import { Badge } from '@/components/base/badges/badges'; import { Button } from '@/components/base/buttons/button'; import { ClickToCallButton } from '@/components/call-desk/click-to-call-button'; +import { AppointmentForm } from '@/components/call-desk/appointment-form'; import { apiClient } from '@/lib/api-client'; import { formatShortDate, getInitials } from '@/lib/format'; +import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; import type { LeadActivity, LeadActivityType, Call, CallDisposition } from '@/types/entities'; @@ -96,15 +98,16 @@ type PatientData = { }; // Appointment row component -const AppointmentRow = ({ appt }: { appt: any }) => { +const AppointmentRow = ({ appt, onEdit }: { appt: any; onEdit?: (appt: any) => void }) => { const scheduledAt = appt.scheduledAt ? formatShortDate(appt.scheduledAt) : '--'; const statusColors: Record = { COMPLETED: 'success', SCHEDULED: 'brand', CONFIRMED: 'brand', CANCELLED: 'error', NO_SHOW: 'warning', RESCHEDULED: 'warning', }; + const canEdit = appt.status !== 'COMPLETED' && appt.status !== 'CANCELLED' && appt.status !== 'NO_SHOW'; return ( -
+
canEdit && onEdit?.(appt)}>
@@ -266,6 +269,9 @@ export const Patient360Page = () => { const { id } = useParams<{ id: string }>(); const [activeTab, setActiveTab] = useState('appointments'); const [noteText, setNoteText] = useState(''); + const [noteSaving, setNoteSaving] = useState(false); + const [apptFormOpen, setApptFormOpen] = useState(false); + const [editingAppt, setEditingAppt] = useState(null); const [patient, setPatient] = useState(null); const [loading, setLoading] = useState(true); const [activities, setActivities] = useState([]); @@ -383,6 +389,11 @@ export const Patient360Page = () => { {leadInfo.source.replace(/_/g, ' ')} )} + {leadInfo?.status && ( + + {leadInfo.status.replace(/_/g, ' ')} + + )}
@@ -423,7 +434,7 @@ export const Patient360Page = () => { {phoneRaw && ( )} - @@ -563,6 +592,33 @@ export const Patient360Page = () => {
+ + { + setApptFormOpen(false); + setEditingAppt(null); + // Refresh patient data + if (id) { + apiClient.graphql<{ patients: { edges: Array<{ node: PatientData }> } }>( + PATIENT_QUERY, { id }, { silent: true }, + ).then(data => setPatient(data.patients.edges[0]?.node ?? null)).catch(() => {}); + } + }} + /> ); };