feat: group schedule toolbar; add Consent, New Patient Form Link, Consent Templates placeholders

- Schedule page: toolbar grouped into Appointment, Consent, Eligibility,
  Claim, Communication and Download sections; new Consent button (stub)
- Patients page: New Patient Form Link button (stub)
- Settings: Consent Templates sidebar entry and placeholder section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 22:00:25 -04:00
co-authored by Claude Opus 5.5
parent aa5d3bf2d5
commit 59a75a4d6b
4 changed files with 368 additions and 275 deletions
@@ -37,6 +37,7 @@ import {
KeyRound,
Copy,
Keyboard,
FileSignature,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useMemo, useState, useEffect } from "react";
@@ -273,6 +274,11 @@ export function Sidebar() {
path: "/settings/npi",
icon: <Stethoscope className="h-4 w-4 text-gray-400" />,
},
{
name: "Consent Templates",
path: "/settings/consenttemplates",
icon: <FileSignature className="h-4 w-4 text-gray-400" />,
},
{
name: "Program Bridge",
path: "/settings/programs",
+337 -275
View File
@@ -32,6 +32,7 @@ import {
ExternalLink,
Bot,
UserCheck,
FileSignature,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Calendar } from "@/components/ui/calendar";
@@ -1531,6 +1532,11 @@ export default function AppointmentsPage() {
}
};
// Consent: logic to be defined later.
const handleConsent = () => {
toast({ title: "Consent", description: "Consent feature coming soon." });
};
const handleDownloadClaimPdfs = async () => {
if (!user) return;
setIsDownloadingClaimPdfs(true);
@@ -1865,7 +1871,7 @@ export default function AppointmentsPage() {
/>
<div className="container mx-auto">
<div className="flex justify-between items-center mb-6">
<div className="flex flex-col gap-4 mb-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Appointment Schedule
@@ -1875,291 +1881,347 @@ export default function AppointmentsPage() {
</p>
</div>
<div className="flex items-center gap-3 flex-wrap">
<Button
onClick={() => {
setEditingAppointment(undefined);
setIsAddModalOpen(true);
}}
disabled={isLoading}
>
<Plus className="h-4 w-4" />
New Appointment
</Button>
{/* Check Eligibility for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleCheckAllEligibilities()}
disabled={isLoading || isCheckingAllElig || selectedStaffColumns.size === 0}
size="sm"
>
{isCheckingAllElig ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Checking...
</>
) : (
<>
<Shield className="h-4 w-4 mr-1" />
Check Eligibility for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedStaffColumns.has(Number(staff.id))}
onChange={() => toggleStaffColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
</div>
{/* Claim for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleClaimForColumn()}
disabled={isLoading || isClaimingColumn || selectedClaimColumns.size === 0}
size="sm"
>
{isClaimingColumn ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Submitting...
</>
) : (
<>
<FileCheck className="h-4 w-4 mr-1" />
Claim for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedClaimColumns.has(Number(staff.id))}
onChange={() => toggleClaimColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
</div>
{/* Claim for Column with AI section */}
<div className="flex flex-wrap items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleClaimForColumnWithAi()}
disabled={isLoading || isClaimingAiColumn || selectedClaimAiColumns.size === 0}
size="sm"
>
{isClaimingAiColumn ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Submitting...
</>
) : (
<>
<Bot className="h-4 w-4 mr-1" />
Claim/Preauth for Column with AI
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedClaimAiColumns.has(Number(staff.id))}
onChange={() => toggleClaimAiColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
<div className="flex items-center gap-2 border-l pl-3 ml-1">
<Switch
id="auto-confirm-toggle"
checked={autoConfirmEnabled}
onCheckedChange={(v) => {
setAutoConfirmEnabled(v);
localStorage.setItem("ai_claim_auto_confirm", v ? "1" : "0");
<div className="flex items-start gap-3 flex-wrap">
{/* Appointment group */}
<div className="flex flex-col gap-1 rounded-lg border border-dashed border-gray-300 bg-gray-50/60 p-2">
<span className="px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Appointment
</span>
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={() => {
setEditingAppointment(undefined);
setIsAddModalOpen(true);
}}
/>
<Label htmlFor="auto-confirm-toggle" className="text-sm font-medium cursor-pointer whitespace-nowrap">
Auto Confirm
</Label>
</div>
<div className="flex items-center gap-2 border-l pl-3 ml-1">
<Switch
id="auto-claim-toggle"
checked={autoClaimEnabled}
onCheckedChange={setAutoClaimEnabled}
/>
<Label htmlFor="auto-claim-toggle" className="text-sm font-medium cursor-pointer whitespace-nowrap">
Auto Claim
</Label>
{autoClaimEnabled && (
<Select value={autoClaimTime} onValueChange={setAutoClaimTime}>
<SelectTrigger className="h-8 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 24 }, (_, h) => {
const value = `${String(h).padStart(2, "0")}:00`;
const label = h === 0 ? "12 AM" : h < 12 ? `${h} AM` : h === 12 ? "12 PM" : `${h - 12} PM`;
return (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
);
})}
</SelectContent>
</Select>
)}
disabled={isLoading}
>
<Plus className="h-4 w-4" />
New Appointment
</Button>
</div>
</div>
{/* Text Reminder for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleSendRemindersForColumn()}
disabled={isLoading || isSendingReminders || selectedReminderColumns.size === 0}
size="sm"
>
{isSendingReminders ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Sending...
</>
) : (
<>
<MessageSquare className="h-4 w-4 mr-1" />
Text Reminder for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
{/* Consent group */}
<div className="flex flex-col gap-1 rounded-lg border border-dashed border-gray-300 bg-gray-50/60 p-2">
<span className="px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Consent
</span>
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={() => handleConsent()}
disabled={isLoading}
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedReminderColumns.has(Number(staff.id))}
onChange={() => toggleReminderColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-gray-200">
<button
type="button"
role="switch"
aria-checked={reminderAiFollowUp}
onClick={() => setReminderAiFollowUp((v) => !v)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${reminderAiFollowUp ? "bg-teal-600" : "bg-gray-300"}`}
>
<span
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform ${reminderAiFollowUp ? "translate-x-4" : "translate-x-0"}`}
/>
</button>
<span className="text-xs text-gray-600 whitespace-nowrap">AI follow up</span>
<FileSignature className="h-4 w-4" />
Consent
</Button>
</div>
</div>
{/* Reschedule for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleSendRescheduleForColumn()}
disabled={isLoading || isSendingReschedule || selectedRescheduleColumns.size === 0}
size="sm"
>
{isSendingReschedule ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Sending...
</>
) : (
<>
<MessageSquare className="h-4 w-4 mr-1" />
Reschedule for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedRescheduleColumns.has(Number(staff.id))}
onChange={() => toggleRescheduleColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-gray-200">
<button
type="button"
role="switch"
aria-checked={rescheduleAiFollowUp}
onClick={() => setRescheduleAiFollowUp((v) => !v)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${rescheduleAiFollowUp ? "bg-teal-600" : "bg-gray-300"}`}
>
<span
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform ${rescheduleAiFollowUp ? "translate-x-4" : "translate-x-0"}`}
/>
</button>
<span className="text-xs text-gray-600 whitespace-nowrap">AI follow up</span>
{/* Eligibility group */}
<div className="flex flex-col gap-1 rounded-lg border border-dashed border-gray-300 bg-gray-50/60 p-2">
<span className="px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Eligibility
</span>
<div className="flex flex-wrap items-center gap-2">
{/* Check Eligibility for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleCheckAllEligibilities()}
disabled={isLoading || isCheckingAllElig || selectedStaffColumns.size === 0}
size="sm"
>
{isCheckingAllElig ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Checking...
</>
) : (
<>
<Shield className="h-4 w-4 mr-1" />
Check Eligibility for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedStaffColumns.has(Number(staff.id))}
onChange={() => toggleStaffColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
</div>
</div>
</div>
{/* Download Filled Claim/Preauth Form: ADA forms (e.g. BCBS) for the selected date */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleDownloadClaimPdfs()}
disabled={isLoading || isDownloadingClaimPdfs}
size="sm"
>
{isDownloadingClaimPdfs ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="h-4 w-4 mr-1" />
Download Filled Claim/Preauth Form
</>
)}
</Button>
{/* Claim group */}
<div className="flex flex-col gap-1 rounded-lg border border-dashed border-gray-300 bg-gray-50/60 p-2">
<span className="px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Claim
</span>
<div className="flex flex-wrap items-center gap-2">
{/* Claim for Column with AI section */}
<div className="flex flex-wrap items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleClaimForColumnWithAi()}
disabled={isLoading || isClaimingAiColumn || selectedClaimAiColumns.size === 0}
size="sm"
>
{isClaimingAiColumn ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Submitting...
</>
) : (
<>
<Bot className="h-4 w-4 mr-1" />
Claim/Preauth for Column with AI
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedClaimAiColumns.has(Number(staff.id))}
onChange={() => toggleClaimAiColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
<div className="flex items-center gap-2 border-l pl-3 ml-1">
<Switch
id="auto-confirm-toggle"
checked={autoConfirmEnabled}
onCheckedChange={(v) => {
setAutoConfirmEnabled(v);
localStorage.setItem("ai_claim_auto_confirm", v ? "1" : "0");
}}
/>
<Label htmlFor="auto-confirm-toggle" className="text-sm font-medium cursor-pointer whitespace-nowrap">
Auto Confirm
</Label>
</div>
<div className="flex items-center gap-2 border-l pl-3 ml-1">
<Switch
id="auto-claim-toggle"
checked={autoClaimEnabled}
onCheckedChange={setAutoClaimEnabled}
/>
<Label htmlFor="auto-claim-toggle" className="text-sm font-medium cursor-pointer whitespace-nowrap">
Auto Claim
</Label>
{autoClaimEnabled && (
<Select value={autoClaimTime} onValueChange={setAutoClaimTime}>
<SelectTrigger className="h-8 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 24 }, (_, h) => {
const value = `${String(h).padStart(2, "0")}:00`;
const label = h === 0 ? "12 AM" : h < 12 ? `${h} AM` : h === 12 ? "12 PM" : `${h - 12} PM`;
return (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
);
})}
</SelectContent>
</Select>
)}
</div>
</div>
{/* Claim for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleClaimForColumn()}
disabled={isLoading || isClaimingColumn || selectedClaimColumns.size === 0}
size="sm"
>
{isClaimingColumn ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Submitting...
</>
) : (
<>
<FileCheck className="h-4 w-4 mr-1" />
Claim for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedClaimColumns.has(Number(staff.id))}
onChange={() => toggleClaimColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
</div>
</div>
</div>
{/* Communication group */}
<div className="flex flex-col gap-1 rounded-lg border border-dashed border-gray-300 bg-gray-50/60 p-2">
<span className="px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Communication
</span>
<div className="flex flex-wrap items-center gap-2">
{/* Text Reminder for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleSendRemindersForColumn()}
disabled={isLoading || isSendingReminders || selectedReminderColumns.size === 0}
size="sm"
>
{isSendingReminders ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Sending...
</>
) : (
<>
<MessageSquare className="h-4 w-4 mr-1" />
Text Reminder for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedReminderColumns.has(Number(staff.id))}
onChange={() => toggleReminderColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-gray-200">
<button
type="button"
role="switch"
aria-checked={reminderAiFollowUp}
onClick={() => setReminderAiFollowUp((v) => !v)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${reminderAiFollowUp ? "bg-teal-600" : "bg-gray-300"}`}
>
<span
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform ${reminderAiFollowUp ? "translate-x-4" : "translate-x-0"}`}
/>
</button>
<span className="text-xs text-gray-600 whitespace-nowrap">AI follow up</span>
</div>
</div>
{/* Reschedule for Column section */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleSendRescheduleForColumn()}
disabled={isLoading || isSendingReschedule || selectedRescheduleColumns.size === 0}
size="sm"
>
{isSendingReschedule ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Sending...
</>
) : (
<>
<MessageSquare className="h-4 w-4 mr-1" />
Reschedule for Column
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedRescheduleColumns.has(Number(staff.id))}
onChange={() => toggleRescheduleColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-gray-200">
<button
type="button"
role="switch"
aria-checked={rescheduleAiFollowUp}
onClick={() => setRescheduleAiFollowUp((v) => !v)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none ${rescheduleAiFollowUp ? "bg-teal-600" : "bg-gray-300"}`}
>
<span
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform ${rescheduleAiFollowUp ? "translate-x-4" : "translate-x-0"}`}
/>
</button>
<span className="text-xs text-gray-600 whitespace-nowrap">AI follow up</span>
</div>
</div>
</div>
</div>
{/* Download group */}
<div className="flex flex-col gap-1 rounded-lg border border-dashed border-gray-300 bg-gray-50/60 p-2">
<span className="px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Download
</span>
<div className="flex flex-wrap items-center gap-2">
{/* Download Filled Claim/Preauth Form: ADA forms (e.g. BCBS) for the selected date */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleDownloadClaimPdfs()}
disabled={isLoading || isDownloadingClaimPdfs}
size="sm"
>
{isDownloadingClaimPdfs ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-1 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="h-4 w-4 mr-1" />
Download Filled Claim/Preauth Form
</>
)}
</Button>
</div>
</div>
</div>
</div>
</div>
+13
View File
@@ -15,6 +15,7 @@ import {
Camera,
NotebookPen,
FileCheck2,
Link2,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import {
@@ -398,6 +399,18 @@ export default function PatientsPage() {
<Plus className="h-4 w-4" />
New Patient
</Button>
<Button
variant="outline"
onClick={() => {
// New patient form link: logic to be defined later.
toast({ title: "New Patient Form Link", description: "Coming soon." });
}}
className="gap-1"
disabled={isLoading}
>
<Link2 className="h-4 w-4" />
New Patient Form Link
</Button>
</div>
</div>
+12
View File
@@ -29,6 +29,7 @@ type SectionId =
| "account"
| "credentials"
| "npi"
| "consenttemplates"
| "programs"
| "twilio"
| "telnyx"
@@ -256,6 +257,17 @@ export default function SettingsPage() {
case "npi":
return <NpiProviderTable />;
case "consenttemplates":
// Consent templates: logic to be defined later.
return (
<Card>
<CardContent className="py-6">
<h2 className="text-lg font-semibold text-gray-900">Consent Templates</h2>
<p className="text-sm text-gray-500 mt-1">Coming soon.</p>
</CardContent>
</Card>
);
case "programs":
return <ProgramBridgeTable />;