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:
2026-04-10 14:29:32 +05:30
parent 72012f099c
commit fb92da113e
7 changed files with 27 additions and 13 deletions

View File

@@ -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),
),
);

View File

@@ -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 {

View File

@@ -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,
}));
}

View File

@@ -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={

View File

@@ -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';

View File

@@ -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);

View File

@@ -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