claim page done

This commit is contained in:
2025-07-28 17:49:01 +05:30
parent d9ce19df80
commit 0930b70bfd
7 changed files with 152 additions and 206 deletions

View File

@@ -183,6 +183,8 @@ router.post(
}); });
const userId = req.user!.id; const userId = req.user!.id;
const originalStartTime = appointmentData.startTime;
const MAX_END_TIME = "18:30";
// 1. Verify patient exists and belongs to user // 1. Verify patient exists and belongs to user
const patient = await storage.getPatient(appointmentData.patientId); const patient = await storage.getPatient(appointmentData.patientId);
@@ -196,39 +198,87 @@ router.post(
}); });
} }
// 2. Check if patient already has an appointment on the same date and time. // 2. Attempt to find the next available slot
const sameDayAppointment = await storage.getPatientAppointmentByDateTime( let [hour, minute] = originalStartTime.split(":").map(Number);
appointmentData.patientId,
appointmentData.date,
appointmentData.startTime
);
// 3. Check if there's already an appointment at this time slot of Staff.
const staffConflict = await storage.getStaffAppointmentByDateTime(
appointmentData.staffId,
appointmentData.date,
appointmentData.startTime,
sameDayAppointment?.id
);
if (staffConflict) { const pad = (n: number) => n.toString().padStart(2, "0");
return res.status(409).json({
message:
"This time slot is already booked for the selected staff. Please choose another time or staff member.",
});
}
// 4. If same-day appointment exists, update it while (`${pad(hour)}:${pad(minute)}` <= MAX_END_TIME) {
if (sameDayAppointment?.id !== undefined) { const currentStartTime = `${pad(hour)}:${pad(minute)}`;
const updatedAppointment = await storage.updateAppointment(
sameDayAppointment.id, // Check patient appointment at this time
appointmentData const sameDayAppointment =
await storage.getPatientAppointmentByDateTime(
appointmentData.patientId,
appointmentData.date,
currentStartTime
);
// Check staff conflict at this time
const staffConflict = await storage.getStaffAppointmentByDateTime(
appointmentData.staffId,
appointmentData.date,
currentStartTime,
sameDayAppointment?.id // Ignore self if updating
); );
return res.status(200).json(updatedAppointment);
if (!staffConflict) {
const endMinute = minute + 30;
let endHour = hour + Math.floor(endMinute / 60);
let realEndMinute = endMinute % 60;
const currentEndTime = `${pad(endHour)}:${pad(realEndMinute)}`;
const payload = {
...appointmentData,
startTime: currentStartTime,
endTime: currentEndTime,
};
let responseData;
if (sameDayAppointment?.id !== undefined) {
const updated = await storage.updateAppointment(
sameDayAppointment.id,
payload
);
responseData = {
...updated,
originalRequestedTime: originalStartTime,
finalScheduledTime: currentStartTime,
message:
originalStartTime !== currentStartTime
? `Your requested time (${originalStartTime}) was unavailable. Appointment was updated to ${currentStartTime}.`
: `Appointment successfully updated at ${currentStartTime}.`,
};
return res.status(200).json(responseData);
}
const created = await storage.createAppointment(payload);
responseData = {
...created,
originalRequestedTime: originalStartTime,
finalScheduledTime: currentStartTime,
message:
originalStartTime !== currentStartTime
? `Your requested time (${originalStartTime}) was unavailable. Appointment was scheduled at ${currentStartTime}.`
: `Appointment successfully scheduled at ${currentStartTime}.`,
};
return res.status(201).json(responseData);
}
// Move to next 30-min slot
minute += 30;
if (minute >= 60) {
hour += 1;
minute = 0;
}
} }
// 6. Otherwise, create a new appointment return res.status(409).json({
const newAppointment = await storage.createAppointment(appointmentData); message:
return res.status(201).json(newAppointment); "No available slots remaining until 6:30 PM for this Staff. Please choose another day.",
});
} catch (error) { } catch (error) {
console.error("Error in upsert appointment:", error); console.error("Error in upsert appointment:", error);

View File

@@ -1,6 +1,6 @@
import { Router } from 'express'; import { Router } from 'express';
import patientRoutes from './patients'; import patientRoutes from './patients';
import appointmentRoutes from './appointements' import appointmentRoutes from './appointments'
import userRoutes from './users' import userRoutes from './users'
import staffRoutes from './staffs' import staffRoutes from './staffs'
import pdfExtractionRoutes from './pdfExtraction'; import pdfExtractionRoutes from './pdfExtraction';

View File

@@ -159,6 +159,29 @@ router.get("/search", async (req: Request, res: Response): Promise<any> => {
} }
}); });
// get patient by insurance id
router.get("/by-insurance-id", async (req: Request, res: Response): Promise<any> => {
const insuranceId = req.query.insuranceId?.toString();
if (!insuranceId) {
return res.status(400).json({ error: "Missing insuranceId" });
}
try {
const patient = await storage.getPatientByInsuranceId(insuranceId);
if (patient) {
return res.status(200).json(patient);
} else {
return res.status(404).json(null);
}
} catch (err) {
console.error("Failed to lookup patient:", err);
return res.status(500).json({ error: "Internal server error" });
}
});
// Get a single patient by ID // Get a single patient by ID
router.get( router.get(
"/:id", "/:id",

View File

@@ -24,7 +24,11 @@ const PatientSchema = (
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
export default function ClaimsOfPatientModal() { interface ClaimsOfPatientModalProps {
onNewClaim?: (patientId: number) => void;
}
export default function ClaimsOfPatientModal({ onNewClaim }: ClaimsOfPatientModalProps) {
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null); const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [claimsPage, setClaimsPage] = useState(1); const [claimsPage, setClaimsPage] = useState(1);
@@ -70,13 +74,18 @@ export default function ClaimsOfPatientModal() {
<CardHeader> <CardHeader>
<CardTitle>Patient Records</CardTitle> <CardTitle>Patient Records</CardTitle>
<CardDescription> <CardDescription>
View and manage all patient information Select any patient and View all their recent claims.
</CardDescription>
<CardDescription>
Also create new claim for any patients.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PatientTable <PatientTable
allowView allowView
allowCheckbox allowCheckbox
allowNewClaim
onNewClaim={onNewClaim}
onSelectPatient={handleSelectPatient} onSelectPatient={handleSelectPatient}
/> />
</CardContent> </CardContent>

View File

@@ -94,7 +94,6 @@ export default function ClaimsRecentTable({
patientId, patientId,
}: ClaimsRecentTableProps) { }: ClaimsRecentTableProps) {
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth();
const [isViewClaimOpen, setIsViewClaimOpen] = useState(false); const [isViewClaimOpen, setIsViewClaimOpen] = useState(false);
const [isEditClaimOpen, setIsEditClaimOpen] = useState(false); const [isEditClaimOpen, setIsEditClaimOpen] = useState(false);

View File

@@ -8,7 +8,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Delete, Edit, Eye } from "lucide-react"; import { Delete, Edit, Eye, FileCheck } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { import {
Pagination, Pagination,
@@ -69,6 +69,8 @@ interface PatientTableProps {
allowView?: boolean; allowView?: boolean;
allowDelete?: boolean; allowDelete?: boolean;
allowCheckbox?: boolean; allowCheckbox?: boolean;
allowNewClaim?: boolean;
onNewClaim?: (patientId: number) => void;
onSelectPatient?: (patient: Patient | null) => void; onSelectPatient?: (patient: Patient | null) => void;
onPageChange?: (page: number) => void; onPageChange?: (page: number) => void;
onSearchChange?: (searchTerm: string) => void; onSearchChange?: (searchTerm: string) => void;
@@ -79,6 +81,8 @@ export function PatientTable({
allowView, allowView,
allowDelete, allowDelete,
allowCheckbox, allowCheckbox,
allowNewClaim,
onNewClaim,
onSelectPatient, onSelectPatient,
onPageChange, onPageChange,
onSearchChange, onSearchChange,
@@ -481,6 +485,17 @@ export function PatientTable({
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
)} )}
{allowNewClaim && (
<Button
variant="ghost"
size="icon"
onClick={() => onNewClaim?.(patient.id)}
className="text-green-600 hover:text-green-800 hover:bg-green-50"
aria-label="New Claim"
>
<FileCheck className="h-5 w-5" />
</Button>
)}
{allowView && ( {allowView && (
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -97,39 +97,13 @@ export default function ClaimsPage() {
setIsMobileMenuOpen(!isMobileMenuOpen); setIsMobileMenuOpen(!isMobileMenuOpen);
}; };
// Fetch patients
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
Patient[]
>({
queryKey: ["/api/patients/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/patients/");
return res.json();
},
enabled: !!user,
});
// Fetch appointments
const {
data: appointments = [] as Appointment[],
isLoading: isLoadingAppointments,
} = useQuery<Appointment[]>({
queryKey: ["/api/appointments/all"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/appointments/all");
return res.json();
},
enabled: !!user,
});
// Add patient mutation // Add patient mutation
const addPatientMutation = useMutation({ const addPatientMutation = useMutation({
mutationFn: async (patient: InsertPatient) => { mutationFn: async (patient: InsertPatient) => {
const res = await apiRequest("POST", "/api/patients/", patient); const res = await apiRequest("POST", "/api/patients/", patient);
return res.json(); return res.json();
}, },
onSuccess: (newPatient) => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient added successfully!", description: "Patient added successfully!",
@@ -158,7 +132,6 @@ export default function ClaimsPage() {
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient updated successfully!", description: "Patient updated successfully!",
@@ -184,14 +157,11 @@ export default function ClaimsPage() {
); );
return await res.json(); return await res.json();
}, },
onSuccess: () => { onSuccess: (appointment) => {
toast({ toast({
title: "Success", title: "Appointment Scheduled",
description: "Appointment created successfully.", description: appointment.message || "Appointment created successfully.",
}); });
// Invalidate both appointments and patients queries
queryClient.invalidateQueries({ queryKey: ["/api/appointments/all"] });
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
}, },
onError: (error: Error) => { onError: (error: Error) => {
toast({ toast({
@@ -209,6 +179,11 @@ export default function ClaimsPage() {
return res.json(); return res.json();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["claims-recent"],
exact: false,
});
toast({ toast({
title: "Claim created successfully", title: "Claim created successfully",
variant: "default", variant: "default",
@@ -223,8 +198,17 @@ export default function ClaimsPage() {
}, },
}); });
// workflow starts from there - this params are set by pdf extraction/patient page. // helper
// the fields are either send by pdfExtraction or by selecting patients. const getPatientByInsuranceId = async (insuranceId: string) => {
const res = await apiRequest(
"GET",
`/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(insuranceId)}`
);
if (!res.ok) throw new Error("Failed to fetch patient by insuranceId");
return res.json(); // returns patient object or null
};
// workflow starts from there - this params are set by pdf extraction/patient page. then used in claim page here.
const [location] = useLocation(); const [location] = useLocation();
const { name, memberId, dob } = useMemo(() => { const { name, memberId, dob } = useMemo(() => {
const search = window.location.search; const search = window.location.search;
@@ -279,7 +263,7 @@ export default function ClaimsPage() {
}; };
fetchMatchingPatient(); fetchMatchingPatient();
} }
}, [memberId, dob]); }, [memberId]);
// 1. upsert appointment. // 1. upsert appointment.
const handleAppointmentSubmit = async ( const handleAppointmentSubmit = async (
@@ -468,53 +452,6 @@ export default function ClaimsPage() {
window.history.replaceState({}, document.title, url.toString()); window.history.replaceState({}, document.title, url.toString());
}; };
// helper func for frontend
const getDisplayProvider = (provider: string) => {
const insuranceMap: Record<string, string> = {
delta: "Delta Dental",
metlife: "MetLife",
cigna: "Cigna",
aetna: "Aetna",
};
return insuranceMap[provider?.toLowerCase()] || provider;
};
// Get unique patients with appointments - might not needed now, can shift this to the recent patients table
const patientsWithAppointments = patients.reduce(
(acc, patient) => {
const patientAppointments = appointments
.filter((appt) => appt.patientId === patient.id)
.sort(
(a, b) =>
parseLocalDate(b.date).getTime() - parseLocalDate(a.date).getTime()
); // Sort descending by date
if (patientAppointments.length > 0) {
const latestAppointment = patientAppointments[0];
acc.push({
patientId: patient.id,
patientName: `${patient.firstName} ${patient.lastName}`,
appointmentId: latestAppointment!.id,
insuranceProvider: patient.insuranceProvider || "N/A",
insuranceId: patient.insuranceId || "N/A",
lastAppointment: formatLocalDate(
parseLocalDate(latestAppointment!.date)
),
});
}
return acc;
},
[] as Array<{
patientId: number;
patientName: string;
appointmentId: number;
insuranceProvider: string;
insuranceId: string;
lastAppointment: string;
}>
);
return ( return (
<div className="flex h-screen overflow-hidden bg-gray-100"> <div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar <Sidebar
@@ -546,92 +483,8 @@ export default function ClaimsPage() {
</div> </div>
</div> </div>
{/* New Claims Section */} {/* Recent Claims by Patients also handles new claims */}
<div className="mb-8 mt-8"> <ClaimsOfPatientModal onNewClaim={handleNewClaim} />
<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(Number(firstPatient?.patientId));
} 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: (typeof patientsWithAppointments)[number]) => (
<div
key={item.patientId}
className="py-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => handleNewClaim(item.patientId)}
>
<div>
<h3 className="font-medium">{item.patientName}</h3>
<div className="text-sm text-gray-500">
<span>
Insurance:{" "}
{getDisplayProvider(item.insuranceProvider)}
</span>
<span className="mx-2"></span>
<span>ID: {item.insuranceId}</span>
<span className="mx-2"></span>
<span>
Last Visit:{" "}
{parseLocalDate(
item.lastAppointment
).toLocaleDateString("en-CA")}
</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>
{/* Recent Claims Section */} {/* Recent Claims Section */}
<Card> <Card>
@@ -649,9 +502,6 @@ export default function ClaimsPage() {
/> />
</CardContent> </CardContent>
</Card> </Card>
{/* Recent Claims by Patients */}
<ClaimsOfPatientModal />
</main> </main>
</div> </div>