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(
|
const clinicNames = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
slotEdges
|
slotEdges
|
||||||
.map((se) => se.node.clinic?.clinicName)
|
.map((se) => se.node.clinic?.clinicName ?? se.node.clinicId)
|
||||||
.filter((n): n is string => !!n),
|
.filter((n): n is string => !!n),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const APPOINTMENTS_QUERY = `{ appointments(first: 100, orderBy: [{ schedu
|
|||||||
scheduledAt durationMin appointmentType status
|
scheduledAt durationMin appointmentType status
|
||||||
doctorName department reasonForVisit
|
doctorName department reasonForVisit
|
||||||
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
||||||
doctor { id clinic { clinicName } }
|
doctor { id }
|
||||||
} } } }`;
|
} } } }`;
|
||||||
|
|
||||||
export const PATIENTS_QUERY = `{ patients(first: 50) { edges { node {
|
export const PATIENTS_QUERY = `{ patients(first: 50) { edges { node {
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export function transformAppointments(data: any): Appointment[] {
|
|||||||
patientId: n.patient?.id ?? null,
|
patientId: n.patient?.id ?? null,
|
||||||
patientName: n.patient?.fullName ? `${n.patient.fullName.firstName} ${n.patient.fullName.lastName}`.trim() : null,
|
patientName: n.patient?.fullName ? `${n.patient.fullName.firstName} ${n.patient.fullName.lastName}`.trim() : null,
|
||||||
patientPhone: n.patient?.phones?.primaryPhoneNumber ?? 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 { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
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 { AppShell } from "@/components/layout/app-shell";
|
||||||
import { AuthGuard } from "@/components/layout/auth-guard";
|
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 { RoleRouter } from "@/components/layout/role-router";
|
||||||
import { NotFound } from "@/pages/not-found";
|
import { NotFound } from "@/pages/not-found";
|
||||||
import { AllLeadsPage } from "@/pages/all-leads";
|
import { AllLeadsPage } from "@/pages/all-leads";
|
||||||
@@ -31,7 +38,6 @@ import { AccountSettingsPage } from "@/pages/account-settings";
|
|||||||
import { RulesSettingsPage } from "@/pages/rules-settings";
|
import { RulesSettingsPage } from "@/pages/rules-settings";
|
||||||
import { BrandingSettingsPage } from "@/pages/branding-settings";
|
import { BrandingSettingsPage } from "@/pages/branding-settings";
|
||||||
import { TeamSettingsPage } from "@/pages/team-settings";
|
import { TeamSettingsPage } from "@/pages/team-settings";
|
||||||
import { SetupWizardPage } from "@/pages/setup-wizard";
|
|
||||||
import { ClinicsPage } from "@/pages/clinics";
|
import { ClinicsPage } from "@/pages/clinics";
|
||||||
import { DoctorsPage } from "@/pages/doctors";
|
import { DoctorsPage } from "@/pages/doctors";
|
||||||
import { TelephonySettingsPage } from "@/pages/telephony-settings";
|
import { TelephonySettingsPage } from "@/pages/telephony-settings";
|
||||||
@@ -56,8 +62,10 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<AuthGuard />}>
|
<Route element={<AuthGuard />}>
|
||||||
{/* Setup wizard — fullscreen, no AppShell */}
|
{/* Setup wizard — admin only, fullscreen, no AppShell.
|
||||||
<Route path="/setup" element={<SetupWizardPage />} />
|
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
|
<Route
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const QUERY = `{ appointments(first: 100, orderBy: [{ scheduledAt: DescNullsLast
|
|||||||
id scheduledAt durationMin appointmentType status
|
id scheduledAt durationMin appointmentType status
|
||||||
doctorName department reasonForVisit
|
doctorName department reasonForVisit
|
||||||
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
patient { id fullName { firstName lastName } phones { primaryPhoneNumber } }
|
||||||
doctor { clinic { clinicName } }
|
doctor { id }
|
||||||
} } } }`;
|
} } } }`;
|
||||||
|
|
||||||
const formatDate = (iso: string): string => formatDateOnly(iso);
|
const formatDate = (iso: string): string => formatDateOnly(iso);
|
||||||
@@ -103,7 +103,7 @@ export const AppointmentsPage = () => {
|
|||||||
const phone = a.patient?.phones?.primaryPhoneNumber ?? '';
|
const phone = a.patient?.phones?.primaryPhoneNumber ?? '';
|
||||||
const doctor = (a.doctorName ?? '').toLowerCase();
|
const doctor = (a.doctorName ?? '').toLowerCase();
|
||||||
const dept = (a.department ?? '').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);
|
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'
|
? `${appt.patient.fullName?.firstName ?? ''} ${appt.patient.fullName?.lastName ?? ''}`.trim() || 'Unknown'
|
||||||
: 'Unknown';
|
: 'Unknown';
|
||||||
const phone = appt.patient?.phones?.primaryPhoneNumber ?? '';
|
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 statusLabel = STATUS_LABELS[appt.status ?? ''] ?? appt.status ?? '—';
|
||||||
const statusColor = STATUS_COLORS[appt.status ?? ''] ?? 'gray';
|
const statusColor = STATUS_COLORS[appt.status ?? ''] ?? 'gray';
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const toFormValues = (doctor: DoctorNode): DoctorFormValues => ({
|
|||||||
doctor.doctorVisitSlots?.edges.map(
|
doctor.doctorVisitSlots?.edges.map(
|
||||||
(e): DoctorVisitSlotEntry => ({
|
(e): DoctorVisitSlotEntry => ({
|
||||||
id: e.node.id,
|
id: e.node.id,
|
||||||
clinicId: e.node.clinicId ?? e.node.clinic?.id ?? '',
|
clinicId: e.node.clinicId ?? e.node.clinicId ?? '',
|
||||||
dayOfWeek: e.node.dayOfWeek ?? '',
|
dayOfWeek: e.node.dayOfWeek ?? '',
|
||||||
startTime: e.node.startTime ?? null,
|
startTime: e.node.startTime ?? null,
|
||||||
endTime: e.node.endTime ?? null,
|
endTime: e.node.endTime ?? null,
|
||||||
@@ -156,7 +156,7 @@ const summariseVisitSlots = (
|
|||||||
if (edges.length === 0) return 'No slots';
|
if (edges.length === 0) return 'No slots';
|
||||||
const byClinic = new Map<string, DayOfWeek[]>();
|
const byClinic = new Map<string, DayOfWeek[]>();
|
||||||
for (const e of edges) {
|
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 (!cid || !e.node.dayOfWeek) continue;
|
||||||
if (!byClinic.has(cid)) byClinic.set(cid, []);
|
if (!byClinic.has(cid)) byClinic.set(cid, []);
|
||||||
byClinic.get(cid)!.push(e.node.dayOfWeek);
|
byClinic.get(cid)!.push(e.node.dayOfWeek);
|
||||||
|
|||||||
@@ -157,7 +157,13 @@ export const useSip = () => {
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
try {
|
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);
|
console.log('[DIAL] Dial API response:', result);
|
||||||
clearTimeout(safetyTimeout);
|
clearTimeout(safetyTimeout);
|
||||||
// Store UCID from dial response — SIP bridge doesn't carry X-UCID for outbound
|
// Store UCID from dial response — SIP bridge doesn't carry X-UCID for outbound
|
||||||
|
|||||||
Reference in New Issue
Block a user