document page updated
This commit is contained in:
@@ -132,7 +132,7 @@ router.post(
|
|||||||
return sendError(res, "Unauthorized: user info missing", 401);
|
return sendError(res, "Unauthorized: user info missing", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { patientId, claimId, pdf_url } = req.body;
|
const { patientId, pdf_url } = req.body;
|
||||||
|
|
||||||
if (!pdf_url) {
|
if (!pdf_url) {
|
||||||
return sendError(res, "Missing pdf_url");
|
return sendError(res, "Missing pdf_url");
|
||||||
@@ -141,26 +141,35 @@ router.post(
|
|||||||
if (!patientId) {
|
if (!patientId) {
|
||||||
return sendError(res, "Missing Patient Id");
|
return sendError(res, "Missing Patient Id");
|
||||||
}
|
}
|
||||||
if (!claimId) {
|
|
||||||
return sendError(res, "Missing Claim Id");
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedPatientId = parseInt(patientId);
|
const parsedPatientId = parseInt(patientId);
|
||||||
const parsedClaimId = parseInt(claimId);
|
|
||||||
|
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
const filename = path.basename(new URL(pdf_url).pathname);
|
||||||
const pdfResponse = await axios.get(pdf_url, {
|
const pdfResponse = await axios.get(pdf_url, {
|
||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
});
|
});
|
||||||
|
|
||||||
// saving at postgres db
|
const groupTitle = `Insurance Claim`;
|
||||||
await storage.createClaimPdf(
|
const groupCategory = "CLAIM";
|
||||||
|
|
||||||
|
// ✅ Find or create PDF group for this claim
|
||||||
|
let group = await storage.findPdfGroupByPatientTitleAndCategory(
|
||||||
parsedPatientId,
|
parsedPatientId,
|
||||||
parsedClaimId,
|
groupTitle,
|
||||||
filename,
|
groupCategory
|
||||||
pdfResponse.data
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
group = await storage.createPdfGroup(
|
||||||
|
parsedPatientId,
|
||||||
|
groupTitle,
|
||||||
|
groupCategory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Save PDF file into that group
|
||||||
|
await storage.createPdfFile(group.id!, filename, pdfResponse.data);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
pdfPath: `/temp/${filename}`,
|
pdfPath: `/temp/${filename}`,
|
||||||
|
|||||||
@@ -33,6 +33,24 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/pdf-groups/patient/:patientId",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const { patientId } = req.params;
|
||||||
|
if (!patientId) {
|
||||||
|
return res.status(400).json({ error: "Missing patient ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = await storage.getPdfGroupsByPatientId(parseInt(patientId));
|
||||||
|
res.json(groups);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching groups by patient ID:", err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/pdf-groups/:id",
|
"/pdf-groups/:id",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
@@ -126,6 +144,21 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get("/pdf-files/group/:groupId", async (req: Request, res: Response):Promise<any> => {
|
||||||
|
try {
|
||||||
|
const idParam = req.params.groupId;
|
||||||
|
if (!idParam) {
|
||||||
|
return res.status(400).json({ error: "Missing Groupt ID" });
|
||||||
|
}
|
||||||
|
const groupId = parseInt(idParam);
|
||||||
|
const files = await storage.getPdfFilesByGroupId(groupId); // implement this
|
||||||
|
res.json(files);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/pdf-files/:id",
|
"/pdf-files/:id",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Router } from "express";
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
import { forwardToSeleniumInsuranceEligibilityAgent } from "../services/seleniumInsuranceEligibilityClient";
|
import { forwardToSeleniumInsuranceEligibilityAgent } from "../services/seleniumInsuranceEligibilityClient";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -48,15 +50,55 @@ router.post("/check", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const newStatus = result.eligibility === "Y" ? "active" : "inactive";
|
const newStatus = result.eligibility === "Y" ? "active" : "inactive";
|
||||||
await storage.updatePatient(patient.id, { status: newStatus });
|
await storage.updatePatient(patient.id, { status: newStatus });
|
||||||
result.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
result.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
|
// ✅ Step 2: Handle PDF Upload
|
||||||
|
if (result.pdf_path && result.pdf_path.endsWith(".pdf")) {
|
||||||
|
const pdfBuffer = await fs.readFile(result.pdf_path);
|
||||||
|
|
||||||
|
const groupTitle = "Eligibility PDFs";
|
||||||
|
const groupCategory = "ELIGIBILITY";
|
||||||
|
|
||||||
|
let group = await storage.findPdfGroupByPatientTitleAndCategory(
|
||||||
|
patient.id,
|
||||||
|
groupTitle,
|
||||||
|
groupCategory
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2b: Create group if it doesn’t exist
|
||||||
|
if (!group) {
|
||||||
|
group = await storage.createPdfGroup(
|
||||||
|
patient.id,
|
||||||
|
groupTitle,
|
||||||
|
groupCategory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group?.id) {
|
||||||
|
throw new Error("PDF group creation failed: missing group ID");
|
||||||
|
}
|
||||||
|
await storage.createPdfFile(
|
||||||
|
group.id,
|
||||||
|
path.basename(result.pdf_path),
|
||||||
|
pdfBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.unlink(result.pdf_path);
|
||||||
|
|
||||||
|
result.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
||||||
|
} else {
|
||||||
|
result.pdfUploadStatus =
|
||||||
|
"No valid PDF path provided by Selenium, Couldn't upload pdf to server.";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
|
||||||
`No patient found with insuranceId: ${insuranceEligibilityData.memberId}`
|
|
||||||
);
|
|
||||||
result.patientUpdateStatus =
|
result.patientUpdateStatus =
|
||||||
"Patient not found or missing ID; no update performed";
|
"Patient not found or missing ID; no update performed";
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(result);
|
res.json({
|
||||||
|
patientUpdateStatus: result.patientUpdateStatus,
|
||||||
|
pdfUploadStatus: result.pdfUploadStatus,
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|||||||
@@ -266,11 +266,15 @@ export interface IStorage {
|
|||||||
|
|
||||||
// Group management
|
// Group management
|
||||||
createPdfGroup(
|
createPdfGroup(
|
||||||
patientId: number,
|
patientId: number,
|
||||||
title: string,
|
title: string,
|
||||||
category: PdfCategory
|
category: PdfCategory
|
||||||
): Promise<PdfGroup>;
|
): Promise<PdfGroup>;
|
||||||
|
findPdfGroupByPatientTitleAndCategory(
|
||||||
|
patientId: number,
|
||||||
|
title: string,
|
||||||
|
category: PdfCategory
|
||||||
|
): Promise<PdfGroup | undefined>;
|
||||||
getAllPdfGroups(): Promise<PdfGroup[]>;
|
getAllPdfGroups(): Promise<PdfGroup[]>;
|
||||||
getPdfGroupById(id: number): Promise<PdfGroup | undefined>;
|
getPdfGroupById(id: number): Promise<PdfGroup | undefined>;
|
||||||
getPdfGroupsByPatientId(patientId: number): Promise<PdfGroup[]>;
|
getPdfGroupsByPatientId(patientId: number): Promise<PdfGroup[]>;
|
||||||
@@ -694,6 +698,18 @@ export const storage: IStorage = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async findPdfGroupByPatientTitleAndCategory(patientId, title, category) {
|
||||||
|
return (
|
||||||
|
(await db.pdfGroup.findFirst({
|
||||||
|
where: {
|
||||||
|
patientId,
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
},
|
||||||
|
})) ?? undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
async getPdfGroupById(id) {
|
async getPdfGroupById(id) {
|
||||||
return (await db.pdfGroup.findUnique({ where: { id } })) ?? undefined;
|
return (await db.pdfGroup.findUnique({ where: { id } })) ?? undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { z } from "zod";
|
|||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import RecentClaims from "@/components/claims/recent-claims";
|
import RecentClaims from "@/components/claims/recent-claims";
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "@/redux/hooks";
|
import { useAppDispatch, useAppSelector } from "@/redux/hooks";
|
||||||
import {
|
import {
|
||||||
setTaskStatus,
|
setTaskStatus,
|
||||||
@@ -556,9 +555,6 @@ export default function ClaimsPage() {
|
|||||||
// selenium pdf download handler
|
// selenium pdf download handler
|
||||||
const handleSeleniumPdfDownload = async (data: any) => {
|
const handleSeleniumPdfDownload = async (data: any) => {
|
||||||
try {
|
try {
|
||||||
if (!data.claimId) {
|
|
||||||
throw new Error("Missing claimId in handleSeleniumPdfDownload");
|
|
||||||
}
|
|
||||||
if (!selectedPatient) {
|
if (!selectedPatient) {
|
||||||
throw new Error("Missing patientId");
|
throw new Error("Missing patientId");
|
||||||
}
|
}
|
||||||
@@ -572,7 +568,6 @@ export default function ClaimsPage() {
|
|||||||
|
|
||||||
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
||||||
patientId: selectedPatient,
|
patientId: selectedPatient,
|
||||||
claimId: data.claimId,
|
|
||||||
pdf_url: data.pdf_url,
|
pdf_url: data.pdf_url,
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|||||||
474
apps/Frontend/src/pages/documents-page-basic.tsx
Normal file
474
apps/Frontend/src/pages/documents-page-basic.tsx
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Eye,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Settings,
|
||||||
|
Trash,
|
||||||
|
Download,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { ClaimPdfUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
|
import { z } from "zod";
|
||||||
|
import "@react-pdf-viewer/core/lib/styles/index.css";
|
||||||
|
import "@react-pdf-viewer/default-layout/lib/styles/index.css";
|
||||||
|
import { Viewer, Worker } from "@react-pdf-viewer/core";
|
||||||
|
import { defaultLayoutPlugin } from "@react-pdf-viewer/default-layout";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
||||||
|
|
||||||
|
const ClaimPdfSchema =
|
||||||
|
ClaimPdfUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>;
|
||||||
|
type ClaimPdf = z.infer<typeof ClaimPdfSchema>;
|
||||||
|
|
||||||
|
export default function DocumentsPage() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [searchField, setSearchField] = useState("all");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 5;
|
||||||
|
const [selectedPdfId, setSelectedPdfId] = useState<number | null>(null);
|
||||||
|
const defaultLayoutPluginInstance = defaultLayoutPlugin();
|
||||||
|
const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
||||||
|
const [currentPdf, setCurrentPdf] = useState<ClaimPdf | null>(null);
|
||||||
|
|
||||||
|
const { data: pdfs = [], isLoading } = useQuery<ClaimPdf[]>({
|
||||||
|
queryKey: ["/api/documents/claim-pdf/recent"],
|
||||||
|
enabled: !!user,
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/documents/claim-pdf/recent");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletePdfMutation = useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
await apiRequest("DELETE", `/api/documents/claim-pdf/${id}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsDeletePdfOpen(false);
|
||||||
|
setCurrentPdf(null);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["/api/documents/claim-pdf/recent"],
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "PDF deleted successfully!",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
console.error("Error deleting PDF:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to delete PDF: ${error.message || error}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPatientInitials = (first: string, last: string) =>
|
||||||
|
`${first[0]}${last[0]}`.toUpperCase();
|
||||||
|
|
||||||
|
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPdfId) return;
|
||||||
|
let url: string | null = null;
|
||||||
|
|
||||||
|
const fetchPdf = async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/documents/claim-pdf/${selectedPdfId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
setFileBlobUrl(objectUrl);
|
||||||
|
url = objectUrl;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load PDF", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPdf();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (url) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [selectedPdfId]);
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev);
|
||||||
|
|
||||||
|
const viewPdf = (pdfId: number) => {
|
||||||
|
setSelectedPdfId(pdfId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPdf = async (pdfId: number, filename: string) => {
|
||||||
|
try {
|
||||||
|
const res = await apiRequest("GET", `/api/documents/claim-pdf/${pdfId}`);
|
||||||
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to download PDF:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePdf = (pdf: ClaimPdf) => {
|
||||||
|
setCurrentPdf(pdf);
|
||||||
|
setIsDeletePdfOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDeletePdf = () => {
|
||||||
|
if (currentPdf) {
|
||||||
|
deletePdfMutation.mutate(currentPdf.id);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "No PDF selected for deletion.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredPdfs = pdfs.filter((pdf) => {
|
||||||
|
const patient = pdf.patient;
|
||||||
|
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.includes(searchLower) ||
|
||||||
|
patient.phone?.toLowerCase().includes(searchLower) ||
|
||||||
|
patient.email?.toLowerCase().includes(searchLower) ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredPdfs.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const currentPdfs = filteredPdfs.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-gray-50">
|
||||||
|
<Sidebar
|
||||||
|
isMobileOpen={isMobileMenuOpen}
|
||||||
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-auto p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-semibold text-gray-900 mb-2">
|
||||||
|
Documents
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
View and manage recent uploaded claim PDFs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center py-8">Loading data...</div>
|
||||||
|
) : currentPdfs.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
{searchTerm
|
||||||
|
? "No results matching your search."
|
||||||
|
: "No recent claim PDFs available."}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-12 gap-4 p-4 bg-gray-50 border-b text-sm font-medium text-gray-600">
|
||||||
|
<div className="col-span-3">Patient</div>
|
||||||
|
<div className="col-span-2">DOB / Gender</div>
|
||||||
|
<div className="col-span-2">Contact</div>
|
||||||
|
<div className="col-span-2">Insurance</div>
|
||||||
|
<div className="col-span-2">Status</div>
|
||||||
|
<div className="col-span-1">Actions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentPdfs.map((pdf) => {
|
||||||
|
const patient = pdf.patient;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={pdf.id}
|
||||||
|
className="grid grid-cols-12 gap-4 p-4 border-b hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="col-span-2">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="col-span-1">
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => handleDeletePdf(pdf)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4 text-red-600" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() =>
|
||||||
|
downloadPdf(pdf.id, pdf.filename)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 text-blue-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => viewPdf(pdf.id)}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
isOpen={isDeletePdfOpen}
|
||||||
|
onConfirm={handleConfirmDeletePdf}
|
||||||
|
onCancel={() => setIsDeletePdfOpen(false)}
|
||||||
|
entityName={`PDF #${currentPdf?.id}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* PDF Viewer */}
|
||||||
|
{selectedPdfId && fileBlobUrl && (
|
||||||
|
<div className="mt-6 border rounded-lg shadow-sm p-4 bg-white">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-700">
|
||||||
|
Viewing PDF #{selectedPdfId}
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPdfId(null);
|
||||||
|
setFileBlobUrl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="h-[80vh] border">
|
||||||
|
<Worker workerUrl="https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js">
|
||||||
|
<Viewer
|
||||||
|
fileUrl={fileBlobUrl}
|
||||||
|
plugins={[defaultLayoutPluginInstance]}
|
||||||
|
/>
|
||||||
|
</Worker>
|
||||||
|
</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(
|
||||||
|
startIndex + itemsPerPage,
|
||||||
|
filteredPdfs.length
|
||||||
|
)}{" "}
|
||||||
|
of {filteredPdfs.length} results
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
{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(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,164 +1,103 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
import {
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
Card,
|
||||||
import { Input } from "@/components/ui/input";
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Eye,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Settings,
|
|
||||||
Trash,
|
|
||||||
Download,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { ClaimPdfUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|
||||||
import { z } from "zod";
|
|
||||||
import "@react-pdf-viewer/core/lib/styles/index.css";
|
|
||||||
import "@react-pdf-viewer/default-layout/lib/styles/index.css";
|
|
||||||
import { Viewer, Worker } from "@react-pdf-viewer/core";
|
|
||||||
import { defaultLayoutPlugin } from "@react-pdf-viewer/default-layout";
|
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import { Eye, Trash, Download, FolderOpen } from "lucide-react";
|
||||||
|
import {
|
||||||
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
|
PdfFileUncheckedCreateInputObjectSchema,
|
||||||
|
} from "@repo/db/usedSchemas";
|
||||||
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
||||||
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
|
|
||||||
const ClaimPdfSchema =
|
const PatientSchema = (
|
||||||
ClaimPdfUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>;
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
type ClaimPdf = z.infer<typeof ClaimPdfSchema>;
|
).omit({
|
||||||
|
appointments: true,
|
||||||
|
});
|
||||||
|
type Patient = z.infer<typeof PatientSchema>;
|
||||||
|
|
||||||
|
const PdfFileSchema =
|
||||||
|
PdfFileUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>;
|
||||||
|
type PdfFile = z.infer<typeof PdfFileSchema>;
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
const { user } = useAuth();
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
const [searchField, setSearchField] = useState("all");
|
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const itemsPerPage = 5;
|
|
||||||
const [selectedPdfId, setSelectedPdfId] = useState<number | null>(null);
|
const [selectedPdfId, setSelectedPdfId] = useState<number | null>(null);
|
||||||
const defaultLayoutPluginInstance = defaultLayoutPlugin();
|
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
||||||
const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
||||||
const [currentPdf, setCurrentPdf] = useState<ClaimPdf | null>(null);
|
const [currentPdf, setCurrentPdf] = useState<PdfFile | null>(null);
|
||||||
|
|
||||||
const { data: pdfs = [], isLoading } = useQuery<ClaimPdf[]>({
|
const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev);
|
||||||
queryKey: ["/api/documents/claim-pdf/recent"],
|
|
||||||
enabled: !!user,
|
useEffect(() => {
|
||||||
|
setSelectedGroupId(null);
|
||||||
|
}, [selectedPatient]);
|
||||||
|
|
||||||
|
const handleSelectGroup = (groupId: number) => {
|
||||||
|
setSelectedGroupId((prev) => (prev === groupId ? null : groupId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: groups = [] } = useQuery({
|
||||||
|
queryKey: ["groups", selectedPatient?.id],
|
||||||
|
enabled: !!selectedPatient,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest("GET", "/api/documents/claim-pdf/recent");
|
const res = await apiRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/documents/pdf-groups/patient/${selectedPatient?.id}`
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: groupPdfs = [] } = useQuery({
|
||||||
|
queryKey: ["groupPdfs", selectedGroupId],
|
||||||
|
enabled: !!selectedGroupId,
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/documents/pdf-files/group/${selectedGroupId}`
|
||||||
|
);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const deletePdfMutation = useMutation({
|
const deletePdfMutation = useMutation({
|
||||||
mutationFn: async (id: number) => {
|
mutationFn: async (id: number) => {
|
||||||
await apiRequest("DELETE", `/api/documents/claim-pdf/${id}`);
|
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsDeletePdfOpen(false);
|
setIsDeletePdfOpen(false);
|
||||||
setCurrentPdf(null);
|
setCurrentPdf(null);
|
||||||
queryClient.invalidateQueries({
|
if (selectedGroupId != null) {
|
||||||
queryKey: ["/api/documents/claim-pdf/recent"],
|
queryClient.invalidateQueries({
|
||||||
});
|
queryKey: ["groupPdfs", selectedGroupId],
|
||||||
|
});
|
||||||
toast({
|
}
|
||||||
title: "Success",
|
toast({ title: "Success", description: "PDF deleted successfully!" });
|
||||||
description: "PDF deleted successfully!",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
console.error("Error deleting PDF:", error);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: `Failed to delete PDF: ${error.message || error}`,
|
description: error.message || "Failed to delete PDF",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPatientInitials = (first: string, last: string) =>
|
|
||||||
`${first[0]}${last[0]}`.toUpperCase();
|
|
||||||
|
|
||||||
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedPdfId) return;
|
|
||||||
let url: string | null = null;
|
|
||||||
|
|
||||||
const fetchPdf = async () => {
|
|
||||||
try {
|
|
||||||
const res = await apiRequest(
|
|
||||||
"GET",
|
|
||||||
`/api/documents/claim-pdf/${selectedPdfId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const arrayBuffer = await res.arrayBuffer();
|
|
||||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
|
||||||
const objectUrl = URL.createObjectURL(blob);
|
|
||||||
setFileBlobUrl(objectUrl);
|
|
||||||
url = objectUrl;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load PDF", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchPdf();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (url) {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [selectedPdfId]);
|
|
||||||
|
|
||||||
const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev);
|
|
||||||
|
|
||||||
const viewPdf = (pdfId: number) => {
|
|
||||||
setSelectedPdfId(pdfId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadPdf = async (pdfId: number, filename: string) => {
|
|
||||||
try {
|
|
||||||
const res = await apiRequest("GET", `/api/documents/claim-pdf/${pdfId}`);
|
|
||||||
const arrayBuffer = await res.arrayBuffer();
|
|
||||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to download PDF:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePdf = (pdf: ClaimPdf) => {
|
|
||||||
setCurrentPdf(pdf);
|
|
||||||
setIsDeletePdfOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmDeletePdf = () => {
|
const handleConfirmDeletePdf = () => {
|
||||||
if (currentPdf) {
|
if (currentPdf) {
|
||||||
deletePdfMutation.mutate(currentPdf.id);
|
deletePdfMutation.mutate(currentPdf.id);
|
||||||
@@ -171,37 +110,31 @@ export default function DocumentsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredPdfs = pdfs.filter((pdf) => {
|
const handleViewPdf = async (pdfId: number) => {
|
||||||
const patient = pdf.patient;
|
const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
|
||||||
const searchLower = searchTerm.toLowerCase();
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
const fullName = `${patient.firstName} ${patient.lastName}`.toLowerCase();
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
const patientId = `PID-${patient.id.toString().padStart(4, "0")}`;
|
const url = URL.createObjectURL(blob);
|
||||||
|
setFileBlobUrl(url);
|
||||||
|
setSelectedPdfId(pdfId);
|
||||||
|
};
|
||||||
|
|
||||||
switch (searchField) {
|
const handleDownloadPdf = async (pdfId: number, filename: string) => {
|
||||||
case "name":
|
const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
|
||||||
return fullName.includes(searchLower);
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
case "id":
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
return patientId.toLowerCase().includes(searchLower);
|
const url = URL.createObjectURL(blob);
|
||||||
case "phone":
|
const a = document.createElement("a");
|
||||||
return patient.phone?.toLowerCase().includes(searchLower) || false;
|
a.href = url;
|
||||||
case "all":
|
a.download = filename;
|
||||||
default:
|
document.body.appendChild(a);
|
||||||
return (
|
a.click();
|
||||||
fullName.includes(searchLower) ||
|
document.body.removeChild(a);
|
||||||
patientId.includes(searchLower) ||
|
URL.revokeObjectURL(url);
|
||||||
patient.phone?.toLowerCase().includes(searchLower) ||
|
};
|
||||||
patient.email?.toLowerCase().includes(searchLower) ||
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredPdfs.length / itemsPerPage);
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
||||||
const currentPdfs = filteredPdfs.slice(startIndex, startIndex + itemsPerPage);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50">
|
<div className="flex h-screen bg-gray-100">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
isMobileOpen={isMobileMenuOpen}
|
isMobileOpen={isMobileMenuOpen}
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
setIsMobileOpen={setIsMobileMenuOpen}
|
||||||
@@ -211,7 +144,7 @@ export default function DocumentsPage() {
|
|||||||
<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 space-y-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 mb-2">
|
<h1 className="text-2xl font-semibold text-gray-900 mb-2">
|
||||||
Documents
|
Documents
|
||||||
@@ -221,251 +154,142 @@ export default function DocumentsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="mb-6">
|
{selectedPatient && (
|
||||||
<CardContent className="p-4">
|
<Card>
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<CardHeader>
|
||||||
<div className="flex-1 relative">
|
<CardTitle>
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
Document Groups for {selectedPatient.firstName}{" "}
|
||||||
<Input
|
{selectedPatient.lastName}
|
||||||
placeholder="Search patients..."
|
</CardTitle>
|
||||||
value={searchTerm}
|
<CardDescription>Select a group to view PDFs</CardDescription>
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
</CardHeader>
|
||||||
className="pl-10"
|
<CardContent className="space-y-2">
|
||||||
/>
|
{groups.length === 0 ? (
|
||||||
</div>
|
<p className="text-muted-foreground">
|
||||||
<div className="flex gap-2">
|
No groups found for this patient.
|
||||||
<Select value={searchField} onValueChange={setSearchField}>
|
</p>
|
||||||
<SelectTrigger className="w-32">
|
) : (
|
||||||
<SelectValue />
|
groups.map((group: any) => (
|
||||||
</SelectTrigger>
|
<Button
|
||||||
<SelectContent>
|
key={group.id}
|
||||||
<SelectItem value="all">All Fields</SelectItem>
|
variant={
|
||||||
<SelectItem value="name">Name</SelectItem>
|
group.id === selectedGroupId ? "default" : "outline"
|
||||||
<SelectItem value="id">Patient ID</SelectItem>
|
}
|
||||||
<SelectItem value="phone">Phone</SelectItem>
|
onClick={() =>
|
||||||
</SelectContent>
|
setSelectedGroupId((prevId) =>
|
||||||
</Select>
|
prevId === group.id ? null : group.id
|
||||||
<Button variant="outline" size="sm">
|
)
|
||||||
<Settings className="h-4 w-4 mr-2" />
|
}
|
||||||
Advanced
|
>
|
||||||
</Button>
|
<FolderOpen className="w-4 h-4 mr-2" />
|
||||||
</div>
|
Group #{group.id} - {group.title}
|
||||||
</div>
|
</Button>
|
||||||
</CardContent>
|
))
|
||||||
</Card>
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card>
|
{selectedGroupId && (
|
||||||
<CardContent className="p-0">
|
<Card>
|
||||||
{isLoading ? (
|
<CardHeader>
|
||||||
<div className="text-center py-8">Loading data...</div>
|
<CardTitle>PDFs in Group #{selectedGroupId}</CardTitle>
|
||||||
) : currentPdfs.length === 0 ? (
|
</CardHeader>
|
||||||
<div className="text-center py-8 text-gray-500">
|
<CardContent className="space-y-2">
|
||||||
{searchTerm
|
{groupPdfs.length === 0 ? (
|
||||||
? "No results matching your search."
|
<p className="text-muted-foreground">
|
||||||
: "No recent claim PDFs available."}
|
No PDFs found in this group.
|
||||||
</div>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
groupPdfs.map((pdf: any) => (
|
||||||
<div className="grid grid-cols-12 gap-4 p-4 bg-gray-50 border-b text-sm font-medium text-gray-600">
|
<div
|
||||||
<div className="col-span-3">Patient</div>
|
key={pdf.id}
|
||||||
<div className="col-span-2">DOB / Gender</div>
|
className="flex justify-between items-center border p-2 rounded"
|
||||||
<div className="col-span-2">Contact</div>
|
>
|
||||||
<div className="col-span-2">Insurance</div>
|
<span className="text-sm">{pdf.filename}</span>
|
||||||
<div className="col-span-2">Status</div>
|
<div className="flex gap-2">
|
||||||
<div className="col-span-1">Actions</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentPdfs.map((pdf) => {
|
|
||||||
const patient = pdf.patient;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={pdf.id}
|
|
||||||
className="grid grid-cols-12 gap-4 p-4 border-b hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="col-span-1">
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => handleDeletePdf(pdf)}
|
|
||||||
>
|
|
||||||
<Trash className="h-4 w-4 text-red-600" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() =>
|
|
||||||
downloadPdf(pdf.id, pdf.filename)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 text-blue-600" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => viewPdf(pdf.id)}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 text-gray-600" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
isOpen={isDeletePdfOpen}
|
|
||||||
onConfirm={handleConfirmDeletePdf}
|
|
||||||
onCancel={() => setIsDeletePdfOpen(false)}
|
|
||||||
entityName={`PDF #${currentPdf?.id}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* PDF Viewer */}
|
|
||||||
{selectedPdfId && fileBlobUrl && (
|
|
||||||
<div className="mt-6 border rounded-lg shadow-sm p-4 bg-white">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700">
|
|
||||||
Viewing PDF #{selectedPdfId}
|
|
||||||
</h2>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewPdf(pdf.id)}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleDownloadPdf(pdf.id, pdf.filename)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 text-blue-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedPdfId(null);
|
setCurrentPdf(pdf);
|
||||||
setFileBlobUrl(null);
|
setIsDeletePdfOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close
|
<Trash className="w-4 h-4 text-red-600" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[80vh] border">
|
|
||||||
<Worker workerUrl="https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js">
|
|
||||||
<Viewer
|
|
||||||
fileUrl={fileBlobUrl}
|
|
||||||
plugins={[defaultLayoutPluginInstance]}
|
|
||||||
/>
|
|
||||||
</Worker>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
<Card>
|
||||||
{totalPages > 1 && (
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between p-4 border-t bg-gray-50">
|
<CardTitle>Patient Records</CardTitle>
|
||||||
<div className="text-sm text-gray-700">
|
<CardDescription>
|
||||||
Showing {startIndex + 1} to{" "}
|
Select a patient to view document groups
|
||||||
{Math.min(
|
</CardDescription>
|
||||||
startIndex + itemsPerPage,
|
</CardHeader>
|
||||||
filteredPdfs.length
|
<CardContent>
|
||||||
)}{" "}
|
<PatientTable
|
||||||
of {filteredPdfs.length} results
|
allowView
|
||||||
</div>
|
allowDelete
|
||||||
<div className="flex items-center space-x-2">
|
allowCheckbox
|
||||||
<Button
|
allowEdit
|
||||||
variant="outline"
|
onSelectPatient={setSelectedPatient}
|
||||||
size="sm"
|
/>
|
||||||
onClick={() => setCurrentPage(currentPage - 1)}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
{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(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
isOpen={isDeletePdfOpen}
|
||||||
|
onConfirm={handleConfirmDeletePdf}
|
||||||
|
onCancel={() => setIsDeletePdfOpen(false)}
|
||||||
|
entityName={`PDF #${currentPdf?.id}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{fileBlobUrl && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex justify-between items-center">
|
||||||
|
<CardTitle>Viewing PDF #{selectedPdfId}</CardTitle>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setFileBlobUrl(null);
|
||||||
|
setSelectedPdfId(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<iframe
|
||||||
|
src={fileBlobUrl}
|
||||||
|
className="w-full h-[80vh] border rounded"
|
||||||
|
title="PDF Viewer"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { CalendarIcon, CheckCircle } from "lucide-react";
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
CheckCircle,
|
||||||
|
LoaderCircleIcon,
|
||||||
|
} 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";
|
||||||
@@ -58,9 +62,7 @@ export default function InsuranceEligibilityPage() {
|
|||||||
const { status, message, show } = useAppSelector(
|
const { status, message, show } = useAppSelector(
|
||||||
(state) => state.seleniumEligibilityCheckTask
|
(state) => state.seleniumEligibilityCheckTask
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
@@ -71,6 +73,7 @@ export default function InsuranceEligibilityPage() {
|
|||||||
const [dateOfBirth, setDateOfBirth] = useState<Date | undefined>();
|
const [dateOfBirth, setDateOfBirth] = useState<Date | undefined>();
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [isCheckingEligibility, setIsCheckingEligibility] = useState(false);
|
||||||
|
|
||||||
// Populate fields from selected patient
|
// Populate fields from selected patient
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -125,48 +128,6 @@ export default function InsuranceEligibilityPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insurance eligibility check mutation --- not using right now
|
|
||||||
const checkInsuranceMutation = useMutation({
|
|
||||||
mutationFn: async ({
|
|
||||||
provider,
|
|
||||||
patientId,
|
|
||||||
credentials,
|
|
||||||
}: {
|
|
||||||
provider: string;
|
|
||||||
patientId: number;
|
|
||||||
credentials: { username: string; password: string };
|
|
||||||
}) => {
|
|
||||||
const response = await fetch("/api/insurance/check", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ provider, patientId, credentials }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to check insurance");
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast({
|
|
||||||
title: "Insurance Check Complete",
|
|
||||||
description: result.isEligible
|
|
||||||
? `Patient is eligible. Plan: ${result.planName}`
|
|
||||||
: "Patient eligibility could not be verified",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast({
|
|
||||||
title: "Insurance Check Failed",
|
|
||||||
description: error.message || "Unable to verify insurance eligibility",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// handle selenium
|
// handle selenium
|
||||||
const handleSelenium = async () => {
|
const handleSelenium = async () => {
|
||||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||||
@@ -194,7 +155,8 @@ export default function InsuranceEligibilityPage() {
|
|||||||
dispatch(
|
dispatch(
|
||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
status: "success",
|
status: "success",
|
||||||
message: "Submitted to Selenium.",
|
message:
|
||||||
|
"Patient status is updated, and Its eligibility pdf is uploaded at Document Page.",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -216,10 +178,12 @@ export default function InsuranceEligibilityPage() {
|
|||||||
description: error.message || "An error occurred.",
|
description: error.message || "An error occurred.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsCheckingEligibility(false); // End loading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddPatient = () => {
|
const handleAddPatient = async () => {
|
||||||
const newPatient: InsertPatient = {
|
const newPatient: InsertPatient = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
@@ -230,11 +194,11 @@ export default function InsuranceEligibilityPage() {
|
|||||||
status: "active",
|
status: "active",
|
||||||
insuranceId: memberId,
|
insuranceId: memberId,
|
||||||
};
|
};
|
||||||
addPatientMutation.mutate(newPatient);
|
await addPatientMutation.mutateAsync(newPatient);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle insurance provider button clicks
|
// Handle insurance provider button clicks
|
||||||
const handleMHButton = () => {
|
const handleMHButton = async () => {
|
||||||
// Form Fields check
|
// Form Fields check
|
||||||
if (!memberId || !dateOfBirth || !firstName) {
|
if (!memberId || !dateOfBirth || !firstName) {
|
||||||
toast({
|
toast({
|
||||||
@@ -246,10 +210,14 @@ export default function InsuranceEligibilityPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsCheckingEligibility(true);
|
||||||
|
|
||||||
// Adding patient if same patient exists then it will skip.
|
// Adding patient if same patient exists then it will skip.
|
||||||
handleAddPatient();
|
handleAddPatient();
|
||||||
|
|
||||||
handleSelenium();
|
handleSelenium();
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["patients"] });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -352,10 +320,19 @@ export default function InsuranceEligibilityPage() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => handleMHButton()}
|
onClick={() => handleMHButton()}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={checkInsuranceMutation.isPending}
|
disabled={isCheckingEligibility}
|
||||||
>
|
>
|
||||||
<CheckCircle className="h-4 w-4 mr-2" />
|
{isCheckingEligibility ? (
|
||||||
MH
|
<>
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
|
MH
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ async def start_workflow(request: Request):
|
|||||||
try:
|
try:
|
||||||
bot = AutomationMassHealthEligibilityCheck(data)
|
bot = AutomationMassHealthEligibilityCheck(data)
|
||||||
result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp")
|
result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp")
|
||||||
print(result)
|
|
||||||
|
|
||||||
if result.get("status") != "success":
|
if result.get("status") != "success":
|
||||||
return {"status": "error", "message": result.get("message")}
|
return {"status": "error", "message": result.get("message")}
|
||||||
|
|||||||
@@ -1,615 +0,0 @@
|
|||||||
%PDF-1.3
|
|
||||||
%<25>쏢
|
|
||||||
1 0 obj
|
|
||||||
<</Type /Catalog
|
|
||||||
/Pages 2 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
2 0 obj
|
|
||||||
<</Type /Pages
|
|
||||||
/Count 1
|
|
||||||
/Kids [11 0 R]
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
3 0 obj
|
|
||||||
<</Producer (Persits Software AspPDF - www.persits.com)
|
|
||||||
/Title (Member Eligibility)
|
|
||||||
/Creator (MassDHP)
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
4 0 obj
|
|
||||||
<</Type /Font
|
|
||||||
/Subtype /Type0
|
|
||||||
/BaseFont /MFLEVF+Arial
|
|
||||||
/Name /F1
|
|
||||||
/Encoding /Identity-H
|
|
||||||
/DescendantFonts [5 0 R]
|
|
||||||
/ToUnicode 9 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
5 0 obj
|
|
||||||
<</Type /Font
|
|
||||||
/Subtype /CIDFontType2
|
|
||||||
/BaseFont /MFLEVF+Arial
|
|
||||||
/CIDSystemInfo 6 0 R
|
|
||||||
/CIDToGIDMap /Identity
|
|
||||||
/FontDescriptor 7 0 R
|
|
||||||
/W [0 [750 0 277 277 277 354 556 556 889 666 190 333 333 389 583 277 333 277 277 556
|
|
||||||
556 556 556 556 556 556 556 556 556 277 277 583 583 583 556 1015 666 666 722 722
|
|
||||||
666 610 777 722 277 500 666 556 833 722 777 666 777 722 666 610 722 666 943 666
|
|
||||||
666 610 277 277 277 469 556 333 556 556 500 556 556 277 556 556 222 222 500 222
|
|
||||||
833 556 556 556 556 333 500 277 556 500 722 500 500 500 333 259 333 583 666 666
|
|
||||||
722 666 722 777 722 556 556 556 556 556 556 500 556 556 556 556 277 277 277 277
|
|
||||||
556 556 556 556 556 556 556 556 556 556 556 399 556 556 556 350 537 610 736 736
|
|
||||||
1000 333 333 548 1000 777 712 548 548 548 556 576 494 712 823 548 273 370 365 768
|
|
||||||
889 610 610 333 583 548 556 548 611 556 556 1000 666 666 777 1000 943 556 1000 333
|
|
||||||
333 222 222 548 494 500 666 166 556 333 333 500 500 556 277 222 333 1000 666 666
|
|
||||||
666 666 666 277 277 277 277 777 777 777 722 722 722 277 333 333 333 333 333 333
|
|
||||||
333 333 333 333 556 222 666 500 610 500 259 722 556 666 500 666 556 583 583 333
|
|
||||||
333 333 833 833 833 556 777 556 277 666 500 722 500 722 500 556 552 333 666 556
|
|
||||||
666 556 722 614 722 666 556 666 556 556 222 556 291 556 333 722 556 722 556 777
|
|
||||||
556 722 333 722 333 666 500 610 277 610 375 722 556 722 556 610 500 610 500 550
|
|
||||||
777 797 578 556 445 617 395 648 552 500 364 1093 1000 500 1000 500 1000 500 500 979
|
|
||||||
718 583 604 583 604 604 708 625 708 708 708 708 708 708 708 708 708 708 708 708
|
|
||||||
708 708 708 708 708 708 708 708 708 708 708 708 708 708 708 708 708 708 708 708
|
|
||||||
708 708 708 708 708 708 708 708 708 708 708 708 708 729 604 1000 989 989 989 989
|
|
||||||
604 604 604 1020 1052 916 750 750 531 656 593 510 500 750 734 443 604 187 354 885
|
|
||||||
323 604 354 354 604 354 666 556 722 500 722 500 666 556 666 556 666 556 777 556
|
|
||||||
777 556 777 556 722 556 722 556 277 277 277 277 277 277 277 222 500 222 666 500
|
|
||||||
500 556 222 722 556 723 556 777 556 777 556 722 333 666 500 610 277 722 556 722
|
|
||||||
556 722 556 722 556 943 722 666 500 222 666 556 1000 889 777 610 277 943 722 943
|
|
||||||
722 943 722 666 500 222 333 556 600 833 833 833 833 333 333 333 333 667 784 837
|
|
||||||
383 774 855 752 222 666 666 667 666 610 722 277 666 667 833 722 649 777 722 666
|
|
||||||
618 610 666 666 835 747 277 666 578 445 556 222 546 575 500 440 556 556 222 500
|
|
||||||
500 576 500 447 556 568 481 546 524 712 780 222 546 556 546 780 667 864 541 718
|
|
||||||
666 277 277 500 1057 1010 854 582 635 718 666 656 666 541 677 666 923 604 718 718
|
|
||||||
582 656 833 722 777 718 666 722 610 635 760 666 739 666 916 937 791 885 656 718
|
|
||||||
1010 722 556 572 531 364 583 556 668 458 558 558 437 583 687 552 556 541 556 500
|
|
||||||
458 500 822 500 572 520 802 822 625 718 520 510 750 541 556 556 364 510 500 222
|
|
||||||
277 222 906 812 556 437 500 552 488 411 1000 1072 689 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 382 0 274 0 0 277 562 541 398 508 602 246 382 598
|
|
||||||
589 246 509 460 462 598 601 246 352 574 529 566 546 461 478 549 509 694 642 493
|
|
||||||
493 493 235 416 815 246 509 509 462 462 535 694 694 694 694 562 562 562 541 398
|
|
||||||
508 602 286 411 589 286 509 460 462 601 352 574 566 546 478 549 509 694 642 246
|
|
||||||
541 460 546 575 0 0 0 0 318 318 356 412 207 0 0 0 0 0 0 0
|
|
||||||
0 525 525 525 525 525 525 525 525 525 525 525 318 525 750 750 282 750 525 525
|
|
||||||
525 750 750 750 750 750 0 750 750 750 750 750 750 750 750 638 750 750 750 713
|
|
||||||
713 244 244 750 750 750 750 562 525 529 529 488 488 812 932 394 514 812 932 394
|
|
||||||
514 638 588 375 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 0
|
|
||||||
0 0 0 0 750 750 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 556 1000 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750
|
|
||||||
750 750 750 750 750 750 750 750 750 750 750 750 318 318 750 616 412 207 229 207
|
|
||||||
229 432 432 207 229 638 588 244 244 207 229 713 713 244 244 282 375 713 713 244
|
|
||||||
244 713 713 244 244 562 525 529 529 562 525 529 529 562 525 529 529 337 337 337
|
|
||||||
337 488 488 488 488 821 821 530 530 821 821 530 530 1098 1098 846 846 1098 1098 846
|
|
||||||
846 581 581 581 581 581 581 581 581 543 450 525 394 543 450 525 394 788 788 267
|
|
||||||
262 581 581 267 262 601 601 394 394 506 506 207 207 337 337 394 394 525 525 244
|
|
||||||
244 282 375 450 394 432 432 638 588 638 588 244 244 543 600 543 600 543 600 543
|
|
||||||
600 750 750 0 0 750 750 750 0 0 750 750 0 0 750 750 750 0 0 0
|
|
||||||
0 0 0 750 0 0 750 750 750 750 750 750 750 750 750 750 750 750 750 750
|
|
||||||
750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750
|
|
||||||
750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750
|
|
||||||
318 318 318 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750 750
|
|
||||||
750 750 750 750 750 750 750 125 1000 2000 857 655 854 669 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 513 833 833 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 222 666 556 666 556 666 556 666 556 666 556 666 556
|
|
||||||
666 556 666 556 666 556 666 556 666 556 666 556 666 556 666 556 666 556 666 556
|
|
||||||
666 556 666 556 666 556 666 556 277 222 277 222 777 556 777 556 777 556 777 556
|
|
||||||
777 556 777 556 777 556 857 655 857 655 857 655 857 655 857 655 722 556 722 556
|
|
||||||
854 669 854 669 854 669 854 669 854 669 666 500 666 500 666 500 666 556 277 222
|
|
||||||
777 556 722 556 722 556 722 556 722 556 722 556 0 0 0 0 541 364 923 668
|
|
||||||
582 437 582 437 722 552 556 500 556 500 666 500 666 520 666 556 752 556 777 556
|
|
||||||
713 244 267 262 581 244 244 244 244 244 244 269 0 0 333 333 0 0 0 0
|
|
||||||
207 229 207 229 207 229 207 229 432 432 432 432 638 588 713 713 244 244 713 713
|
|
||||||
244 244 713 713 244 244 713 713 244 244 713 713 244 244 713 713 244 244 713 713
|
|
||||||
244 244 562 525 529 529 562 525 529 529 562 525 529 529 562 525 529 529 562 525
|
|
||||||
529 529 562 525 529 529 337 337 337 337 337 337 337 337 337 337 337 337 337 337
|
|
||||||
337 337 337 337 488 488 488 488 488 488 488 488 488 488 488 488 488 488 488 488
|
|
||||||
821 821 530 530 821 821 530 530 821 821 530 530 1098 1098 846 846 1098 1098 846 846
|
|
||||||
581 581 543 450 525 394 788 788 788 267 262 788 788 267 262 788 788 267 262 788
|
|
||||||
788 267 262 788 788 267 262 581 581 581 581 1155 1155 906 906 812 932 394 514 601
|
|
||||||
601 394 394 601 601 394 394 601 601 394 394 812 932 394 514 812 932 394 514 812
|
|
||||||
932 394 514 812 932 394 514 812 932 394 514 506 506 207 207 506 506 207 207 506
|
|
||||||
506 207 207 506 506 207 207 525 525 244 244 525 525 525 525 525 525 244 244 525
|
|
||||||
525 562 525 529 529 282 375 387 387 387 432 432 432 432 432 432 432 432 432 432
|
|
||||||
432 432 432 432 432 432 638 588 638 588 244 244 432 432 638 588 244 244 638 588
|
|
||||||
812 812 812 812 207 0 0 0 0 0 0 0 1123 1084 0 0 0 0 0 0
|
|
||||||
193 370 0 0 600 0 0 0 821 821 530 530 1098 1098 846 846 543 450 525 394
|
|
||||||
412 337 282 244 320 244 244 244 244 244 812 932 246 0 341 493 543 600 543 600
|
|
||||||
543 600 543 600 543 600 543 600 543 600 525 525 543 600 556 758 656 556 656 556
|
|
||||||
722 722 500 722 809 656 556 556 666 604 610 777 624 880 222 277 666 500 222 500
|
|
||||||
890 722 556 777 868 667 754 556 666 666 500 618 380 277 610 277 610 747 722 772
|
|
||||||
500 610 500 610 610 544 544 556 556 458 486 556 259 413 583 277 1333 1222 1048 1062
|
|
||||||
833 451 1222 944 770 556 666 556 0 666 556 1000 889 777 556 777 556 666 500 777
|
|
||||||
556 777 556 610 544 222 1333 1222 1048 777 556 1034 618 722 556 666 556 666 556 666
|
|
||||||
556 666 556 277 277 277 277 777 556 777 556 722 333 722 333 722 556 722 556 666
|
|
||||||
500 610 277 544 436 722 556 706 604 565 610 500 666 556 666 556 777 556 0 777
|
|
||||||
556 777 556 777 556 666 500 556 556 556 556 500 500 556 556 556 738 458 458 631
|
|
||||||
507 277 556 556 558 500 616 556 556 556 222 222 355 327 303 222 571 833 833 833
|
|
||||||
556 556 552 556 790 780 549 333 333 333 333 333 333 333 541 541 500 222 259 222
|
|
||||||
349 277 277 556 568 546 500 722 500 519 500 541 544 544 500 500 500 500 777 531
|
|
||||||
507 558 552 397 500 403 556 500 500 964 906 1005 712 429 718 763 661 632 485 527
|
|
||||||
383 383 159 239 239 239 364 480 320 190 354 222 222 222 333 333 348 348 583 583
|
|
||||||
583 583 333 333 333 333 333 333 333 277 277 333 333 333 333 333 333 333 333 322
|
|
||||||
157 339 328 348 382 382 382 382 382 333 333 333 333 333 542 542 542 542 542 542
|
|
||||||
542 542 542 382 542 542 542 542 542 382 542 542 542 542 542 382 542 542 542 542
|
|
||||||
542 382 542 542 542 542 542 382 542 542 542 542 542 542 542 542 542 382 542 542
|
|
||||||
542 542 542 382 542 542 542 542 542 382 542 542 542 542 542 382 542 542 542 542
|
|
||||||
542 382 542 542 542 542 542 542 542 542 542 382 542 542 542 542 542 382 542 542
|
|
||||||
542 542 542 382 542 542 542 542 542 382 542 542 542 542 542 382 542 542 542 542
|
|
||||||
542 542 542 542 542 382 542 542 542 542 542 382 542 542 542 542 542 382 542 542
|
|
||||||
542 542 542 382 542 542 542 542 542 382 542 542 542 542 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 333 333 333 575 546 772 958 772 560 780 601 777 556 722 500
|
|
||||||
610 403 624 529 756 576 890 833 674 556 673 500 666 666 609 596 736 553 463 409
|
|
||||||
601 572 500 222 777 441 441 666 718 556 558 1337 624 777 612 949 713 667 500 897
|
|
||||||
695 828 685 1053 867 604 458 796 688 777 556 803 630 803 630 1074 896 833 612 1190
|
|
||||||
851 0 1337 624 722 500 503 0 0 0 0 0 0 718 558 656 520 666 556 670
|
|
||||||
548 604 458 582 437 741 535 879 647 1136 870 752 520 722 500 610 458 925 690 666
|
|
||||||
520 861 666 861 666 277 923 668 667 550 656 583 722 552 722 552 666 520 833 687
|
|
||||||
333 666 556 666 556 1000 889 666 556 752 556 923 668 604 458 604 544 718 558 718
|
|
||||||
558 777 556 777 556 718 510 635 500 635 500 635 500 666 520 885 718 656 556 968
|
|
||||||
876 956 815 662 509 970 909 1034 878 777 558 746 665 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 666 556 666 556 666 556 666 556 722
|
|
||||||
500 722 556 722 556 722 556 722 556 722 556 666 556 666 556 666 556 666 556 666
|
|
||||||
556 610 277 777 556 722 556 722 556 722 556 722 556 722 556 277 222 277 277 666
|
|
||||||
500 666 500 666 500 556 222 556 222 556 222 556 222 833 833 833 833 833 833 722
|
|
||||||
556 722 556 722 556 722 556 777 556 777 556 777 556 777 556 666 556 666 556 722
|
|
||||||
333 722 333 722 333 722 333 666 500 666 500 666 500 666 500 666 500 610 277 610
|
|
||||||
277 610 277 610 277 722 556 722 556 722 556 722 556 722 556 666 500 666 500 943
|
|
||||||
722 943 722 666 500 666 500 666 500 610 500 610 500 610 500 556 277 722 500 556
|
|
||||||
222 578 578 578 578 578 578 578 578 666 666 813 813 813 813 813 813 445 445 445
|
|
||||||
445 445 445 764 764 927 927 927 927 556 556 556 556 556 556 556 556 819 819 1015
|
|
||||||
1015 1015 1015 1015 1015 222 222 222 222 222 222 222 222 375 375 570 570 570 570 570
|
|
||||||
570 556 556 556 556 556 556 826 826 1021 1021 973 973 546 546 546 546 546 546 546
|
|
||||||
546 813 959 1008 959 780 780 780 780 780 780 780 780 796 796 991 991 942 942 942
|
|
||||||
942 578 578 445 445 556 556 222 222 556 556 546 546 780 780 578 578 578 578 578
|
|
||||||
578 578 578 666 666 813 813 813 813 813 813 556 556 556 556 556 556 556 556 819
|
|
||||||
819 1015 1015 1015 1015 1015 1015 780 780 780 780 780 780 780 780 796 796 991 991 942
|
|
||||||
942 942 942 578 578 578 578 578 578 578 666 666 666 666 666 333 333 333 333 333
|
|
||||||
556 556 556 556 556 813 813 868 868 722 333 333 333 222 222 222 222 222 222 277
|
|
||||||
277 424 424 333 333 333 546 546 546 546 568 568 546 546 666 666 862 886 764 333
|
|
||||||
333 333 780 780 780 780 780 924 826 894 796 747 333 333 556 722 722 833 722 1164
|
|
||||||
943 666 610 1000 500 594 0 0 0 0 222 222 520 666 681 349 684 367 687 687
|
|
||||||
333 333 333 333 333 333 333 333 333 277 333 333 333 333 397 397 333 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 666 556 496 747 889 531 500 551 551 489 458 222
|
|
||||||
422 500 401 687 558 556 500 608 608 608 943 457 556 556 520 541 541 458 546 596
|
|
||||||
733 596 500 722 500 458 427 606 364 500 541 520 712 583 453 663 414 414 449 410
|
|
||||||
410 496 428 166 314 424 351 510 430 429 511 382 418 451 432 429 622 372 372 376
|
|
||||||
599 377 377 371 371 318 318 376 157 338 572 382 377 354 377 377 377 219 382 407
|
|
||||||
572 321 390 385 321 377 439 343 157 239 382 321 385 321 379 439 343 936 1299 438
|
|
||||||
1272 656 238 543 0 0 0 0 0 0 0 0 0 337 337 488 488 450 394 450
|
|
||||||
394 709 654 748 607 609 745 655 789 583 0 0 0 556 333 354 207 207 207 207
|
|
||||||
792 1221 500 1000 500 1000 333 250 166 556 277 200 83 0 736 722 833 687 908 886
|
|
||||||
886 666 722 500 556 610 500 500 580 0 0 0 0 0 568 722 722 722 541 364
|
|
||||||
0 0 0 352 0 262 289 0 0 0 0 0 0 0 713 713 244 244 713 713
|
|
||||||
244 244 713 713 244 244 713 713 244 244 713 713 244 244 713 713 244 244 713 713
|
|
||||||
244 244 562 525 529 529 562 525 529 529 337 337 337 337 488 488 821 821 530 530
|
|
||||||
543 450 525 394 543 450 525 394 543 450 525 394 788 788 267 262 788 788 267 262
|
|
||||||
812 932 394 514 812 932 394 514 812 932 394 514 337 337 394 394 337 337 394 394
|
|
||||||
525 525 244 244 525 525 244 244 525 525 244 244 506 506 207 207 488 488 488 488
|
|
||||||
821 821 530 530 556 556 277 833 556 556 333 333 500 277 500 556 380 556 785 222
|
|
||||||
222 556 546 568 556 556 277 712 500 222 833 556 556 333 500 386 500 500 500 556
|
|
||||||
556 556 556 458 458 650 222 500 222 556 544 376 354 348 373 318 229 229 376 383
|
|
||||||
157 157 157 157 270 157 157 274 571 571 382 382 381 377 375 339 157 219 382 387
|
|
||||||
377 353 321 358 358 358 369 364 0 0 0 0 277 372 371 377 328 371 777 666
|
|
||||||
556 722 333 578 578 578 578 578 578 578 578 222 222 222 222 222 222 222 222 546
|
|
||||||
546 546 546 546 546 546 546 222 222 222 222 546 546 546 546 543 600 453 666 722
|
|
||||||
667 666 556 500 222 737 556 722 333 666 500 500 500 500 222 541 364 666 500 666
|
|
||||||
500 604 458 656 583 0 0 0 0 0 0 0 0 0 942 489 500 556 222 556
|
|
||||||
666 722 556 277 722 556 666 500 610 500 500 577 425 648 0 0 0 0 0 0
|
|
||||||
222 723 722 723 0 0 0 0 0 0 0 0 0 0 0 777 556 943 722 702
|
|
||||||
0 732 596 1037 840 277 437 190 190 500 500 277 277 277 333 0 0 0 0 0
|
|
||||||
0 0 0 610 556 556 383 539 534 556 539 561 519 556 559 556 387 556 556 556
|
|
||||||
556 561 522 556 560 721 728 746 1161 746 375 656 777 555 222 496 254 556 289 558
|
|
||||||
556 556 375 254 222 555 566 595 612 554 503 647 617 239 431 566 466 722 615 648
|
|
||||||
553 648 606 553 507 607 550 793 554 552 506 820 833 466 648 554 612 595 555 555
|
|
||||||
555 555 555 555 595 554 554 554 554 239 239 239 239 615 648 648 648 648 648 607
|
|
||||||
607 607 607 552 555 555 555 595 595 595 595 612 612 554 554 554 554 554 647 647
|
|
||||||
647 647 617 618 239 239 239 239 239 657 431 566 466 466 466 466 615 615 615 619
|
|
||||||
648 648 648 606 606 606 553 553 553 553 553 507 507 507 506 607 607 607 607 607
|
|
||||||
607 793 793 793 793 552 552 552 506 506 506 555 820 648 555 566 459 555 554 506
|
|
||||||
617 648 239 566 543 722 615 522 648 612 553 518 507 552 659 554 657 648 555 554
|
|
||||||
617 239 648 552 648 239 552 554 710 459 597 553 239 239 431 869 838 731 510 548
|
|
||||||
612 555 565 566 459 551 554 791 515 611 611 510 551 722 617 648 612 553 595 507
|
|
||||||
548 631 554 607 561 769 764 685 737 541 596 835 606 392 333 333 333 333 333 333
|
|
||||||
333 333 333 333 333 333 333 721 721 721 721 721 721 721 721 721 721 721 728 728
|
|
||||||
728 728 728 728 728 728 728 728 728 746 746 746 746 746 746 746 746 746 746 746
|
|
||||||
746 746 375 375 375 375 375 375 375 375 375 510 375 375 375 254 254 301 330 254
|
|
||||||
375 375 375 375 375 375 375 375 656 555 555 555 555 555 555 555 555 555 222 496
|
|
||||||
254 254 301 330 254 289 289 375 289 558 558 558 558 578 333 333 333 333 616 615
|
|
||||||
615 755 604 735 268 268 268 268 268 268 268 268 268 268 268 268 268 268 268 1573
|
|
||||||
1755 0 1852 0 0 0 0 562 525 529 529 562 525 529 529 821 821 530 530 488
|
|
||||||
488 562 525 529 529 207 229 207 229 638 588 244 244 638 588 244 244 638 588 244
|
|
||||||
244 432 432 432 432 812 812 812 812 562 525 529 529 821 821 530 530 821 821 530
|
|
||||||
530 601 601 394 394 587 625 573 611 919 731 881 634 1464 0 0 0 0 0 638
|
|
||||||
588 244 244 812 932 394 514 812 932 394 514 638 588 244 244 638 588 244 244 638
|
|
||||||
588 244 244 0 577 475 610 458 718 583 666 556 1299 556 666 959 760 788 717 957
|
|
||||||
856 666 500 1068 884 1131 850 722 541 704 554 277 277 556 766 397 590 556 668 575
|
|
||||||
833 666 732 695 333 556 489 159 321 666 610 277 779 1416 1036 1380 1852 207 207 207
|
|
||||||
229 207 207 207 207 289 207 207 207 207 207 207 207 207 207 207 207 244 244 244
|
|
||||||
244 244 272 244 199 343 343 556 364 364 519 519 638 638 638 638 638 638 638 638
|
|
||||||
562 562 486 562 562 486 713 713 244 244 562 525 529 529 581 581 581 581 788 788
|
|
||||||
267 262 581 581 267 262 506 506 207 207 337 337 394 394 638 588 244 244 638 588
|
|
||||||
244 244 464 464 432 432 427 427 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 543 600 0 398 508
|
|
||||||
602 642 0 0 318 318 533 529 533 529 533 529 533 533 529 581 318 394 273 184
|
|
||||||
0 792 739 724 715 717 724 708 597 723 806 715 658 527 924 766 695 615 706 717
|
|
||||||
700 754 716 708 699 724 699 792 738 764 724 698 659 678 677 515 761 686 782 762
|
|
||||||
273 222 169 200 265 231 513 832 550 579 582 553 550 491 550 667 579 550 219 834
|
|
||||||
542 553 550 522 553 558 550 219 553 456 550 346 832 517 563 550 550 831 550 555
|
|
||||||
394 831 550 554 743 712 277 324 1000 1000 726 1104 1104 1101 1104 1385 556 1000 0 0
|
|
||||||
713 713 244 244 170 337 337 1098 1098 846 846 812 932 394 514 282 196 488 488 0
|
|
||||||
500 722 552 1329 1069 666 564 656 583 829 786 534 752 752 536 743 794 543 450 525
|
|
||||||
394 601 601 394 394 0 277 208 277 208 722 556 829 627 552 552 516 516 586 586
|
|
||||||
503 553 1155 912 1187 918 1020 890 962 734 962 734 962 734 722 500 666 500 666 500
|
|
||||||
666 500 650 310 556 222 802 610 877 651 1364 951 666 556 828 700 933 809 777 556
|
|
||||||
979 747 581 410 581 581 666 500 943 722 548 493 666 556 666 556 509 408 445 445
|
|
||||||
501 501 561 561 326 676 344 961 680 333 750 672 475 777 556 404 333 589 589 577
|
|
||||||
556 222 799 598 404 333 643 500 722 444 767 601 722 500 500 556 800 684 653 277
|
|
||||||
668 524 714 548 668 524 777 556 666 500 722 556 722 333 666 500 806 604 732 684
|
|
||||||
666 610 523 735 666 575 1002 780 769 448 639 833 610 666 833 277 1185 578 900 478
|
|
||||||
556 666 277 556 368 346 242 851 569 556 547 547 610 943 943 943 951 951 549 606
|
|
||||||
333 502 457 627 474 699 222 556 556 833 833 612 524 613 593 604 500 604 500 500
|
|
||||||
333 383 273 247 415 720 765 943 918 556 648 666 610 713 713 244 244 713 713 244
|
|
||||||
244 713 713 385 385 488 488 638 588 244 244 788 788 267 262 581 581 267 262 525
|
|
||||||
525 244 244 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
||||||
483 1056]]
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
6 0 obj
|
|
||||||
<</Registry (Adobe)
|
|
||||||
/Ordering (Identity)
|
|
||||||
/Supplement 0
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
7 0 obj
|
|
||||||
<</Type /FontDescriptor
|
|
||||||
/FontName /MFLEVF+Arial
|
|
||||||
/FontBBox [-664 -324 2000 1039]
|
|
||||||
/Ascent 905
|
|
||||||
/Descent -211
|
|
||||||
/CapHeight 0
|
|
||||||
/ItalicAngle 0
|
|
||||||
/StemV 0
|
|
||||||
/Flags 262176
|
|
||||||
/FontFile2 8 0 R
|
|
||||||
/CIDSet 10 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
8 0 obj
|
|
||||||
<</Filter /FlateDecode
|
|
||||||
/Length 31603
|
|
||||||
/Length1 74505
|
|
||||||
>>
|
|
||||||
stream
|
|
||||||
x<EFBFBD><EFBFBD><EFBFBD> `<14><>?<3F><><EFBFBD>\<5C>;3<><33><EFBFBD><EFBFBD><EFBFBD>n6لl <20><>#<10>"<22>"<22><>d%<25>} <01><><EFBFBD><EFBFBD>4<><34><EFBFBD>g<EFBFBD><67>}R+<2B>T[<5B>B<EFBFBD><42><EFBFBD><EFBFBD>j-<2D>Ѣ|-<2D><16><><EFBFBD>y<EFBFBD><79><EFBFBD>l<EFBFBD><6C><EFBFBD><EFBFBD><EFBFBD>nv晙wf<77>y<EFBFBD><79>s<EFBFBD><73>;A!dF<64><46>C<EFBFBD>ܵk"<22>^<5E><>.<2E><>B<>UV.\><3E>iO!ɂ<><C982>Y<EFBFBD><59><EFBFBD>_l<5F><6C>Y<EFBFBD><1C>Z<><5A>E<EFBFBD><45><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><11>n<06><19>v8<76><03>a<EFBFBD>ϰ]<5D>h<EFBFBD><68>u<EFBFBD>E<EFBFBD><45><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>V̝s<CC9D>$<24><><EFBFBD>L<EFBFBD>n]>g<>J<EFBFBD><4A><EFBFBD><EFBFBD>{<7B>z(<28>r<EFBFBD><72><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><0B>~!<21>V؏<56><D88F><08>"?G><3E>r<EFBFBD><72><EFBFBD>3<EFBFBD><33>.<2E>}F<><46>5<EFBFBD><02><><EFBFBD><EFBFBD>zmË<6D>6<EFBFBD>
|
|
||||||
z<15><><EFBFBD><EFBFBD><EFBFBD>}<7D><13>
|
|
||||||
y<EFBFBD>Hx<EFBFBD><EFBFBD><EFBFBD>Gh<12>L<EFBFBD>s;<3B>_<01><><08>s<EFBFBD><73>=<0E><>8:e/C7<43><37>ȃ}<7D><>э<EFBFBD>V<EFBFBD>m8<6D>V<EFBFBD><56>24MB+Нx\<5C>j4}<7D>ߌ<06>q<EFBFBD>*<2A><12><>f<EFBFBD><66><EFBFBD>ݛ{
|
|
||||||
=<3D><>q<EFBFBD><71>u#<05>\<5C><1E>})<29>1<EFBFBD>g<EFBFBD>θ=<3D>><3E><><EFBFBD>w<EFBFBD>$ܥJ<><04>Bsi<1E><16><>B
|
|
||||||
<EFBFBD><EFBFBD>P<1E>GG<47>!<21><><EFBFBD><EFBFBD>G<EFBFBD>b^ύ<><CF8D><<3C><><EFBFBD>C<>J<>E<EFBFBD>a<EFBFBD><0F><17><>0+7>wy<><1E><><EFBFBD><0F><>h|<7C><><EFBFBD><EFBFBD>}<7D><08>rO<72>N!?<3F>Ac<41>y:<3A>o<EFBFBD>!.۽!<21>-&@+<2B>A<EFBFBD>pd<05>z<1D>1<EFBFBD>s<EFBFBD>BP<42>:!)\<5C>{<07><> | |||||||