patient table done, patient form done
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@repo/db/usedSchemas";
|
} from "@repo/db/usedSchemas";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { extractDobParts } from "../utils/DobParts";
|
import { extractDobParts } from "../utils/DobParts";
|
||||||
import { parseDobParts } from "../utils/DobPartsParsing";
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -134,75 +133,18 @@ router.get("/search", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dob) {
|
if (dob) {
|
||||||
const range = parseDobParts(dob);
|
const parsed = new Date(dob);
|
||||||
|
if (isNaN(parsed.getTime())) {
|
||||||
if (range) {
|
|
||||||
if (!filters.AND) filters.AND = [];
|
|
||||||
|
|
||||||
// Check what kind of match this was
|
|
||||||
const fromYear = range.from.getUTCFullYear();
|
|
||||||
const toYear = range.to.getUTCFullYear();
|
|
||||||
const fromMonth = range.from.getUTCMonth() + 1; // Prisma months: 1-12
|
|
||||||
const toMonth = range.to.getUTCMonth() + 1;
|
|
||||||
const fromDay = range.from.getUTCDate();
|
|
||||||
const toDay = range.to.getUTCDate();
|
|
||||||
|
|
||||||
const isFullDate =
|
|
||||||
fromYear === toYear && fromMonth === toMonth && fromDay === toDay;
|
|
||||||
|
|
||||||
const isDayMonthOnly =
|
|
||||||
fromYear === 1900 &&
|
|
||||||
toYear === 2100 &&
|
|
||||||
fromMonth === toMonth &&
|
|
||||||
fromDay === toDay;
|
|
||||||
|
|
||||||
const isMonthOnly =
|
|
||||||
fromYear === 1900 &&
|
|
||||||
toYear === 2100 &&
|
|
||||||
fromDay === 1 &&
|
|
||||||
toDay >= 28 &&
|
|
||||||
fromMonth === toMonth &&
|
|
||||||
toMonth === toMonth;
|
|
||||||
|
|
||||||
const isYearOnly = fromMonth === 1 && toMonth === 12;
|
|
||||||
|
|
||||||
if (isFullDate) {
|
|
||||||
filters.AND.push({
|
|
||||||
dob_day: fromDay,
|
|
||||||
dob_month: fromMonth,
|
|
||||||
dob_year: fromYear,
|
|
||||||
});
|
|
||||||
} else if (isDayMonthOnly) {
|
|
||||||
filters.AND.push({
|
|
||||||
dob_day: fromDay,
|
|
||||||
dob_month: fromMonth,
|
|
||||||
});
|
|
||||||
} else if (isMonthOnly) {
|
|
||||||
filters.AND.push({
|
|
||||||
dob_month: fromMonth,
|
|
||||||
});
|
|
||||||
} else if (isYearOnly) {
|
|
||||||
filters.AND.push({
|
|
||||||
dob_year: fromYear,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback: search via dateOfBirth range
|
|
||||||
filters.AND.push({
|
|
||||||
dateOfBirth: {
|
|
||||||
gte: range.from,
|
|
||||||
lte: range.to,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
message: `Invalid date format for DOB. Try formats like "12 March", "March", "1980", "12/03/1980", etc.`,
|
message: "Invalid date format for DOB. Use format: YYYY-MM-DD",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Match exact dateOfBirth (optional: adjust for timezone)
|
||||||
|
filters.dateOfBirth = parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [patients, totalCount] = await Promise.all([
|
const [patients, totalCount] = await Promise.all([
|
||||||
storage.searchPatients({
|
storage.searchPatients({
|
||||||
filters,
|
filters,
|
||||||
limit: parseInt(limit),
|
limit: parseInt(limit),
|
||||||
offset: parseInt(offset),
|
offset: parseInt(offset),
|
||||||
@@ -259,13 +201,8 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
|
|||||||
userId: req.user!.id,
|
userId: req.user!.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract dob_* from dateOfBirth
|
|
||||||
const dobParts = extractDobParts(new Date(patientData.dateOfBirth));
|
|
||||||
|
|
||||||
const patient = await storage.createPatient({
|
const patient = await storage.createPatient(patientData);
|
||||||
...patientData,
|
|
||||||
...dobParts, // adds dob_day/month/year
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(201).json(patient);
|
res.status(201).json(patient);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -307,15 +244,8 @@ router.put(
|
|||||||
// Validate request body
|
// Validate request body
|
||||||
const patientData = updatePatientSchema.parse(req.body);
|
const patientData = updatePatientSchema.parse(req.body);
|
||||||
|
|
||||||
let dobParts = {};
|
|
||||||
if (patientData.dateOfBirth) {
|
|
||||||
dobParts = extractDobParts(new Date(patientData.dateOfBirth));
|
|
||||||
}
|
|
||||||
// Update patient
|
// Update patient
|
||||||
const updatedPatient = await storage.updatePatient(patientId, {
|
const updatedPatient = await storage.updatePatient(patientId, patientData);
|
||||||
...patientData,
|
|
||||||
...dobParts,
|
|
||||||
});
|
|
||||||
res.json(updatedPatient);
|
res.json(updatedPatient);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
export function parseDobParts(input: string): { from: Date; to: Date } | null {
|
|
||||||
if (!input || typeof input !== "string") return null;
|
|
||||||
|
|
||||||
const parts = input.trim().split(/[\s/-]+/).filter(Boolean);
|
|
||||||
|
|
||||||
if (parts.length === 1) {
|
|
||||||
const part = parts[0]?.toLowerCase();
|
|
||||||
|
|
||||||
// Year
|
|
||||||
if (part && /^\d{4}$/.test(part)) {
|
|
||||||
const year = parseInt(part);
|
|
||||||
return {
|
|
||||||
from: new Date(Date.UTC(year, 0, 1)),
|
|
||||||
to: new Date(Date.UTC(year, 11, 31, 23, 59, 59)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Month
|
|
||||||
const month = part ? parseMonth(part) : null;
|
|
||||||
if (month !== null) {
|
|
||||||
return {
|
|
||||||
from: new Date(Date.UTC(1900, month, 1)),
|
|
||||||
to: new Date(Date.UTC(2100, month + 1, 0, 23, 59, 59)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const [part1, part2] = parts;
|
|
||||||
const day = tryParseInt(part1);
|
|
||||||
const month = part2 ? parseMonth(part2) : null;
|
|
||||||
|
|
||||||
if (day !== null && month !== null) {
|
|
||||||
return {
|
|
||||||
from: new Date(Date.UTC(1900, month, day)),
|
|
||||||
to: new Date(Date.UTC(2100, month, day, 23, 59, 59)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length === 3) {
|
|
||||||
const [part1, part2, part3] = parts;
|
|
||||||
const day = tryParseInt(part1);
|
|
||||||
const month = part2 ? parseMonth(part2) : null;
|
|
||||||
const year = tryParseInt(part3);
|
|
||||||
|
|
||||||
if (day !== null && month !== null && year !== null) {
|
|
||||||
return {
|
|
||||||
from: new Date(Date.UTC(year, month, day)),
|
|
||||||
to: new Date(Date.UTC(year, month, day, 23, 59, 59)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMonth(input: string): number | null {
|
|
||||||
const normalized = input.toLowerCase();
|
|
||||||
const months = [
|
|
||||||
"january", "february", "march", "april", "may", "june",
|
|
||||||
"july", "august", "september", "october", "november", "december",
|
|
||||||
];
|
|
||||||
|
|
||||||
const index = months.findIndex(
|
|
||||||
(m) => m === normalized || m.startsWith(normalized)
|
|
||||||
);
|
|
||||||
return index !== -1 ? index : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseInt(value?: string): number | null {
|
|
||||||
if (!value) return null;
|
|
||||||
const parsed = parseInt(value);
|
|
||||||
return isNaN(parsed) ? null : parsed;
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,16 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { forwardRef, useImperativeHandle } from "react";
|
import { forwardRef, useImperativeHandle } from "react";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Calendar } from "../ui/calendar";
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const PatientSchema = (
|
const PatientSchema = (
|
||||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
@@ -116,7 +126,9 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
|||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
submit() {
|
submit() {
|
||||||
(document.getElementById("patient-form") as HTMLFormElement | null)?.requestSubmit();
|
(
|
||||||
|
document.getElementById("patient-form") as HTMLFormElement | null
|
||||||
|
)?.requestSubmit();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -195,9 +207,41 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Date of Birth *</FormLabel>
|
<FormLabel>Date of Birth *</FormLabel>
|
||||||
<FormControl>
|
<Popover>
|
||||||
<Input type="date" {...field} />
|
<PopoverTrigger asChild>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full pl-3 text-left font-normal",
|
||||||
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value ? (
|
||||||
|
format(field.value, "PPP")
|
||||||
|
) : (
|
||||||
|
<span>Pick a date</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-4">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={field.value}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date) {
|
||||||
|
const localDate = format(date, "yyyy-MM-dd");
|
||||||
|
field.onChange(localDate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
(date) => date > new Date() // Prevent future dates
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -319,6 +363,32 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
|
|||||||
Insurance Information
|
Insurance Information
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="status"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Status *</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
defaultValue="active"
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="insuranceProvider"
|
name="insuranceProvider"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Search, X } from "lucide-react";
|
import { CalendarIcon, Search, X } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,14 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
|
||||||
export type SearchCriteria = {
|
export type SearchCriteria = {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
@@ -75,17 +83,54 @@ export function PatientSearch({
|
|||||||
<div className="w-full pt-8 pb-4 px-4">
|
<div className="w-full pt-8 pb-4 px-4">
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Input
|
{searchBy === "dob" ? (
|
||||||
placeholder="Search patients..."
|
<Popover>
|
||||||
value={searchTerm}
|
<PopoverTrigger asChild>
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
<Button
|
||||||
className="pr-10"
|
variant="outline"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") handleSearch();
|
||||||
handleSearch();
|
}}
|
||||||
}
|
className={cn(
|
||||||
}}
|
"w-full pl-3 pr-20 text-left font-normal",
|
||||||
/>
|
!searchTerm && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{searchTerm ? (
|
||||||
|
format(new Date(searchTerm), "PPP")
|
||||||
|
) : (
|
||||||
|
<span>Pick a date</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-4">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={searchTerm ? new Date(searchTerm) : undefined}
|
||||||
|
onSelect={(date: Date | undefined) => {
|
||||||
|
if (date) {
|
||||||
|
const formattedDate = format(date, "yyyy-MM-dd");
|
||||||
|
setSearchTerm(String(formattedDate));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={(date) => date > new Date()}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
placeholder="Search patients..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pr-10"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<button
|
<button
|
||||||
className="absolute right-10 top-3 text-gray-400 hover:text-gray-600"
|
className="absolute right-10 top-3 text-gray-400 hover:text-gray-600"
|
||||||
@@ -97,6 +142,7 @@ export function PatientSearch({
|
|||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
|
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
@@ -168,14 +214,59 @@ export function PatientSearch({
|
|||||||
<label className="text-right text-sm font-medium">
|
<label className="text-right text-sm font-medium">
|
||||||
Search term
|
Search term
|
||||||
</label>
|
</label>
|
||||||
<Input
|
{advancedCriteria.searchBy === "dob" ? (
|
||||||
className="col-span-3"
|
<Popover>
|
||||||
value={advancedCriteria.searchTerm}
|
<PopoverTrigger asChild>
|
||||||
onChange={(e) =>
|
<Button
|
||||||
updateAdvancedCriteria("searchTerm", e.target.value)
|
variant="outline"
|
||||||
}
|
onKeyDown={(e) => {
|
||||||
placeholder="Enter search term..."
|
if (e.key === "Enter") handleSearch();
|
||||||
/>
|
}}
|
||||||
|
className={cn(
|
||||||
|
"col-span-3 text-left font-normal",
|
||||||
|
!advancedCriteria.searchTerm &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{advancedCriteria.searchTerm ? (
|
||||||
|
format(new Date(advancedCriteria.searchTerm), "PPP")
|
||||||
|
) : (
|
||||||
|
<span>Pick a date</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-4">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={
|
||||||
|
advancedCriteria.searchTerm
|
||||||
|
? new Date(advancedCriteria.searchTerm)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date) {
|
||||||
|
const formattedDate = format(date, "yyyy-MM-dd");
|
||||||
|
updateAdvancedCriteria(
|
||||||
|
"searchTerm",
|
||||||
|
String(formattedDate)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={(date) => date > new Date()}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
className="col-span-3"
|
||||||
|
value={advancedCriteria.searchTerm}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAdvancedCriteria("searchTerm", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Enter search term..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Delete, Edit, Eye } from "lucide-react";
|
import { Delete, Edit, Eye } from "lucide-react";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
@@ -38,6 +37,7 @@ import { useAuth } from "@/hooks/use-auth";
|
|||||||
import { PatientSearch, SearchCriteria } from "./patient-search";
|
import { PatientSearch, SearchCriteria } from "./patient-search";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Checkbox } from "../ui/checkbox";
|
||||||
|
|
||||||
const PatientSchema = (
|
const PatientSchema = (
|
||||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
@@ -67,12 +67,16 @@ interface PatientTableProps {
|
|||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
allowView?: boolean;
|
allowView?: boolean;
|
||||||
allowDelete?: boolean;
|
allowDelete?: boolean;
|
||||||
|
allowCheckbox?: boolean;
|
||||||
|
onSelectPatient?: (patient: Patient) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PatientTable({
|
export function PatientTable({
|
||||||
allowEdit,
|
allowEdit,
|
||||||
allowView,
|
allowView,
|
||||||
allowDelete,
|
allowDelete,
|
||||||
|
allowCheckbox,
|
||||||
|
onSelectPatient,
|
||||||
}: PatientTableProps) {
|
}: PatientTableProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -94,12 +98,32 @@ export function PatientTable({
|
|||||||
);
|
);
|
||||||
const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500);
|
const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500);
|
||||||
|
|
||||||
|
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectPatient = (patient: Patient) => {
|
||||||
|
const isSelected = selectedPatientId === patient.id;
|
||||||
|
const newSelectedId = isSelected ? null : patient.id;
|
||||||
|
setSelectedPatientId(newSelectedId);
|
||||||
|
|
||||||
|
if (!isSelected && onSelectPatient) {
|
||||||
|
onSelectPatient(patient);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: patientsData,
|
data: patientsData,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery<PatientApiResponse, Error>({
|
} = useQuery<PatientApiResponse, Error>({
|
||||||
queryKey: ["patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent" }],
|
queryKey: [
|
||||||
|
"patients",
|
||||||
|
{
|
||||||
|
page: currentPage,
|
||||||
|
search: debouncedSearchCriteria?.searchTerm || "recent",
|
||||||
|
},
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim();
|
const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim();
|
||||||
const isSearch = trimmedTerm && trimmedTerm.length > 0;
|
const isSearch = trimmedTerm && trimmedTerm.length > 0;
|
||||||
@@ -149,8 +173,7 @@ export function PatientTable({
|
|||||||
patients: [],
|
patients: [],
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Update patient mutation
|
// Update patient mutation
|
||||||
const updatePatientMutation = useMutation({
|
const updatePatientMutation = useMutation({
|
||||||
@@ -166,7 +189,15 @@ export function PatientTable({
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsAddPatientOpen(false);
|
setIsAddPatientOpen(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ["patients", currentPage] });
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [
|
||||||
|
"patients",
|
||||||
|
{
|
||||||
|
page: currentPage,
|
||||||
|
search: debouncedSearchCriteria?.searchTerm || "recent",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Patient updated successfully!",
|
description: "Patient updated successfully!",
|
||||||
@@ -189,7 +220,15 @@ export function PatientTable({
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsDeletePatientOpen(false);
|
setIsDeletePatientOpen(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ["patients", currentPage] });
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [
|
||||||
|
"patients",
|
||||||
|
{
|
||||||
|
page: currentPage,
|
||||||
|
search: debouncedSearchCriteria?.searchTerm || "recent",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Patient deleted successfully!",
|
description: "Patient deleted successfully!",
|
||||||
@@ -305,6 +344,7 @@ export function PatientTable({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
{allowCheckbox && <TableHead>Select</TableHead>}
|
||||||
<TableHead>Patient</TableHead>
|
<TableHead>Patient</TableHead>
|
||||||
<TableHead>DOB / Gender</TableHead>
|
<TableHead>DOB / Gender</TableHead>
|
||||||
<TableHead>Contact</TableHead>
|
<TableHead>Contact</TableHead>
|
||||||
@@ -344,6 +384,15 @@ export function PatientTable({
|
|||||||
) : (
|
) : (
|
||||||
patientsData?.patients.map((patient) => (
|
patientsData?.patients.map((patient) => (
|
||||||
<TableRow key={patient.id} className="hover:bg-gray-50">
|
<TableRow key={patient.id} className="hover:bg-gray-50">
|
||||||
|
{allowCheckbox && (
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedPatientId === patient.id}
|
||||||
|
onCheckedChange={() => handleSelectPatient(patient)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -393,7 +442,7 @@ export function PatientTable({
|
|||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
||||||
patient.status === "active"
|
patient.status === "active"
|
||||||
? "bg-green-100 text-green-800"
|
? "bg-green-100 text-green-800"
|
||||||
: "bg-gray-100 text-gray-800"
|
: "bg-red-100 text-red-800"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{patient.status === "active" ? "Active" : "Inactive"}
|
{patient.status === "active" ? "Active" : "Inactive"}
|
||||||
@@ -498,7 +547,7 @@ export function PatientTable({
|
|||||||
className={`${
|
className={`${
|
||||||
currentPatient.status === "active"
|
currentPatient.status === "active"
|
||||||
? "text-green-600"
|
? "text-green-600"
|
||||||
: "text-amber-600"
|
: "text-red-600"
|
||||||
} font-medium`}
|
} font-medium`}
|
||||||
>
|
>
|
||||||
{currentPatient.status.charAt(0).toUpperCase() +
|
{currentPatient.status.charAt(0).toUpperCase() +
|
||||||
|
|||||||
@@ -3,21 +3,24 @@ import { DayPicker } from "react-day-picker";
|
|||||||
import type { DateRange } from "react-day-picker";
|
import type { DateRange } from "react-day-picker";
|
||||||
import "react-day-picker/style.css";
|
import "react-day-picker/style.css";
|
||||||
|
|
||||||
type BaseProps = Omit<React.ComponentProps<typeof DayPicker>, 'mode' | 'selected' | 'onSelect'>;
|
type BaseProps = Omit<
|
||||||
|
React.ComponentProps<typeof DayPicker>,
|
||||||
|
"mode" | "selected" | "onSelect"
|
||||||
|
>;
|
||||||
|
|
||||||
type CalendarProps =
|
type CalendarProps =
|
||||||
| (BaseProps & {
|
| (BaseProps & {
|
||||||
mode: 'single';
|
mode: "single";
|
||||||
selected?: Date;
|
selected?: Date;
|
||||||
onSelect?: (date: Date | undefined) => void;
|
onSelect?: (date: Date | undefined) => void;
|
||||||
})
|
})
|
||||||
| (BaseProps & {
|
| (BaseProps & {
|
||||||
mode: 'range';
|
mode: "range";
|
||||||
selected?: DateRange;
|
selected?: DateRange;
|
||||||
onSelect?: (range: DateRange | undefined) => void;
|
onSelect?: (range: DateRange | undefined) => void;
|
||||||
})
|
})
|
||||||
| (BaseProps & {
|
| (BaseProps & {
|
||||||
mode: 'multiple';
|
mode: "multiple";
|
||||||
selected?: Date[];
|
selected?: Date[];
|
||||||
onSelect?: (dates: Date[] | undefined) => void;
|
onSelect?: (dates: Date[] | undefined) => void;
|
||||||
});
|
});
|
||||||
@@ -25,7 +28,8 @@ type CalendarProps =
|
|||||||
export function Calendar(props: CalendarProps) {
|
export function Calendar(props: CalendarProps) {
|
||||||
const { mode, selected, onSelect, className, ...rest } = props;
|
const { mode, selected, onSelect, className, ...rest } = props;
|
||||||
|
|
||||||
const [internalSelected, setInternalSelected] = useState<typeof selected>(selected);
|
const [internalSelected, setInternalSelected] =
|
||||||
|
useState<typeof selected>(selected);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalSelected(selected);
|
setInternalSelected(selected);
|
||||||
@@ -38,23 +42,24 @@ export function Calendar(props: CalendarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${className || ''} day-picker-small-scale`}
|
className={`${className || ""} day-picker-small-scale`}
|
||||||
style={{
|
style={{
|
||||||
transform: 'scale(0.9)',
|
transform: "scale(0.9)",
|
||||||
transformOrigin: 'top left',
|
transformOrigin: "top left",
|
||||||
width: 'fit-content',
|
width: "fit-content",
|
||||||
height: 'fit-content',
|
height: "fit-content",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mode === 'single' && (
|
{mode === "single" && (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={internalSelected as Date | undefined}
|
selected={internalSelected as Date | undefined}
|
||||||
onSelect={handleSelect as (date: Date | undefined) => void}
|
onSelect={handleSelect as (date: Date | undefined) => void}
|
||||||
|
captionLayout="dropdown" // ✅ Enables month/year dropdown
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mode === 'range' && (
|
{mode === "range" && (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
mode="range"
|
mode="range"
|
||||||
selected={internalSelected as DateRange | undefined}
|
selected={internalSelected as DateRange | undefined}
|
||||||
@@ -62,7 +67,7 @@ export function Calendar(props: CalendarProps) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mode === 'multiple' && (
|
{mode === "multiple" && (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
selected={internalSelected as Date[] | undefined}
|
selected={internalSelected as Date[] | undefined}
|
||||||
|
|||||||
@@ -64,4 +64,12 @@
|
|||||||
body {
|
body {
|
||||||
@apply font-sans antialiased bg-background text-foreground;
|
@apply font-sans antialiased bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.day-picker-small-scale select {
|
||||||
|
@apply text-sm text-gray-900 bg-white border border-gray-300 rounded-md focus:outline-none focus:border-blue-600 focus:ring-1 focus:ring-blue-600;
|
||||||
|
height: 32px; /* Fixed height: ~h-8 */
|
||||||
|
line-height: 1.25rem; /* To align text nicely */
|
||||||
|
padding: 0 0.5rem; /* Override padding */
|
||||||
|
min-width: 6rem; /* Prevent shrinking */
|
||||||
|
appearance: none; /* Removes native styling */
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { CheckCircle } from "lucide-react";
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Edit,
|
|
||||||
Eye,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Settings,
|
|
||||||
CheckCircle
|
|
||||||
} from "lucide-react";
|
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { CredentialsModal } from "@/components/insurance/credentials-modal";
|
import { z } from "zod";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {z} from 'zod';
|
|
||||||
|
|
||||||
const PatientSchema = (
|
const PatientSchema = (
|
||||||
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
@@ -40,53 +28,59 @@ type Patient = z.infer<typeof PatientSchema>;
|
|||||||
|
|
||||||
export default function InsuranceEligibilityPage() {
|
export default function InsuranceEligibilityPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const toggleMobileMenu = () => {
|
||||||
const [searchField, setSearchField] = useState("all");
|
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
};
|
||||||
const itemsPerPage = 5;
|
|
||||||
|
|
||||||
// Insurance eligibility check form fields
|
// Insurance eligibility check form fields
|
||||||
const [memberId, setMemberId] = useState("");
|
const [memberId, setMemberId] = useState("");
|
||||||
const [dateOfBirth, setDateOfBirth] = useState("");
|
const [dateOfBirth, setDateOfBirth] = useState("");
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
|
|
||||||
// Selected patient for insurance check
|
// Selected patient for insurance check
|
||||||
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
|
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
// Insurance automation states
|
// Insurance automation states
|
||||||
const [isCredentialsModalOpen, setIsCredentialsModalOpen] = useState(false);
|
|
||||||
const [selectedProvider, setSelectedProvider] = useState("");
|
const [selectedProvider, setSelectedProvider] = useState("");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Insurance eligibility check mutation
|
// Insurance eligibility check mutation
|
||||||
const checkInsuranceMutation = useMutation({
|
const checkInsuranceMutation = useMutation({
|
||||||
mutationFn: async ({ provider, patientId, credentials }: {
|
mutationFn: async ({
|
||||||
|
provider,
|
||||||
|
patientId,
|
||||||
|
credentials,
|
||||||
|
}: {
|
||||||
provider: string;
|
provider: string;
|
||||||
patientId: number;
|
patientId: number;
|
||||||
credentials: { username: string; password: string };
|
credentials: { username: string; password: string };
|
||||||
}) => {
|
}) => {
|
||||||
const response = await fetch('/api/insurance/check', {
|
const response = await fetch("/api/insurance/check", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ provider, patientId, credentials }),
|
body: JSON.stringify({ provider, patientId, credentials }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to check insurance');
|
throw new Error("Failed to check insurance");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Insurance Check Complete",
|
title: "Insurance Check Complete",
|
||||||
description: result.isEligible ?
|
description: result.isEligible
|
||||||
`Patient is eligible. Plan: ${result.planName}` :
|
? `Patient is eligible. Plan: ${result.planName}`
|
||||||
"Patient eligibility could not be verified",
|
: "Patient eligibility could not be verified",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
@@ -98,150 +92,41 @@ export default function InsuranceEligibilityPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch patients
|
|
||||||
const {
|
|
||||||
data: patients = [],
|
|
||||||
isLoading: isLoadingPatients,
|
|
||||||
} = useQuery<Patient[]>({
|
|
||||||
queryKey: ["/api/patients"],
|
|
||||||
enabled: !!user,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter patients based on search
|
|
||||||
const filteredPatients = patients.filter(patient => {
|
|
||||||
if (!searchTerm) return true;
|
|
||||||
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
|
||||||
const fullName = `${patient.firstName} ${patient.lastName}`.toLowerCase();
|
|
||||||
const patientId = `PID-${patient.id.toString().padStart(4, '0')}`;
|
|
||||||
|
|
||||||
switch (searchField) {
|
|
||||||
case "name":
|
|
||||||
return fullName.includes(searchLower);
|
|
||||||
case "id":
|
|
||||||
return patientId.toLowerCase().includes(searchLower);
|
|
||||||
case "phone":
|
|
||||||
return patient.phone?.toLowerCase().includes(searchLower) || false;
|
|
||||||
case "all":
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
fullName.includes(searchLower) ||
|
|
||||||
patientId.toLowerCase().includes(searchLower) ||
|
|
||||||
patient.phone?.toLowerCase().includes(searchLower) ||
|
|
||||||
patient.email?.toLowerCase().includes(searchLower) ||
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
const totalPages = Math.ceil(filteredPatients.length / itemsPerPage);
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
||||||
const endIndex = startIndex + itemsPerPage;
|
|
||||||
const currentPatients = filteredPatients.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPatientInitials = (firstName: string, lastName: string) => {
|
|
||||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Toggle patient selection
|
|
||||||
const togglePatientSelection = (patientId: number) => {
|
|
||||||
setSelectedPatientId(selectedPatientId === patientId ? null : patientId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle insurance provider button clicks
|
// Handle insurance provider button clicks
|
||||||
const handleProviderClick = (providerName: string) => {
|
const handleProviderClick = (providerName: string) => {
|
||||||
if (!selectedPatientId) {
|
if (!selectedPatientId) {
|
||||||
toast({
|
toast({
|
||||||
title: "No Patient Selected",
|
title: "No Patient Selected",
|
||||||
description: "Please select a patient first by checking the box next to their name.",
|
description:
|
||||||
|
"Please select a patient first by checking the box next to their name.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedProvider(providerName);
|
setSelectedProvider(providerName);
|
||||||
setIsCredentialsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle credentials submission and start automation
|
|
||||||
const handleCredentialsSubmit = (credentials: { username: string; password: string }) => {
|
|
||||||
if (selectedPatientId && selectedProvider) {
|
|
||||||
// Use the provided MH credentials for MH provider
|
|
||||||
const finalCredentials = selectedProvider.toLowerCase() === 'mh' ? {
|
|
||||||
username: 'kqkgaox@yahoo.com',
|
|
||||||
password: 'Lex123456'
|
|
||||||
} : credentials;
|
|
||||||
|
|
||||||
checkInsuranceMutation.mutate({
|
|
||||||
provider: selectedProvider,
|
|
||||||
patientId: selectedPatientId,
|
|
||||||
credentials: finalCredentials,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setIsCredentialsModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEligibilityCheck = () => {
|
|
||||||
// TODO: Implement insurance eligibility check
|
|
||||||
console.log("Checking MH eligibility:", { memberId, dateOfBirth, firstName, lastName });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeltaMACheck = () => {
|
|
||||||
// TODO: Implement Delta MA eligibility check
|
|
||||||
console.log("Checking Delta MA eligibility:", { memberId, dateOfBirth, firstName, lastName });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMetlifeDentalCheck = () => {
|
|
||||||
// TODO: Implement Metlife Dental eligibility check
|
|
||||||
console.log("Checking Metlife Dental eligibility:", { memberId, dateOfBirth, firstName, lastName });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePatientSelect = (patientId: number) => {
|
|
||||||
const patient = patients.find(p => p.id === patientId);
|
|
||||||
if (patient) {
|
|
||||||
setSelectedPatientId(patientId);
|
|
||||||
// Auto-fill form fields with selected patient data
|
|
||||||
setMemberId(patient.insuranceId || "");
|
|
||||||
setDateOfBirth(patient.dateOfBirth || "");
|
|
||||||
setFirstName(patient.firstName || "");
|
|
||||||
setLastName(patient.lastName || "");
|
|
||||||
} else {
|
|
||||||
setSelectedPatientId(null);
|
|
||||||
// Clear form fields
|
|
||||||
setMemberId("");
|
|
||||||
setDateOfBirth("");
|
|
||||||
setFirstName("");
|
|
||||||
setLastName("");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50">
|
<div className="flex h-screen bg-gray-50">
|
||||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
<Sidebar
|
||||||
|
isMobileOpen={isMobileMenuOpen}
|
||||||
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto p-6">
|
<main className="flex-1 overflow-auto p-6">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 mb-2">Insurance Eligibility</h1>
|
<h1 className="text-2xl font-semibold text-gray-900 mb-2">
|
||||||
<p className="text-gray-600">Check insurance eligibility and view patient information</p>
|
Insurance Eligibility
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Check insurance eligibility and view patient information
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Insurance Eligibility Check Form */}
|
{/* Insurance Eligibility Check Form */}
|
||||||
@@ -288,236 +173,43 @@ export default function InsuranceEligibilityPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<Button
|
||||||
<div>
|
onClick={() => handleProviderClick("MH")}
|
||||||
<Button
|
className="w-full"
|
||||||
onClick={() => handleProviderClick('MH')}
|
disabled={checkInsuranceMutation.isPending}
|
||||||
className="w-full"
|
>
|
||||||
disabled={checkInsuranceMutation.isPending}
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
>
|
{checkInsuranceMutation.isPending &&
|
||||||
<CheckCircle className="h-4 w-4 mr-2" />
|
selectedProvider === "MH"
|
||||||
{checkInsuranceMutation.isPending && selectedProvider === 'MH' ? 'Checking...' : 'MH'}
|
? "Checking..."
|
||||||
</Button>
|
: "MH"}
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Search and Filters */}
|
{/* Patients Table */}
|
||||||
<Card className="mb-6">
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
|
||||||
<div className="flex-1 relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search patients..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select value={searchField} onValueChange={setSearchField}>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Fields</SelectItem>
|
|
||||||
<SelectItem value="name">Name</SelectItem>
|
|
||||||
<SelectItem value="id">Patient ID</SelectItem>
|
|
||||||
<SelectItem value="phone">Phone</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Settings className="h-4 w-4 mr-2" />
|
|
||||||
Advanced
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Patient List */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardHeader>
|
||||||
{isLoadingPatients ? (
|
<CardTitle>Patient Records</CardTitle>
|
||||||
<div className="text-center py-8">Loading patients...</div>
|
<CardDescription>
|
||||||
) : (
|
Select Patients and Check Their Eligibility
|
||||||
<>
|
</CardDescription>
|
||||||
{/* Table Header */}
|
</CardHeader>
|
||||||
<div className="grid grid-cols-12 gap-4 p-4 bg-gray-50 border-b text-sm font-medium text-gray-600">
|
<CardContent>
|
||||||
<div className="col-span-1">Select</div>
|
<PatientTable
|
||||||
<div className="col-span-3">Patient</div>
|
allowView={true}
|
||||||
<div className="col-span-2">DOB / Gender</div>
|
allowDelete={true}
|
||||||
<div className="col-span-2">Contact</div>
|
allowCheckbox={true}
|
||||||
<div className="col-span-2">Insurance</div>
|
allowEdit={true}
|
||||||
<div className="col-span-1">Status</div>
|
onSelectPatient={setSelectedPatient}
|
||||||
<div className="col-span-1">Actions</div>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table Rows */}
|
|
||||||
{currentPatients.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
{searchTerm ? "No patients found matching your search." : "No patients available."}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
currentPatients.map((patient) => (
|
|
||||||
<div
|
|
||||||
key={patient.id}
|
|
||||||
className={cn(
|
|
||||||
"grid grid-cols-12 gap-4 p-4 border-b hover:bg-gray-50 transition-colors",
|
|
||||||
selectedPatientId === patient.id && "bg-blue-50 border-blue-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Select Checkbox */}
|
|
||||||
<div className="col-span-1 flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedPatientId === patient.id}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (checked) {
|
|
||||||
handlePatientSelect(patient.id);
|
|
||||||
} else {
|
|
||||||
handlePatientSelect(0); // This will clear the selection
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Patient Info */}
|
|
||||||
<div className="col-span-3 flex items-center space-x-3">
|
|
||||||
<div className="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center text-sm font-medium text-gray-600">
|
|
||||||
{getPatientInitials(patient.firstName, patient.lastName)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-gray-900">
|
|
||||||
{patient.firstName} {patient.lastName}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
PID-{patient.id.toString().padStart(4, '0')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* DOB / Gender */}
|
|
||||||
<div className="col-span-2">
|
|
||||||
<div className="text-sm text-gray-900">
|
|
||||||
{formatDate(patient.dateOfBirth)}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500 capitalize">
|
|
||||||
{patient.gender}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact */}
|
|
||||||
<div className="col-span-2">
|
|
||||||
<div className="text-sm text-gray-900">
|
|
||||||
{patient.phone || 'Not provided'}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{patient.email || 'No email'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Insurance */}
|
|
||||||
<div className="col-span-2">
|
|
||||||
<div className="text-sm text-gray-900">
|
|
||||||
{patient.insuranceProvider ?
|
|
||||||
`${patient.insuranceProvider.charAt(0).toUpperCase()}${patient.insuranceProvider.slice(1)}` :
|
|
||||||
'Not specified'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
ID: {patient.insuranceId || 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status */}
|
|
||||||
<div className="col-span-1">
|
|
||||||
<span className={cn(
|
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
|
||||||
patient.status === 'active'
|
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-gray-100 text-gray-800"
|
|
||||||
)}>
|
|
||||||
{patient.status === 'active' ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="col-span-1">
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
||||||
<Edit className="h-4 w-4 text-blue-600" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
||||||
<Eye className="h-4 w-4 text-gray-600" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<div className="flex items-center justify-between p-4 border-t bg-gray-50">
|
|
||||||
<div className="text-sm text-gray-700">
|
|
||||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredPatients.length)} of {filteredPatients.length} results
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Page Numbers */}
|
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
|
||||||
<Button
|
|
||||||
key={page}
|
|
||||||
variant={currentPage === page ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentPage(page)}
|
|
||||||
className="w-8 h-8 p-0"
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Credentials Modal */}
|
|
||||||
<CredentialsModal
|
|
||||||
isOpen={isCredentialsModalOpen}
|
|
||||||
onClose={() => setIsCredentialsModalOpen(false)}
|
|
||||||
onSubmit={handleCredentialsSubmit}
|
|
||||||
providerName={selectedProvider}
|
|
||||||
isLoading={checkInsuranceMutation.isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ model Patient {
|
|||||||
firstName String
|
firstName String
|
||||||
lastName String
|
lastName String
|
||||||
dateOfBirth DateTime @db.Date
|
dateOfBirth DateTime @db.Date
|
||||||
dob_day Int?
|
|
||||||
dob_month Int?
|
|
||||||
dob_year Int?
|
|
||||||
gender String
|
gender String
|
||||||
phone String
|
phone String
|
||||||
email String?
|
email String?
|
||||||
|
|||||||
Reference in New Issue
Block a user