feat: chat window, preferred language, insurance contact, and AI call eligibility
- Schedule: right-click Chat option opens floating SMS chat window - Chat window: SMS template selector with appointment date/time pre-filled - Chat window: office name and phone pulled from Settings > Office Contact - Chat window: Preferred Language selector (English, Spanish, Portuguese, Mandarin, Cantonese, Arabic, Haitian Creole) with fully translated templates and locale-aware date/time formatting - Patient form: Preferred Language field (add/edit), default English - Settings > Office Contact: added Dental Office Name field - Settings > Advanced: Insurance Contact page (CRUD — company name + phone) - Prisma schema: preferredLanguage on Patient, officeName on OfficeContact, new InsuranceContact model - Patient management: Upload Patient Document moved below Patient Records - Insurance Eligibility: AI Call Insurance collapsible section; insurance company and phone auto-populated from saved Insurance Contacts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
LoaderCircleIcon,
|
||||
Stethoscope,
|
||||
Download,
|
||||
MessageSquare,
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
||||
import { PatientStatusBadge } from "@/components/appointments/patient-status-badge";
|
||||
import type { OfficeHoursData } from "@/components/settings/office-hours-card";
|
||||
import { MessageThread } from "@/components/patient-connection/message-thread";
|
||||
|
||||
// Define types for scheduling
|
||||
interface TimeSlot {
|
||||
@@ -201,6 +203,10 @@ export default function AppointmentsPage() {
|
||||
const [selectProceduresPatientId, setSelectProceduresPatientId] = useState<number | null>(null);
|
||||
const [selectProceduresAppointmentId, setSelectProceduresAppointmentId] = useState<number | null>(null);
|
||||
|
||||
// Chat popup state
|
||||
const [chatPatient, setChatPatient] = useState<Patient | null>(null);
|
||||
const [chatAppointmentInfo, setChatAppointmentInfo] = useState<{ date: string; startTime: string } | undefined>(undefined);
|
||||
|
||||
// Create context menu hook
|
||||
const { show } = useContextMenu({
|
||||
id: APPOINTMENT_CONTEXT_MENU_ID,
|
||||
@@ -756,6 +762,24 @@ export default function AppointmentsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Open chat window for the patient linked to this appointment
|
||||
const handleChat = (appointmentId: number) => {
|
||||
const apt = appointments.find((a) => a.id === appointmentId);
|
||||
if (!apt) return;
|
||||
const patient = patientsFromDay.find((p) => p.id === (apt as any).patientId);
|
||||
if (!patient) return;
|
||||
const processed = processedAppointments.find((a) => a.id === appointmentId);
|
||||
setChatPatient(patient as Patient);
|
||||
if (processed) {
|
||||
setChatAppointmentInfo({
|
||||
date: typeof processed.date === "string" ? processed.date : formatLocalDate(processed.date as Date),
|
||||
startTime: typeof processed.startTime === "string" ? processed.startTime.substring(0, 5) : "",
|
||||
});
|
||||
} else {
|
||||
setChatAppointmentInfo(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to display context menu
|
||||
const handleContextMenu = (e: React.MouseEvent, appointmentId: number) => {
|
||||
// Prevent the default browser context menu
|
||||
@@ -1478,6 +1502,14 @@ export default function AppointmentsPage() {
|
||||
Claim Status
|
||||
</span>
|
||||
</Item>
|
||||
|
||||
{/* Chat */}
|
||||
<Item onClick={({ props }) => handleChat(props.appointmentId)}>
|
||||
<span className="flex items-center gap-2 text-blue-600">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Chat
|
||||
</span>
|
||||
</Item>
|
||||
</Menu>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -1688,6 +1720,19 @@ export default function AppointmentsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat popup */}
|
||||
{chatPatient && (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg h-[600px] flex flex-col overflow-hidden">
|
||||
<MessageThread
|
||||
patient={chatPatient}
|
||||
appointmentInfo={chatAppointmentInfo}
|
||||
onBack={() => { setChatPatient(null); setChatAppointmentInfo(undefined); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Select Procedures modal — stays on appointments page */}
|
||||
{isSelectProceduresOpen && selectProceduresPatientId !== null && (
|
||||
<ClaimForm
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -10,7 +10,14 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CheckCircle, LoaderCircleIcon } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { CheckCircle, LoaderCircleIcon, Bot, PhoneCall, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
@@ -57,6 +64,25 @@ export default function InsuranceStatusPage() {
|
||||
const [isCheckingEligibilityClaimsPreAuth, setIsCheckingEligibilityClaimsPreAuth] =
|
||||
useState(false);
|
||||
|
||||
// AI Call Insurance section
|
||||
const [aiCallOpen, setAiCallOpen] = useState(false);
|
||||
const [aiSelectedContactId, setAiSelectedContactId] = useState<string>("");
|
||||
const [aiCallNotes, setAiCallNotes] = useState("");
|
||||
|
||||
type InsuranceContact = { id: number; name: string; phoneNumber?: string | null };
|
||||
const { data: insuranceContacts = [] } = useQuery<InsuranceContact[]>({
|
||||
queryKey: ["/api/insurance-contacts"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/insurance-contacts");
|
||||
if (!res.ok) return [];
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
const selectedInsuranceContact = insuranceContacts.find(
|
||||
(c) => String(c.id) === aiSelectedContactId
|
||||
) ?? null;
|
||||
|
||||
// PDF preview modal state
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
|
||||
@@ -742,6 +768,119 @@ export default function InsuranceStatusPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* AI Call Insurance */}
|
||||
<Card>
|
||||
<CardHeader
|
||||
className="cursor-pointer select-none"
|
||||
onClick={() => setAiCallOpen((o) => !o)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-9 w-9 rounded-lg bg-violet-100 flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="h-5 w-5 text-violet-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">AI Call Insurance</CardTitle>
|
||||
<CardDescription className="mt-0.5">
|
||||
Use AI to call the insurance company and check eligibility automatically
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
{aiCallOpen
|
||||
? <ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
: <ChevronDown className="h-4 w-4 text-muted-foreground" />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{aiCallOpen && (
|
||||
<CardContent className="space-y-5 pt-0">
|
||||
{/* Patient context banner */}
|
||||
{selectedPatient && (
|
||||
<div className="flex items-center gap-2 text-sm bg-violet-50 border border-violet-200 rounded-md px-3 py-2">
|
||||
<Bot className="h-4 w-4 text-violet-500 flex-shrink-0" />
|
||||
<span>
|
||||
Selected patient:{" "}
|
||||
<span className="font-medium">
|
||||
{selectedPatient.firstName} {selectedPatient.lastName}
|
||||
</span>
|
||||
{selectedPatient.insuranceId && (
|
||||
<> — Member ID: <span className="font-medium">{selectedPatient.insuranceId}</span></>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Insurance Company</Label>
|
||||
{insuranceContacts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic py-2">
|
||||
No insurance contacts saved.{" "}
|
||||
<a href="/settings/insurancecontact" className="underline text-violet-600">
|
||||
Add one in Settings → Insurance Contact
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<Select
|
||||
value={aiSelectedContactId}
|
||||
onValueChange={setAiSelectedContactId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select insurance company…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{insuranceContacts.map((c) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Phone Number</Label>
|
||||
<Input
|
||||
readOnly
|
||||
value={selectedInsuranceContact?.phoneNumber ?? ""}
|
||||
placeholder="Auto-filled from insurance contact"
|
||||
className="bg-gray-50 text-gray-600 cursor-default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 sm:col-span-2">
|
||||
<Label htmlFor="ai-call-notes">
|
||||
Additional Notes for AI{" "}
|
||||
<span className="text-muted-foreground font-normal">(Optional)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-call-notes"
|
||||
placeholder="e.g. Ask about dental benefits and annual maximum"
|
||||
value={aiCallNotes}
|
||||
onChange={(e) => setAiCallNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
disabled
|
||||
className="gap-2 bg-violet-600 hover:bg-violet-700 text-white opacity-60 cursor-not-allowed"
|
||||
>
|
||||
<PhoneCall className="h-4 w-4" />
|
||||
Start AI Call
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{selectedInsuranceContact
|
||||
? `Will call ${selectedInsuranceContact.name}${selectedInsuranceContact.phoneNumber ? ` at ${selectedInsuranceContact.phoneNumber}` : ""} — AI calling logic coming soon`
|
||||
: "Select an insurance company to begin — AI calling logic coming soon"}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Patients Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -401,6 +401,24 @@ export default function PatientsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Patients Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>
|
||||
View and manage all patient information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowDelete={true}
|
||||
allowEdit={true}
|
||||
allowView={true}
|
||||
allowFinancial={true}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* File Upload Zone */}
|
||||
<div className="space-y-8 py-8">
|
||||
<Card>
|
||||
@@ -518,24 +536,6 @@ export default function PatientsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Patients Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>
|
||||
View and manage all patient information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowDelete={true}
|
||||
allowEdit={true}
|
||||
allowView={true}
|
||||
allowFinancial={true}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Add/Edit Patient Modal */}
|
||||
<AddPatientModal
|
||||
ref={addPatientModalRef}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AiSettingsCard } from "@/components/settings/ai-settings-card";
|
||||
import { OfficeHoursCard } from "@/components/settings/office-hours-card";
|
||||
import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
||||
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
||||
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
||||
|
||||
type SectionId =
|
||||
| "staff"
|
||||
@@ -30,7 +31,8 @@ type SectionId =
|
||||
| "ai"
|
||||
| "officehours"
|
||||
| "officecontact"
|
||||
| "proceduretimeslot";
|
||||
| "proceduretimeslot"
|
||||
| "insurancecontact";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { toast } = useToast();
|
||||
@@ -266,6 +268,9 @@ export default function SettingsPage() {
|
||||
case "proceduretimeslot":
|
||||
return <ProcedureTimeslotCard />;
|
||||
|
||||
case "insurancecontact":
|
||||
return <InsuranceContactCard />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user