mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
fix: setup wizard role guard, Doctor.clinic removal, dial sends agent config
Defect 1: Setup wizard (/setup) now guarded by AdminSetupGuard — CC agents and other non-admin roles are redirected to / instead of seeing the setup wizard they can't complete. Defect 3: Removed all references to Doctor.clinic (relation was replaced by DoctorVisitSlot entity). Updated queries.ts, appointments.tsx, transforms.ts, doctors.tsx, appointment-form.tsx. Defect 6 (frontend side): Dial request now sends agentId and campaignName from localStorage agent config so the sidecar dials with the correct per-agent credentials, not global defaults. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,7 +144,7 @@ export const AppointmentForm = ({
|
||||
const clinicNames = Array.from(
|
||||
new Set(
|
||||
slotEdges
|
||||
.map((se) => se.node.clinic?.clinicName)
|
||||
.map((se) => se.node.clinic?.clinicName ?? se.node.clinicId)
|
||||
.filter((n): n is string => !!n),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ export const APPOINTMENTS_QUERY = `{ appointments(first: 100, orderBy: [{ schedu
|
||||
scheduledAt durationMin appointmentType status
|
||||
doctorName department reasonForVisit
|
||||
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
||||
doctor { id clinic { clinicName } }
|
||||
doctor { id }
|
||||
} } } }`;
|
||||
|
||||
export const PATIENTS_QUERY = `{ patients(first: 50) { edges { node {
|
||||
|
||||
@@ -168,7 +168,7 @@ export function transformAppointments(data: any): Appointment[] {
|
||||
patientId: n.patient?.id ?? null,
|
||||
patientName: n.patient?.fullName ? `${n.patient.fullName.firstName} ${n.patient.fullName.lastName}`.trim() : null,
|
||||
patientPhone: n.patient?.phones?.primaryPhoneNumber ?? null,
|
||||
clinicName: n.doctor?.clinic?.clinicName ?? null,
|
||||
clinicName: n.department ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
16
src/main.tsx
16
src/main.tsx
@@ -1,8 +1,15 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
|
||||
import { BrowserRouter, Navigate, Outlet, Route, Routes } from "react-router";
|
||||
import { AppShell } from "@/components/layout/app-shell";
|
||||
import { AuthGuard } from "@/components/layout/auth-guard";
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
import { SetupWizardPage } from "@/pages/setup-wizard";
|
||||
|
||||
const AdminSetupGuard = () => {
|
||||
const { isAdmin } = useAuth();
|
||||
return isAdmin ? <SetupWizardPage /> : <Navigate to="/" replace />;
|
||||
};
|
||||
import { RoleRouter } from "@/components/layout/role-router";
|
||||
import { NotFound } from "@/pages/not-found";
|
||||
import { AllLeadsPage } from "@/pages/all-leads";
|
||||
@@ -31,7 +38,6 @@ import { AccountSettingsPage } from "@/pages/account-settings";
|
||||
import { RulesSettingsPage } from "@/pages/rules-settings";
|
||||
import { BrandingSettingsPage } from "@/pages/branding-settings";
|
||||
import { TeamSettingsPage } from "@/pages/team-settings";
|
||||
import { SetupWizardPage } from "@/pages/setup-wizard";
|
||||
import { ClinicsPage } from "@/pages/clinics";
|
||||
import { DoctorsPage } from "@/pages/doctors";
|
||||
import { TelephonySettingsPage } from "@/pages/telephony-settings";
|
||||
@@ -56,8 +62,10 @@ createRoot(document.getElementById("root")!).render(
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<AuthGuard />}>
|
||||
{/* Setup wizard — fullscreen, no AppShell */}
|
||||
<Route path="/setup" element={<SetupWizardPage />} />
|
||||
{/* Setup wizard — admin only, fullscreen, no AppShell.
|
||||
CC agents and other non-admin roles are redirected to
|
||||
the call desk — they can't complete setup anyway. */}
|
||||
<Route path="/setup" element={<AdminSetupGuard />} />
|
||||
|
||||
<Route
|
||||
element={
|
||||
|
||||
@@ -58,7 +58,7 @@ const QUERY = `{ appointments(first: 100, orderBy: [{ scheduledAt: DescNullsLast
|
||||
id scheduledAt durationMin appointmentType status
|
||||
doctorName department reasonForVisit
|
||||
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
||||
doctor { clinic { clinicName } }
|
||||
doctor { id }
|
||||
} } } }`;
|
||||
|
||||
const formatDate = (iso: string): string => formatDateOnly(iso);
|
||||
@@ -103,7 +103,7 @@ export const AppointmentsPage = () => {
|
||||
const phone = a.patient?.phones?.primaryPhoneNumber ?? '';
|
||||
const doctor = (a.doctorName ?? '').toLowerCase();
|
||||
const dept = (a.department ?? '').toLowerCase();
|
||||
const branch = (a.doctor?.clinic?.clinicName ?? '').toLowerCase();
|
||||
const branch = (a.department ?? '').toLowerCase();
|
||||
return patientName.includes(q) || phone.includes(q) || doctor.includes(q) || dept.includes(q) || branch.includes(q);
|
||||
});
|
||||
}
|
||||
@@ -177,7 +177,7 @@ export const AppointmentsPage = () => {
|
||||
? `${appt.patient.fullName?.firstName ?? ''} ${appt.patient.fullName?.lastName ?? ''}`.trim() || 'Unknown'
|
||||
: 'Unknown';
|
||||
const phone = appt.patient?.phones?.primaryPhoneNumber ?? '';
|
||||
const branch = appt.doctor?.clinic?.clinicName ?? '—';
|
||||
const branch = appt.department ?? '—';
|
||||
const statusLabel = STATUS_LABELS[appt.status ?? ''] ?? appt.status ?? '—';
|
||||
const statusColor = STATUS_COLORS[appt.status ?? ''] ?? 'gray';
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ const toFormValues = (doctor: DoctorNode): DoctorFormValues => ({
|
||||
doctor.doctorVisitSlots?.edges.map(
|
||||
(e): DoctorVisitSlotEntry => ({
|
||||
id: e.node.id,
|
||||
clinicId: e.node.clinicId ?? e.node.clinic?.id ?? '',
|
||||
clinicId: e.node.clinicId ?? e.node.clinicId ?? '',
|
||||
dayOfWeek: e.node.dayOfWeek ?? '',
|
||||
startTime: e.node.startTime ?? null,
|
||||
endTime: e.node.endTime ?? null,
|
||||
@@ -156,7 +156,7 @@ const summariseVisitSlots = (
|
||||
if (edges.length === 0) return 'No slots';
|
||||
const byClinic = new Map<string, DayOfWeek[]>();
|
||||
for (const e of edges) {
|
||||
const cid = e.node.clinicId ?? e.node.clinic?.id;
|
||||
const cid = e.node.clinicId ?? e.node.clinicId;
|
||||
if (!cid || !e.node.dayOfWeek) continue;
|
||||
if (!byClinic.has(cid)) byClinic.set(cid, []);
|
||||
byClinic.get(cid)!.push(e.node.dayOfWeek);
|
||||
|
||||
@@ -157,7 +157,13 @@ export const useSip = () => {
|
||||
}, 30000);
|
||||
|
||||
try {
|
||||
const result = await apiClient.post<{ status: string; ucid?: string }>('/api/ozonetel/dial', { phoneNumber });
|
||||
// Send agent config so the sidecar dials with the correct agent ID + campaign
|
||||
const agentConfig = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
|
||||
const result = await apiClient.post<{ status: string; ucid?: string }>('/api/ozonetel/dial', {
|
||||
phoneNumber,
|
||||
agentId: agentConfig.ozonetelAgentId,
|
||||
campaignName: agentConfig.campaignName,
|
||||
});
|
||||
console.log('[DIAL] Dial API response:', result);
|
||||
clearTimeout(safetyTimeout);
|
||||
// Store UCID from dial response — SIP bridge doesn't carry X-UCID for outbound
|
||||
|
||||
Reference in New Issue
Block a user