just brought

This commit is contained in:
2025-05-21 15:59:46 +05:30
parent b46824ff98
commit f30d010d59
8 changed files with 1185 additions and 14 deletions

View File

@@ -1,37 +1,48 @@
import { Switch, Route } from "wouter"; import { Switch, Route } from "wouter";
import React, { Suspense, lazy } from "react";
import { queryClient } from "./lib/queryClient"; import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "./components/ui/toaster"; import { Toaster } from "./components/ui/toaster";
import { TooltipProvider } from "./components/ui/tooltip"; import { TooltipProvider } from "./components/ui/tooltip";
import NotFound from "./pages/not-found";
import Dashboard from "./pages/dashboard";
import AuthPage from "./pages/auth-page";
import AppointmentsPage from "./pages/appointments-page";
import PatientsPage from "./pages/patients-page";
import { ProtectedRoute } from "./lib/protected-route"; import { ProtectedRoute } from "./lib/protected-route";
import { AuthProvider } from "./hooks/use-auth"; import { AuthProvider } from "./hooks/use-auth";
import SettingsPage from "./pages/settings-page";
import Dashboard from "./pages/dashboard";
const AuthPage = lazy(() => import("./pages/auth-page"));
const AppointmentsPage = lazy(() => import("./pages/appointments-page"));
const PatientsPage = lazy(() => import("./pages/patients-page"));
const SettingsPage = lazy(() => import("./pages/settings-page"));
const ClaimsPage = lazy(() => import("./pages/claims-page"));
const PreAuthorizationsPage = lazy(() => import("./pages/preauthorizations-page"));
const PaymentsPage = lazy(() => import("./pages/payments-page"));
const NotFound = lazy(() => import("./pages/not-found"));
function Router() { function Router() {
return ( return (
<Switch> <Switch>
<ProtectedRoute path="/" component={Dashboard} /> <ProtectedRoute path="/" component={() => <Dashboard />} />
<ProtectedRoute path="/appointments" component={AppointmentsPage} /> <ProtectedRoute path="/appointments" component={() => <AppointmentsPage />} />
<ProtectedRoute path="/patients" component={PatientsPage} /> <ProtectedRoute path="/patients" component={() => <PatientsPage />} />
<ProtectedRoute path="/settings" component={SettingsPage}/> <ProtectedRoute path="/settings" component={() => <SettingsPage />} />
<Route path="/auth" component={AuthPage} /> <ProtectedRoute path="/claims" component={() => <ClaimsPage />} />
<Route component={NotFound} /> <ProtectedRoute path="/preauthorizations" component={() => <PreAuthorizationsPage />} />
<ProtectedRoute path="/payments" component={() => <PaymentsPage />} />
<Route path="/auth" component={() => <AuthPage />} />
<Route component={() => <NotFound />} />
</Switch> </Switch>
); );
} }
function App() { function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider> <AuthProvider>
<TooltipProvider> <TooltipProvider>
<Toaster /> <Toaster />
<Suspense fallback={<div>Loading...</div>}>
<Router /> <Router />
</Suspense>
</TooltipProvider> </TooltipProvider>
</AuthProvider> </AuthProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -0,0 +1,50 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
interface AppointmentsByDayProps {
appointments: any[];
}
export function AppointmentsByDay({ appointments }: AppointmentsByDayProps) {
// Data processing for appointments by day
const daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
// Initialize counts for each day
const countsByDay = daysOfWeek.map(day => ({ day, count: 0 }));
// Count appointments by day of week
appointments.forEach(appointment => {
const date = new Date(appointment.date);
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, ...
const dayIndex = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Adjust to make Monday first
countsByDay[dayIndex].count += 1;
});
return (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-medium">Appointments by Day</CardTitle>
<p className="text-xs text-muted-foreground">Distribution of appointments throughout the week</p>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={countsByDay}
margin={{ top: 5, right: 5, left: 0, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="day" fontSize={12} tickLine={false} axisLine={false} />
<YAxis fontSize={12} tickLine={false} axisLine={false} />
<Tooltip
formatter={(value) => [`${value} appointments`, "Count"]}
labelFormatter={(value) => `${value}`}
/>
<Bar dataKey="count" fill="#2563eb" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,67 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
interface NewPatientsProps {
patients: any[];
}
export function NewPatients({ patients }: NewPatientsProps) {
// Get months for the chart
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// Process patient data by registration month
const patientsByMonth = months.map(month => ({ name: month, count: 0 }));
// Count new patients by month
patients.forEach(patient => {
const createdDate = new Date(patient.createdAt);
const monthIndex = createdDate.getMonth();
patientsByMonth[monthIndex].count += 1;
});
// Add some sample data for visual effect if no patients
if (patients.length === 0) {
// Sample data pattern similar to the screenshot
const sampleData = [17, 12, 22, 16, 15, 17, 22, 28, 20, 16];
sampleData.forEach((value, index) => {
if (index < patientsByMonth.length) {
patientsByMonth[index].count = value;
}
});
}
return (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-medium">New Patients</CardTitle>
<p className="text-xs text-muted-foreground">Monthly trend of new patient registrations</p>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={patientsByMonth}
margin={{ top: 5, right: 5, left: 0, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="name" fontSize={12} tickLine={false} axisLine={false} />
<YAxis fontSize={12} tickLine={false} axisLine={false} />
<Tooltip
formatter={(value) => [`${value} patients`, "Count"]}
labelFormatter={(value) => `${value}`}
/>
<Line
type="monotone"
dataKey="count"
stroke="#f97316"
strokeWidth={2}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6, strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,5 +1,6 @@
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
import { LayoutDashboard, Users, Calendar, FileText, Settings } from "lucide-react"; import { LayoutDashboard, Users, Calendar, Settings, FileCheck, ClipboardCheck, CreditCard } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface SidebarProps { interface SidebarProps {
@@ -26,6 +27,21 @@ export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
path: "/patients", path: "/patients",
icon: <Users className="h-5 w-5" />, icon: <Users className="h-5 w-5" />,
}, },
{
name: "Claims",
path: "/claims",
icon: <FileCheck className="h-5 w-5" />,
},
{
name: "Pre-authorizations",
path: "/preauthorizations",
icon: <ClipboardCheck className="h-5 w-5" />,
},
{
name: "Payments",
path: "/payments",
icon: <CreditCard className="h-5 w-5" />,
},
{ {
name: "Settings", name: "Settings",
path: "/settings", path: "/settings",

View File

@@ -0,0 +1,255 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { ClaimForm } from "@/components/claims/claim-form";
import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth";
// import { Patient, Appointment } from "@shared/schema";
import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import { Plus, FileCheck, CheckCircle, Clock, AlertCircle } from "lucide-react";
import { format } from "date-fns";
export default function ClaimsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
const [selectedPatient, setSelectedPatient] = useState<number | null>(null);
const [selectedAppointment, setSelectedAppointment] = useState<number | null>(null);
const { toast } = useToast();
const { user } = useAuth();
// Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({
queryKey: ["/api/patients"],
enabled: !!user,
});
// Fetch appointments
const {
data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments
} = useQuery<Appointment[]>({
queryKey: ["/api/appointments"],
enabled: !!user,
});
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const handleNewClaim = (patientId: number, appointmentId: number) => {
setSelectedPatient(patientId);
setSelectedAppointment(appointmentId);
setIsClaimFormOpen(true);
};
const closeClaim = () => {
setIsClaimFormOpen(false);
setSelectedPatient(null);
setSelectedAppointment(null);
};
// Get unique patients with appointments
const patientsWithAppointments = appointments.reduce((acc, appointment) => {
if (!acc.some(item => item.patientId === appointment.patientId)) {
const patient = patients.find(p => p.id === appointment.patientId);
if (patient) {
acc.push({
patientId: patient.id,
patientName: `${patient.firstName} ${patient.lastName}`,
appointmentId: appointment.id,
insuranceProvider: patient.insuranceProvider || 'N/A',
insuranceId: patient.insuranceId || 'N/A',
lastAppointment: appointment.date
});
}
}
return acc;
}, [] as Array<{
patientId: number;
patientName: string;
appointmentId: number;
insuranceProvider: string;
insuranceId: string;
lastAppointment: string;
}>);
return (
<div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
<div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
<main className="flex-1 overflow-y-auto p-4">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-semibold text-gray-800">Insurance Claims</h1>
<p className="text-gray-600">Manage and submit insurance claims for patients</p>
</div>
{/* New Claims Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<div
className="flex items-center cursor-pointer group"
onClick={() => {
if (patientsWithAppointments.length > 0) {
const firstPatient = patientsWithAppointments[0];
handleNewClaim(firstPatient.patientId, firstPatient.appointmentId);
} else {
toast({
title: "No patients available",
description: "There are no patients with appointments to create a claim",
});
}
}}
>
<h2 className="text-xl font-medium text-gray-800 group-hover:text-primary">New Claims</h2>
<div className="ml-2 text-primary">
<FileCheck className="h-5 w-5" />
</div>
</div>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Recent Patients for Claims</CardTitle>
</CardHeader>
<CardContent>
{isLoadingPatients || isLoadingAppointments ? (
<div className="text-center py-4">Loading patients data...</div>
) : patientsWithAppointments.length > 0 ? (
<div className="divide-y">
{patientsWithAppointments.map((item) => (
<div
key={item.patientId}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => handleNewClaim(item.patientId, item.appointmentId)}
>
<div>
<h3 className="font-medium">{item.patientName}</h3>
<div className="text-sm text-gray-500">
<span>Insurance: {item.insuranceProvider === 'delta'
? 'Delta Dental'
: item.insuranceProvider === 'metlife'
? 'MetLife'
: item.insuranceProvider === 'cigna'
? 'Cigna'
: item.insuranceProvider === 'aetna'
? 'Aetna'
: item.insuranceProvider}</span>
<span className="mx-2"></span>
<span>ID: {item.insuranceId}</span>
<span className="mx-2"></span>
<span>Last Visit: {new Date(item.lastAppointment).toLocaleDateString()}</span>
</div>
</div>
<div className="text-primary">
<FileCheck className="h-5 w-5" />
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<FileCheck className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No eligible patients for claims</h3>
<p className="text-gray-500 mt-1">
Patients with appointments will appear here for insurance claim processing
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Old Claims Section */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">Old Claims</h2>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Submitted Claims History</CardTitle>
</CardHeader>
<CardContent>
{/* Sample Old Claims */}
<div className="divide-y">
{patientsWithAppointments.slice(0, 3).map((item, index) => (
<div
key={`old-claim-${index}`}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => toast({
title: "Claim Details",
description: `Viewing details for claim #${2000 + index}`
})}
>
<div>
<h3 className="font-medium">{item.patientName}</h3>
<div className="text-sm text-gray-500">
<span>Claim #: {2000 + index}</span>
<span className="mx-2"></span>
<span>Submitted: {format(new Date(new Date().setDate(new Date().getDate() - (index * 15))), 'MMM dd, yyyy')}</span>
<span className="mx-2"></span>
<span>Amount: ${(Math.floor(Math.random() * 500) + 100).toFixed(2)}</span>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
index === 0 ? 'bg-yellow-100 text-yellow-800' :
index === 1 ? 'bg-green-100 text-green-800' :
'bg-blue-100 text-blue-800'
}`}>
{index === 0 ? (
<span className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
Pending
</span>
) : index === 1 ? (
<span className="flex items-center">
<CheckCircle className="h-3 w-3 mr-1" />
Approved
</span>
) : (
<span className="flex items-center">
<AlertCircle className="h-3 w-3 mr-1" />
Review
</span>
)}
</span>
</div>
</div>
))}
{patientsWithAppointments.length === 0 && (
<div className="text-center py-8">
<Clock className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No claim history</h3>
<p className="text-gray-500 mt-1">
Submitted insurance claims will appear here
</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</main>
</div>
{/* Claim Form Modal */}
{isClaimFormOpen && selectedPatient !== null && selectedAppointment !== null && (
<ClaimForm
patientId={selectedPatient}
appointmentId={selectedAppointment}
patientName="" // Will be loaded by the component
onClose={closeClaim}
/>
)}
</div>
);
}

View File

@@ -12,6 +12,8 @@ import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { apiRequest, queryClient } from "@/lib/queryClient";
import { AppointmentsByDay } from "@/components/analytics/appointments-by-day";
import { NewPatients } from "@/components/analytics/new-patients";
import { import {
AppointmentUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema,
PatientUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema,
@@ -542,6 +544,12 @@ export default function Dashboard() {
</Card> </Card>
</div> </div>
{/* Analytics Dashboard Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<AppointmentsByDay appointments={appointments} />
<NewPatients patients={patients} />
</div>
{/* Patient Management Section */} {/* Patient Management Section */}
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
{/* Patient Header */} {/* Patient Header */}

View File

@@ -0,0 +1,414 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar";
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth";
// import { Patient, Appointment } from "@repo/db/shared/schemas";
import { Patient, Appointment } from "@repo/db/shared/schemas";
import {
CreditCard,
Clock,
CheckCircle,
AlertCircle,
DollarSign,
Receipt,
Plus,
ArrowDown,
ReceiptText
} from "lucide-react";
import { format } from "date-fns";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function PaymentsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
const { toast } = useToast();
const { user } = useAuth();
// Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({
queryKey: ["/api/patients"],
enabled: !!user,
});
// Fetch appointments
const {
data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments
} = useQuery<Appointment[]>({
queryKey: ["/api/appointments"],
enabled: !!user,
});
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
// Sample payment data
const samplePayments = [
{
id: "PMT-1001",
patientId: patients[0]?.id || 1,
amount: 75.00,
date: new Date(new Date().setDate(new Date().getDate() - 2)),
method: "Credit Card",
status: "completed",
description: "Co-pay for cleaning"
},
{
id: "PMT-1002",
patientId: patients[0]?.id || 1,
amount: 150.00,
date: new Date(new Date().setDate(new Date().getDate() - 7)),
method: "Insurance",
status: "processing",
description: "Insurance claim for x-rays"
},
{
id: "PMT-1003",
patientId: patients[0]?.id || 1,
amount: 350.00,
date: new Date(new Date().setDate(new Date().getDate() - 14)),
method: "Check",
status: "completed",
description: "Payment for root canal"
},
{
id: "PMT-1004",
patientId: patients[0]?.id || 1,
amount: 120.00,
date: new Date(new Date().setDate(new Date().getDate() - 30)),
method: "Credit Card",
status: "completed",
description: "Filling procedure"
}
];
// Sample outstanding balances
const sampleOutstanding = [
{
id: "INV-5001",
patientId: patients[0]?.id || 1,
amount: 210.50,
dueDate: new Date(new Date().setDate(new Date().getDate() + 7)),
description: "Crown procedure",
created: new Date(new Date().setDate(new Date().getDate() - 10)),
status: "pending"
},
{
id: "INV-5002",
patientId: patients[0]?.id || 1,
amount: 85.00,
dueDate: new Date(new Date().setDate(new Date().getDate() - 5)),
description: "Diagnostic & preventive",
created: new Date(new Date().setDate(new Date().getDate() - 20)),
status: "overdue"
}
];
// Calculate summary data
const totalOutstanding = sampleOutstanding.reduce((sum, item) => sum + item.amount, 0);
const totalCollected = samplePayments
.filter(payment => payment.status === "completed")
.reduce((sum, payment) => sum + payment.amount, 0);
const pendingAmount = samplePayments
.filter(payment => payment.status === "processing")
.reduce((sum, payment) => sum + payment.amount, 0);
const handleRecordPayment = (patientId: number, invoiceId?: string) => {
const patient = patients.find(p => p.id === patientId);
toast({
title: "Payment form opened",
description: `Recording payment for ${patient?.firstName} ${patient?.lastName}${invoiceId ? ` (Invoice: ${invoiceId})` : ''}`,
});
};
return (
<div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
<div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
<main className="flex-1 overflow-y-auto p-4">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<div>
<h1 className="text-2xl font-semibold text-gray-800">Payments</h1>
<p className="text-gray-600">Manage patient payments and outstanding balances</p>
</div>
<div className="mt-4 md:mt-0 flex items-center space-x-2">
<Select
defaultValue="all-time"
onValueChange={value => setPaymentPeriod(value)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select period" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all-time">All Time</SelectItem>
<SelectItem value="this-month">This Month</SelectItem>
<SelectItem value="last-month">Last Month</SelectItem>
<SelectItem value="last-90-days">Last 90 Days</SelectItem>
<SelectItem value="this-year">This Year</SelectItem>
</SelectContent>
</Select>
<Button onClick={() => handleRecordPayment(patients[0]?.id || 1)}>
<Plus className="h-4 w-4 mr-2" />
Record Payment
</Button>
</div>
</div>
{/* Payment Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500">Outstanding Balance</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<DollarSign className="h-5 w-5 text-yellow-500 mr-2" />
<div className="text-2xl font-bold">${totalOutstanding.toFixed(2)}</div>
</div>
<p className="text-xs text-gray-500 mt-1">
From {sampleOutstanding.length} outstanding invoices
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500">Payments Collected</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<DollarSign className="h-5 w-5 text-green-500 mr-2" />
<div className="text-2xl font-bold">${totalCollected.toFixed(2)}</div>
</div>
<p className="text-xs text-gray-500 mt-1">
From {samplePayments.filter(p => p.status === "completed").length} completed payments
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-500">Pending Payments</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center">
<DollarSign className="h-5 w-5 text-blue-500 mr-2" />
<div className="text-2xl font-bold">${pendingAmount.toFixed(2)}</div>
</div>
<p className="text-xs text-gray-500 mt-1">
From {samplePayments.filter(p => p.status === "processing").length} pending transactions
</p>
</CardContent>
</Card>
</div>
{/* Outstanding Balances Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">Outstanding Balances</h2>
</div>
<Card>
<CardContent className="p-0">
{sampleOutstanding.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Patient</TableHead>
<TableHead>Invoice</TableHead>
<TableHead>Description</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sampleOutstanding.map((invoice) => {
const patient = patients.find(p => p.id === invoice.patientId) ||
{ firstName: "Sample", lastName: "Patient" };
return (
<TableRow key={invoice.id}>
<TableCell>
{patient.firstName} {patient.lastName}
</TableCell>
<TableCell>{invoice.id}</TableCell>
<TableCell>{invoice.description}</TableCell>
<TableCell>${invoice.amount.toFixed(2)}</TableCell>
<TableCell>{format(invoice.dueDate, 'MMM dd, yyyy')}</TableCell>
<TableCell>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
invoice.status === 'overdue'
? 'bg-red-100 text-red-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{invoice.status === 'overdue' ? (
<>
<AlertCircle className="h-3 w-3 mr-1" />
Overdue
</>
) : (
<>
<Clock className="h-3 w-3 mr-1" />
Pending
</>
)}
</span>
</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="outline"
onClick={() => handleRecordPayment(invoice.patientId, invoice.id)}
>
Pay Now
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
) : (
<div className="text-center py-8">
<AlertCircle className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No outstanding balances</h3>
<p className="text-gray-500 mt-1">
All patient accounts are current
</p>
</div>
)}
</CardContent>
{sampleOutstanding.length > 0 && (
<CardFooter className="flex justify-between px-6 py-4 border-t">
<div className="flex items-center text-sm text-gray-500">
<ArrowDown className="h-4 w-4 mr-1" />
<span>Download statement</span>
</div>
<div className="font-medium">
Total: ${totalOutstanding.toFixed(2)}
</div>
</CardFooter>
)}
</Card>
</div>
{/* Recent Payments Section */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">Recent Payments</h2>
</div>
<Card>
<CardContent className="p-0">
{samplePayments.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Patient</TableHead>
<TableHead>Payment ID</TableHead>
<TableHead>Description</TableHead>
<TableHead>Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Method</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{samplePayments.map((payment) => {
const patient = patients.find(p => p.id === payment.patientId) ||
{ firstName: "Sample", lastName: "Patient" };
return (
<TableRow key={payment.id}>
<TableCell>
{patient.firstName} {patient.lastName}
</TableCell>
<TableCell>{payment.id}</TableCell>
<TableCell>{payment.description}</TableCell>
<TableCell>{format(payment.date, 'MMM dd, yyyy')}</TableCell>
<TableCell>${payment.amount.toFixed(2)}</TableCell>
<TableCell>{payment.method}</TableCell>
<TableCell>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
payment.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{payment.status === 'completed' ? (
<>
<CheckCircle className="h-3 w-3 mr-1" />
Completed
</>
) : (
<>
<Clock className="h-3 w-3 mr-1" />
Processing
</>
)}
</span>
</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="ghost"
onClick={() => {
toast({
title: "Receipt Generated",
description: `Receipt for payment ${payment.id} has been generated`,
});
}}
>
<ReceiptText className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
) : (
<div className="text-center py-8">
<Receipt className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No payment history</h3>
<p className="text-gray-500 mt-1">
Payments will appear here once processed
</p>
</div>
)}
</CardContent>
</Card>
</div>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,350 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { useAuth } from "@/hooks/use-auth";
// import { Patient, Appointment } from "@repo/db/shared/schemas";
import { Patient, Appointment } from "@repo/db/shared/schemas";
import { Plus, ClipboardCheck, Clock, CheckCircle, AlertCircle } from "lucide-react";
import { format } from "date-fns";
export default function PreAuthorizationsPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isPreAuthFormOpen, setIsPreAuthFormOpen] = useState(false);
const [selectedPatient, setSelectedPatient] = useState<number | null>(null);
const [selectedProcedure, setSelectedProcedure] = useState<string | null>(null);
const { toast } = useToast();
const { user } = useAuth();
// Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<Patient[]>({
queryKey: ["/api/patients"],
enabled: !!user,
});
// Fetch appointments
const {
data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments
} = useQuery<Appointment[]>({
queryKey: ["/api/appointments"],
enabled: !!user,
});
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const handleNewPreAuth = (patientId: number, procedure: string) => {
setSelectedPatient(patientId);
setSelectedProcedure(procedure);
setIsPreAuthFormOpen(true);
// Show a toast notification of success
const patient = patients.find(p => p.id === patientId);
toast({
title: "Pre-authorization Request Started",
description: `Started pre-auth for ${patient?.firstName} ${patient?.lastName} - ${procedure}`,
});
};
// Common dental procedures requiring pre-authorization
const dentalProcedures = [
{ code: "D2740", name: "Crown - porcelain/ceramic" },
{ code: "D2950", name: "Core buildup, including any pins" },
{ code: "D3330", name: "Root Canal - molar" },
{ code: "D4341", name: "Periodontal scaling & root planing" },
{ code: "D4910", name: "Periodontal maintenance" },
{ code: "D5110", name: "Complete denture - maxillary" },
{ code: "D6010", name: "Surgical placement of implant body" },
{ code: "D7240", name: "Removal of impacted tooth" },
];
// Get patients with active insurance
const patientsWithInsurance = patients.filter(patient =>
patient.insuranceProvider && patient.insuranceId
);
// Sample pre-authorization data
const samplePreAuths = [
{
id: "PA2023-001",
patientId: patientsWithInsurance[0]?.id || 1,
procedureCode: "D2740",
procedureName: "Crown - porcelain/ceramic",
requestDate: new Date(new Date().setDate(new Date().getDate() - 14)),
status: "approved",
approvalDate: new Date(new Date().setDate(new Date().getDate() - 7)),
expirationDate: new Date(new Date().setMonth(new Date().getMonth() + 6)),
},
{
id: "PA2023-002",
patientId: patientsWithInsurance[0]?.id || 1,
procedureCode: "D3330",
procedureName: "Root Canal - molar",
requestDate: new Date(new Date().setDate(new Date().getDate() - 5)),
status: "pending",
approvalDate: null,
expirationDate: null,
},
{
id: "PA2023-003",
patientId: patientsWithInsurance[0]?.id || 1,
procedureCode: "D7240",
procedureName: "Removal of impacted tooth",
requestDate: new Date(new Date().setDate(new Date().getDate() - 30)),
status: "denied",
approvalDate: null,
expirationDate: null,
denialReason: "Not medically necessary based on submitted documentation",
}
];
return (
<div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
<div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
<main className="flex-1 overflow-y-auto p-4">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-semibold text-gray-800">Pre-authorizations</h1>
<p className="text-gray-600">Manage insurance pre-authorizations for dental procedures</p>
</div>
{/* New Pre-Authorization Request Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">New Pre-Authorization Request</h2>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Recent Patients for Pre-Authorization</CardTitle>
</CardHeader>
<CardContent>
{isLoadingPatients ? (
<div className="text-center py-4">Loading patients data...</div>
) : patientsWithInsurance.length > 0 ? (
<div className="divide-y">
{patientsWithInsurance.map((patient) => (
<div
key={patient.id}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => {
setSelectedPatient(patient.id);
handleNewPreAuth(
patient.id,
dentalProcedures[Math.floor(Math.random() * 3)].name
);
}}
>
<div>
<h3 className="font-medium">{patient.firstName} {patient.lastName}</h3>
<div className="text-sm text-gray-500">
<span>Insurance: {patient.insuranceProvider === 'delta'
? 'Delta Dental'
: patient.insuranceProvider === 'metlife'
? 'MetLife'
: patient.insuranceProvider === 'cigna'
? 'Cigna'
: patient.insuranceProvider === 'aetna'
? 'Aetna'
: patient.insuranceProvider}</span>
<span className="mx-2"></span>
<span>ID: {patient.insuranceId}</span>
<span className="mx-2"></span>
<span>Procedure needed: {dentalProcedures[0].name}</span>
</div>
</div>
<div className="text-primary">
<ClipboardCheck className="h-5 w-5" />
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<ClipboardCheck className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No patients with insurance</h3>
<p className="text-gray-500 mt-1">
Add insurance information to patients to request pre-authorizations
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Pre-Authorization Submitted Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">Pre-Authorization Submitted</h2>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Pending Pre-Authorization Requests</CardTitle>
</CardHeader>
<CardContent>
{patientsWithInsurance.length > 0 ? (
<div className="divide-y">
{samplePreAuths.filter(auth => auth.status === 'pending').map((preAuth) => {
const patient = patients.find(p => p.id === preAuth.patientId) ||
{ firstName: "Unknown", lastName: "Patient" };
return (
<div
key={preAuth.id}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => toast({
title: "Pre-Authorization Details",
description: `Viewing details for ${preAuth.id}`
})}
>
<div>
<h3 className="font-medium">{patient.firstName} {patient.lastName} - {preAuth.procedureName}</h3>
<div className="text-sm text-gray-500">
<span>ID: {preAuth.id}</span>
<span className="mx-2"></span>
<span>Submitted: {format(preAuth.requestDate, 'MMM dd, yyyy')}</span>
<span className="mx-2"></span>
<span>Expected Response: {format(new Date(preAuth.requestDate.getTime() + 7 * 24 * 60 * 60 * 1000), 'MMM dd, yyyy')}</span>
</div>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-800">
<span className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
Pending
</span>
</span>
</div>
</div>
);
})}
{samplePreAuths.filter(auth => auth.status === 'pending').length === 0 && (
<div className="text-center py-8">
<Clock className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No pending requests</h3>
<p className="text-gray-500 mt-1">
Submitted pre-authorization requests will appear here
</p>
</div>
)}
</div>
) : (
<div className="text-center py-8">
<Clock className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No pre-authorization history</h3>
<p className="text-gray-500 mt-1">
Submitted pre-authorization requests will appear here
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Pre-Authorization Results Section */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-medium text-gray-800">Pre-Authorization Results</h2>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle>Completed Pre-Authorization Requests</CardTitle>
</CardHeader>
<CardContent>
{patientsWithInsurance.length > 0 ? (
<div className="divide-y">
{samplePreAuths.filter(auth => auth.status !== 'pending').map((preAuth) => {
const patient = patients.find(p => p.id === preAuth.patientId) ||
{ firstName: "Unknown", lastName: "Patient" };
return (
<div
key={preAuth.id}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => toast({
title: "Pre-Authorization Details",
description: `Viewing details for ${preAuth.id}`
})}
>
<div>
<h3 className="font-medium">{patient.firstName} {patient.lastName} - {preAuth.procedureName}</h3>
<div className="text-sm text-gray-500">
<span>ID: {preAuth.id}</span>
<span className="mx-2"></span>
<span>Requested: {format(preAuth.requestDate, 'MMM dd, yyyy')}</span>
{preAuth.status === 'approved' && (
<>
<span className="mx-2"></span>
<span>Expires: {format(preAuth.expirationDate as Date, 'MMM dd, yyyy')}</span>
</>
)}
{preAuth.status === 'denied' && preAuth.denialReason && (
<>
<span className="mx-2"></span>
<span className="text-red-600">Reason: {preAuth.denialReason}</span>
</>
)}
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
preAuth.status === 'approved' ? 'bg-green-100 text-green-800' :
'bg-red-100 text-red-800'
}`}>
{preAuth.status === 'approved' ? (
<span className="flex items-center">
<CheckCircle className="h-3 w-3 mr-1" />
Approved
</span>
) : (
<span className="flex items-center">
<AlertCircle className="h-3 w-3 mr-1" />
Denied
</span>
)}
</span>
</div>
</div>
);
})}
{samplePreAuths.filter(auth => auth.status !== 'pending').length === 0 && (
<div className="text-center py-8">
<CheckCircle className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No completed requests</h3>
<p className="text-gray-500 mt-1">
Processed pre-authorization results will appear here
</p>
</div>
)}
</div>
) : (
<div className="text-center py-8">
<CheckCircle className="h-12 w-12 mx-auto text-gray-400 mb-3" />
<h3 className="text-lg font-medium">No pre-authorization results</h3>
<p className="text-gray-500 mt-1">
Completed pre-authorization requests will appear here
</p>
</div>
)}
</CardContent>
</Card>
</div>
</main>
</div>
</div>
);
}