Files
DentalManagementMH05/apps/Frontend/src/components/patient-connection/sms-template-diaog.tsx

227 lines
7.9 KiB
TypeScript
Executable File

import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { MessageSquare, Send, Loader2, Save } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import type { Patient } from "@repo/db/types";
interface SmsTemplateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
patient: Patient | null;
}
const DEFAULT_TEMPLATES: Record<string, { name: string; body: string }> = {
appointment_reminder: {
name: "Appointment Reminder",
body: (firstName: string) =>
`Hi ${firstName}, this is your dental office. Reminder: You have an appointment scheduled. Please confirm or call us if you need to reschedule.`,
} as any,
appointment_confirmation: {
name: "Appointment Confirmation",
body: (firstName: string) =>
`Hi ${firstName}, your appointment has been confirmed. We look forward to seeing you! If you have any questions, please call our office.`,
} as any,
follow_up: {
name: "Follow-Up",
body: (firstName: string) =>
`Hi ${firstName}, thank you for visiting our dental office. How are you feeling after your treatment? Please let us know if you have any concerns.`,
} as any,
payment_reminder: {
name: "Payment Reminder",
body: (firstName: string) =>
`Hi ${firstName}, this is a friendly reminder about your outstanding balance. Please contact our office to discuss payment options.`,
} as any,
general: {
name: "General Message",
body: (firstName: string) => `Hi ${firstName}, this is your dental office. `,
} as any,
custom: {
name: "Custom Message",
body: () => "",
} as any,
};
const TEMPLATE_KEYS = Object.keys(DEFAULT_TEMPLATES);
function getDefaultBody(key: string, firstName: string): string {
const t = DEFAULT_TEMPLATES[key];
if (!t) return "";
return typeof t.body === "function" ? (t.body as any)(firstName) : t.body;
}
export function SmsTemplateDialog({
open,
onOpenChange,
patient,
}: SmsTemplateDialogProps) {
const [selectedKey, setSelectedKey] = useState("appointment_reminder");
const [messageText, setMessageText] = useState("");
const { toast } = useToast();
const { data: savedTemplates = {} } = useQuery<Record<string, string>>({
queryKey: ["/api/twilio/templates"],
enabled: open,
});
// Resolve effective body for a given key (saved override or default)
const resolveBody = (key: string) => {
if (key === "custom") return "";
if (savedTemplates[key]) {
// Replace placeholder first name with actual patient name
return savedTemplates[key].replace(/^Hi \w+,/, `Hi ${patient?.firstName ?? ""},`);
}
return getDefaultBody(key, patient?.firstName ?? "");
};
// When dialog opens or template changes, populate message
useEffect(() => {
if (open) setMessageText(resolveBody(selectedKey));
}, [open, selectedKey, savedTemplates, patient?.firstName]);
const sendMutation = useMutation({
mutationFn: async (message: string) => {
return apiRequest("POST", "/api/twilio/send-sms", {
to: patient!.phone,
message,
patientId: patient!.id,
});
},
onSuccess: () => {
toast({ title: "SMS Sent", description: `Message sent to ${patient?.firstName} ${patient?.lastName}` });
onOpenChange(false);
setSelectedKey("appointment_reminder");
setMessageText("");
},
onError: (err: any) => {
toast({ title: "Failed to Send SMS", description: err.message || "Please check your Twilio configuration.", variant: "destructive" });
},
});
const updateMutation = useMutation({
mutationFn: async ({ key, body }: { key: string; body: string }) => {
return apiRequest("PUT", `/api/twilio/templates/${key}`, { body });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/twilio/templates"] });
toast({ title: "Template Updated", description: "This template will be used going forward." });
},
onError: (err: any) => {
toast({ title: "Failed to Update Template", description: err.message, variant: "destructive" });
},
});
const handleTemplateChange = (key: string) => {
setSelectedKey(key);
setMessageText(resolveBody(key));
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Send SMS to {patient?.firstName} {patient?.lastName}
</DialogTitle>
<DialogDescription>
Choose a template or write a custom message
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Message Template</Label>
<Select value={selectedKey} onValueChange={handleTemplateChange}>
<SelectTrigger data-testid="select-sms-template">
<SelectValue placeholder="Select a template" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_KEYS.map((key) => (
<SelectItem key={key} value={key}>
{DEFAULT_TEMPLATES[key]?.name ?? key}
{savedTemplates[key] ? " ✎" : ""}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Message</Label>
{selectedKey !== "custom" && (
<Button
type="button"
variant="outline"
size="sm"
className="h-7 text-xs gap-1"
disabled={!messageText.trim() || updateMutation.isPending}
onClick={() => updateMutation.mutate({ key: selectedKey, body: messageText })}
>
{updateMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Save className="h-3 w-3" />
)}
{updateMutation.isPending ? "Saving..." : "Update Template"}
</Button>
)}
</div>
<Textarea
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
placeholder="Type your message here..."
rows={5}
className="resize-none"
data-testid="textarea-sms-message"
/>
<p className="text-xs text-muted-foreground">
{patient?.phone
? `Will be sent to: ${patient.phone}`
: "No phone number available"}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={() => sendMutation.mutate(messageText)}
disabled={!patient?.phone || !messageText.trim() || sendMutation.isPending || !patient}
className="gap-2"
data-testid="button-send-sms"
>
{sendMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
{sendMutation.isPending ? "Sending..." : "Send SMS"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}