just brought
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
apps/Frontend/src/components/analytics/new-patients.tsx
Normal file
67
apps/Frontend/src/components/analytics/new-patients.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
255
apps/Frontend/src/pages/claims-page.tsx
Normal file
255
apps/Frontend/src/pages/claims-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 */}
|
||||||
|
|||||||
414
apps/Frontend/src/pages/payments-page.tsx
Normal file
414
apps/Frontend/src/pages/payments-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
350
apps/Frontend/src/pages/preauthorizations-page.tsx
Normal file
350
apps/Frontend/src/pages/preauthorizations-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user