checkpoint date working
This commit is contained in:
@@ -3,6 +3,9 @@ import { Request, Response } from "express";
|
|||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
|
import multer from "multer";
|
||||||
|
import forwardToSeleniumAgent from "../services/seleniumClient";
|
||||||
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -37,6 +40,26 @@ const ExtendedClaimSchema = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
const multerStorage = multer.memoryStorage(); // NO DISK
|
||||||
|
const upload = multer({ storage: multerStorage });
|
||||||
|
|
||||||
|
router.post("/selenium", upload.array("pdfs"), async (req: Request, res: Response): Promise<any> => {
|
||||||
|
if (!req.files || !req.body.data) {
|
||||||
|
return res.status(400).json({ error: "Missing files or claim data for selenium" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const claimData = JSON.parse(req.body.data);
|
||||||
|
const files = req.files as Express.Multer.File[];
|
||||||
|
|
||||||
|
const result = await forwardToSeleniumAgent(claimData, files);
|
||||||
|
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Failed to forward to selenium agent" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get all claims for the logged-in user
|
// Get all claims for the logged-in user
|
||||||
router.get("/", async (req: Request, res: Response) => {
|
router.get("/", async (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -200,18 +200,10 @@ router.delete(
|
|||||||
.json({ message: "Forbidden: Patient belongs to a different user" });
|
.json({ message: "Forbidden: Patient belongs to a different user" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const appointments = await storage.getAppointmentsByPatientId(patientId);
|
|
||||||
console.log(appointments)
|
|
||||||
if (appointments.length > 0) {
|
|
||||||
throw new Error(`Cannot delete patient with ID ${patientId} because they have appointments`);
|
|
||||||
}
|
|
||||||
// Delete patient
|
// Delete patient
|
||||||
await storage.deletePatient(patientId);
|
await storage.deletePatient(patientId);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error:any) {
|
} catch (error:any) {
|
||||||
if (error.message.includes("have appointments")) {
|
|
||||||
return res.status(400).json({ message: error.message });
|
|
||||||
}
|
|
||||||
console.error("Delete patient error:", error);
|
console.error("Delete patient error:", error);
|
||||||
res.status(500).json({ message: "Failed to delete patient" });
|
res.status(500).json({ message: "Failed to delete patient" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Router } from "express";
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
const router = Router();
|
const router = Router();
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import forwardToPythonService from "../services/pythonClient";
|
import forwardToPdfService from "../services/PdfClient";
|
||||||
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ router.post("/extract", upload.single("pdf"), async (req: Request, res: Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await forwardToPythonService(req.file);
|
const result = await forwardToPdfService(req.file);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface ExtractedData {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function forwardToPythonService(
|
export default async function forwardToPdfService(
|
||||||
file: Express.Multer.File
|
file: Express.Multer.File
|
||||||
): Promise<ExtractedData> {
|
): Promise<ExtractedData> {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
25
apps/Backend/src/services/seleniumClient.ts
Normal file
25
apps/Backend/src/services/seleniumClient.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export interface SeleniumPayload {
|
||||||
|
claim: any;
|
||||||
|
pdfs: {
|
||||||
|
originalname: string;
|
||||||
|
bufferBase64: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function forwardToSeleniumAgent(
|
||||||
|
claimData: any,
|
||||||
|
files: Express.Multer.File[]
|
||||||
|
): Promise<any> {
|
||||||
|
const payload: SeleniumPayload = {
|
||||||
|
claim: claimData,
|
||||||
|
pdfs: files.map(file => ({
|
||||||
|
originalname: file.originalname,
|
||||||
|
bufferBase64: file.buffer.toString("base64"),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post("http://localhost:5002/run", payload);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
@@ -227,7 +227,8 @@ export const storage: IStorage = {
|
|||||||
try {
|
try {
|
||||||
await db.patient.delete({ where: { id } });
|
await db.patient.delete({ where: { id } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Patient with ID ${id} not found`);
|
console.error("Error deleting patient:", err);
|
||||||
|
throw new Error(`Failed to delete patient: ${err}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ interface ClaimFormData {
|
|||||||
serviceDate: string; // YYYY-MM-DD
|
serviceDate: string; // YYYY-MM-DD
|
||||||
insuranceProvider: string;
|
insuranceProvider: string;
|
||||||
status: string; // default "pending"
|
status: string; // default "pending"
|
||||||
|
massdhp_username?:string,
|
||||||
|
massdhp_password?:string,
|
||||||
serviceLines: ServiceLine[];
|
serviceLines: ServiceLine[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,7 @@ interface ClaimFormProps {
|
|||||||
appointmentData: InsertAppointment | UpdateAppointment
|
appointmentData: InsertAppointment | UpdateAppointment
|
||||||
) => void;
|
) => void;
|
||||||
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
|
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
|
||||||
|
onHandleForSelenium: (data: ClaimFormData) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +120,7 @@ export function ClaimForm({
|
|||||||
extractedData,
|
extractedData,
|
||||||
onHandleAppointmentSubmit,
|
onHandleAppointmentSubmit,
|
||||||
onHandleUpdatePatient,
|
onHandleUpdatePatient,
|
||||||
|
onHandleForSelenium,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
}: ClaimFormProps) {
|
}: ClaimFormProps) {
|
||||||
@@ -170,31 +174,23 @@ export function ClaimForm({
|
|||||||
// Service date state
|
// Service date state
|
||||||
const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
|
const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
|
||||||
const [serviceDate, setServiceDate] = useState<string>(
|
const [serviceDate, setServiceDate] = useState<string>(
|
||||||
format(new Date(), "MM/dd/yyyy")
|
new Date().toLocaleDateString("en-CA") // "YYYY-MM-DD"
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatServiceDate = (date: Date | undefined): string => {
|
|
||||||
return date ? format(date, "MM/dd/yyyy") : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (extractedData?.serviceDate) {
|
if (extractedData?.serviceDate) {
|
||||||
const parsed = new Date(extractedData.serviceDate);
|
const parsed = new Date(extractedData.serviceDate);
|
||||||
|
const isoFormatted = parsed.toLocaleDateString("en-CA");
|
||||||
setServiceDateValue(parsed);
|
setServiceDateValue(parsed);
|
||||||
setServiceDate(formatServiceDate(parsed));
|
setServiceDate(isoFormatted);
|
||||||
}
|
}
|
||||||
}, [extractedData]);
|
}, [extractedData]);
|
||||||
|
|
||||||
// used in submit button to send correct date.
|
|
||||||
function convertToISODate(mmddyyyy: string): string {
|
|
||||||
const [month, day, year] = mmddyyyy.split("/");
|
|
||||||
return `${year}-${month?.padStart(2, "0")}-${day?.padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update service date when calendar date changes
|
// Update service date when calendar date changes
|
||||||
const onServiceDateChange = (date: Date | undefined) => {
|
const onServiceDateChange = (date: Date | undefined) => {
|
||||||
if (date) {
|
if (date) {
|
||||||
const formattedDate = format(date, "MM/dd/yyyy");
|
const formattedDate = date.toLocaleDateString("en-CA"); // "YYYY-MM-DD"
|
||||||
setServiceDateValue(date);
|
setServiceDateValue(date);
|
||||||
setServiceDate(formattedDate);
|
setServiceDate(formattedDate);
|
||||||
setForm((prev) => ({ ...prev, serviceDate: formattedDate }));
|
setForm((prev) => ({ ...prev, serviceDate: formattedDate }));
|
||||||
@@ -215,7 +211,7 @@ export function ClaimForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// MAIN FORM INITIAL STATE
|
// MAIN FORM INITIAL STATE
|
||||||
const [form, setForm] = useState<ClaimFormData>({
|
const [form, setForm] = useState<ClaimFormData & { uploadedFiles: File[] }>({
|
||||||
patientId: patientId || 0,
|
patientId: patientId || 0,
|
||||||
appointmentId: 0, //need to update
|
appointmentId: 0, //need to update
|
||||||
userId: Number(user?.id),
|
userId: Number(user?.id),
|
||||||
@@ -227,6 +223,8 @@ export function ClaimForm({
|
|||||||
serviceDate: serviceDate,
|
serviceDate: serviceDate,
|
||||||
insuranceProvider: "",
|
insuranceProvider: "",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
|
massdhp_username: "",
|
||||||
|
massdhp_password: "",
|
||||||
serviceLines: [
|
serviceLines: [
|
||||||
{
|
{
|
||||||
procedureCode: "",
|
procedureCode: "",
|
||||||
@@ -237,6 +235,8 @@ export function ClaimForm({
|
|||||||
billedAmount: 0,
|
billedAmount: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
uploadedFiles: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync patient data to form when patient updates
|
// Sync patient data to form when patient updates
|
||||||
@@ -294,7 +294,7 @@ export function ClaimForm({
|
|||||||
const updateProcedureDate = (index: number, date: Date | undefined) => {
|
const updateProcedureDate = (index: number, date: Date | undefined) => {
|
||||||
if (!date) return;
|
if (!date) return;
|
||||||
|
|
||||||
const formattedDate = format(date, "MM/dd/yyyy");
|
const formattedDate = date.toLocaleDateString("en-CA");
|
||||||
const updatedLines = [...form.serviceLines];
|
const updatedLines = [...form.serviceLines];
|
||||||
|
|
||||||
if (updatedLines[index]) {
|
if (updatedLines[index]) {
|
||||||
@@ -305,24 +305,26 @@ export function ClaimForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// FILE UPLOAD ZONE
|
// FILE UPLOAD ZONE
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const handleFileUpload = (files: File[]) => {
|
const handleFileUpload = (files: File[]) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
const validFiles = files.filter((file) => file.type === "application/pdf");
|
const validFiles = files.filter((file) => file.type === "application/pdf");
|
||||||
if (validFiles.length > 10) {
|
if (validFiles.length > 5) {
|
||||||
toast({
|
toast({
|
||||||
title: "Too Many Files",
|
title: "Too Many Files",
|
||||||
description: "You can only upload up to 10 PDFs.",
|
description: "You can only upload up to 5 PDFs.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadedFiles(validFiles);
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
uploadedFiles: validFiles,
|
||||||
|
}));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Files Selected",
|
title: "Files Selected",
|
||||||
@@ -334,9 +336,15 @@ export function ClaimForm({
|
|||||||
|
|
||||||
// Delta MA Button Handler
|
// Delta MA Button Handler
|
||||||
const handleDeltaMASubmit = async () => {
|
const handleDeltaMASubmit = async () => {
|
||||||
|
|
||||||
|
function convertToISODate(mmddyyyy: string): string {
|
||||||
|
const [month, day, year] = mmddyyyy.split("/");
|
||||||
|
return `${year}-${month?.padStart(2, "0")}-${day?.padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
const appointmentData = {
|
const appointmentData = {
|
||||||
patientId: patientId,
|
patientId: patientId,
|
||||||
date: convertToISODate(serviceDate),
|
date: serviceDate,
|
||||||
staffId: staff?.id,
|
staffId: staff?.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -361,14 +369,25 @@ export function ClaimForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create Claim(if not)
|
// 3. Create Claim(if not)
|
||||||
|
const { uploadedFiles, massdhp_username, massdhp_password, ...formToCreateClaim } = form;
|
||||||
onSubmit({
|
onSubmit({
|
||||||
...form,
|
...formToCreateClaim,
|
||||||
staffId: Number(staff?.id),
|
staffId: Number(staff?.id),
|
||||||
patientId: patient?.id,
|
patientId: patientId,
|
||||||
insuranceProvider: "Delta MA",
|
insuranceProvider: "Delta MA",
|
||||||
appointmentId: appointmentId!,
|
appointmentId: appointmentId!,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. sending form data to selenium service
|
||||||
|
onHandleForSelenium({
|
||||||
|
...form,
|
||||||
|
staffId: Number(staff?.id),
|
||||||
|
patientId: patientId,
|
||||||
|
insuranceProvider: "Delta MA",
|
||||||
|
appointmentId: appointmentId!,
|
||||||
|
massdhp_username: "kqkgaox@yahoo.com",
|
||||||
|
massdhp_password: "Lex123456", //fetch this from db, by call
|
||||||
|
});
|
||||||
// 4. Close form
|
// 4. Close form
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -630,9 +649,9 @@ export function ClaimForm({
|
|||||||
{/* File Upload Section */}
|
{/* File Upload Section */}
|
||||||
<div className="mt-4 bg-gray-100 p-4 rounded-md space-y-4">
|
<div className="mt-4 bg-gray-100 p-4 rounded-md space-y-4">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Only PDF files allowed. You can upload up to 10 files. File
|
Only PDF files allowed. You can upload up to 5 files. File types
|
||||||
types with 4+ character extensions like .DOCX, .PPTX, or .XLSX
|
with 4+ character extensions like .DOCX, .PPTX, or .XLSX are not
|
||||||
are not allowed.
|
allowed.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center justify-between">
|
<div className="flex flex-wrap gap-4 items-center justify-between">
|
||||||
@@ -657,12 +676,12 @@ export function ClaimForm({
|
|||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
acceptedFileTypes="application/pdf"
|
acceptedFileTypes="application/pdf"
|
||||||
maxFiles={10}
|
maxFiles={5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{uploadedFiles.length > 0 && (
|
{form.uploadedFiles.length > 0 && (
|
||||||
<ul className="text-sm text-gray-700 list-disc ml-6">
|
<ul className="text-sm text-gray-700 list-disc ml-6">
|
||||||
{uploadedFiles.map((file, index) => (
|
{form.uploadedFiles.map((file, index) => (
|
||||||
<li key={index}>{file.name}</li>
|
<li key={index}>{file.name}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function MultipleFileUploadZone({
|
|||||||
onFileUpload,
|
onFileUpload,
|
||||||
isUploading,
|
isUploading,
|
||||||
acceptedFileTypes = "application/pdf",
|
acceptedFileTypes = "application/pdf",
|
||||||
maxFiles = 10,
|
maxFiles = 5,
|
||||||
}: FileUploadZoneProps) {
|
}: FileUploadZoneProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
PatientUncheckedCreateInputObjectSchema,
|
PatientUncheckedCreateInputObjectSchema,
|
||||||
AppointmentUncheckedCreateInputObjectSchema,
|
AppointmentUncheckedCreateInputObjectSchema,
|
||||||
} from "@repo/db/usedSchemas";
|
} from "@repo/db/usedSchemas";
|
||||||
import { FileCheck, CheckCircle, Clock, AlertCircle } from "lucide-react";
|
import { FileCheck } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { parse, format } from "date-fns";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
@@ -209,30 +209,33 @@ export default function ClaimsPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Converts local date to exact UTC date with no offset issues
|
||||||
|
function parseLocalDate(dateInput: Date | string): Date {
|
||||||
|
if (dateInput instanceof Date) return dateInput;
|
||||||
|
|
||||||
|
const dateString = dateInput.split("T")[0] || dateInput;
|
||||||
|
|
||||||
|
const parts = dateString.split("-");
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
throw new Error(`Invalid date format: ${dateString}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = Number(parts[0]);
|
||||||
|
const month = Number(parts[1]);
|
||||||
|
const day = Number(parts[2]);
|
||||||
|
|
||||||
|
if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) {
|
||||||
|
throw new Error(`Invalid date parts in date string: ${dateString}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(year, month - 1, day); // month is 0-indexed
|
||||||
|
}
|
||||||
|
|
||||||
const handleAppointmentSubmit = async (
|
const handleAppointmentSubmit = async (
|
||||||
appointmentData: InsertAppointment | UpdateAppointment
|
appointmentData: InsertAppointment | UpdateAppointment
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
// Converts local date to exact UTC date with no offset issues
|
|
||||||
function parseLocalDate(dateInput: Date | string): Date {
|
|
||||||
if (dateInput instanceof Date) return dateInput;
|
|
||||||
|
|
||||||
const parts = dateInput.split("-");
|
|
||||||
if (parts.length !== 3) {
|
|
||||||
throw new Error(`Invalid date format: ${dateInput}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = Number(parts[0]);
|
|
||||||
const month = Number(parts[1]);
|
|
||||||
const day = Number(parts[2]);
|
|
||||||
|
|
||||||
if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) {
|
|
||||||
throw new Error(`Invalid date parts in date string: ${dateInput}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(year, month - 1, day); // month is 0-indexed
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawDate = parseLocalDate(appointmentData.date);
|
const rawDate = parseLocalDate(appointmentData.date);
|
||||||
|
|
||||||
const formattedDate = rawDate.toLocaleDateString("en-CA"); // YYYY-MM-DD format
|
const formattedDate = rawDate.toLocaleDateString("en-CA"); // YYYY-MM-DD format
|
||||||
|
|
||||||
// Prepare minimal data to update/create
|
// Prepare minimal data to update/create
|
||||||
@@ -354,12 +357,14 @@ export default function ClaimsPage() {
|
|||||||
(a) => a.patientId === patient.id
|
(a) => a.patientId === patient.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dateToUse = lastAppointment
|
||||||
|
? parseLocalDate(lastAppointment.date)
|
||||||
|
: new Date();
|
||||||
|
|
||||||
setClaimFormData((prev: any) => ({
|
setClaimFormData((prev: any) => ({
|
||||||
...prev,
|
...prev,
|
||||||
patientId: patient.id,
|
patientId: patient.id,
|
||||||
serviceDate: lastAppointment
|
serviceDate: dateToUse.toLocaleDateString("en-CA"), // consistent "YYYY-MM-DD"
|
||||||
? new Date(lastAppointment.date).toISOString().slice(0, 10)
|
|
||||||
: new Date().toISOString().slice(0, 10),
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -378,13 +383,15 @@ export default function ClaimsPage() {
|
|||||||
const [firstName, ...rest] = name.trim().split(" ");
|
const [firstName, ...rest] = name.trim().split(" ");
|
||||||
const lastName = rest.join(" ") || "";
|
const lastName = rest.join(" ") || "";
|
||||||
|
|
||||||
const parsedDob = new Date(dob);
|
const parsedDob = parse(dob, "M/d/yyyy", new Date()); // robust for "4/17/1964", "12/1/1975", etc.
|
||||||
const isValidDob = !isNaN(parsedDob.getTime());
|
const isValidDob = !isNaN(parsedDob.getTime());
|
||||||
|
|
||||||
const newPatient: InsertPatient = {
|
const newPatient: InsertPatient = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
dateOfBirth: isValidDob ? parsedDob : new Date(),
|
dateOfBirth: isValidDob
|
||||||
|
? format(parsedDob, "yyyy-MM-dd")
|
||||||
|
: format(new Date(), "yyyy-MM-dd"),
|
||||||
gender: "",
|
gender: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
userId: user?.id ?? 1,
|
userId: user?.id ?? 1,
|
||||||
@@ -463,6 +470,42 @@ export default function ClaimsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// handle selenium
|
||||||
|
const handleSelenium = async (data: any) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("data", JSON.stringify(data));
|
||||||
|
|
||||||
|
const uploadedFiles: File[] = data.uploadedFiles ?? [];
|
||||||
|
|
||||||
|
uploadedFiles.forEach((file: File) => {
|
||||||
|
formData.append("pdfs", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/claims/selenium",
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Selenium service notified",
|
||||||
|
description: "Your claim data was successfully sent to Selenium.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Selenium service error",
|
||||||
|
description: error.message || "An error occurred.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
@@ -580,6 +623,7 @@ export default function ClaimsPage() {
|
|||||||
onSubmit={handleClaimSubmit}
|
onSubmit={handleClaimSubmit}
|
||||||
onHandleAppointmentSubmit={handleAppointmentSubmit}
|
onHandleAppointmentSubmit={handleAppointmentSubmit}
|
||||||
onHandleUpdatePatient={handleUpdatePatient}
|
onHandleUpdatePatient={handleUpdatePatient}
|
||||||
|
onHandleForSelenium={handleSelenium}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
3
apps/SeleniumService/.gitignore
vendored
3
apps/SeleniumService/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
|
/__pycache__
|
||||||
26
apps/SeleniumService/agent.py
Normal file
26
apps/SeleniumService/agent.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import uvicorn
|
||||||
|
from selenium_worker import AutomationMassDHP
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # Replace with your frontend domain for security
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/run")
|
||||||
|
async def run_bot(request: Request):
|
||||||
|
data = await request.json()
|
||||||
|
try:
|
||||||
|
bot = AutomationMassDHP(data)
|
||||||
|
result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=5002)
|
||||||
Binary file not shown.
@@ -1,6 +1,5 @@
|
|||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.common import TimeoutException
|
from selenium.common import TimeoutException
|
||||||
from selenium.common.exceptions import ElementNotInteractableException
|
|
||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.chrome.service import Service
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
@@ -13,6 +12,7 @@ from dotenv import load_dotenv
|
|||||||
import os
|
import os
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
# this file is just for testing selenium solely.
|
||||||
procedure_codes = [
|
procedure_codes = [
|
||||||
{
|
{
|
||||||
"procedure_code": "D0210",
|
"procedure_code": "D0210",
|
||||||
@@ -38,7 +38,7 @@ data = {
|
|||||||
"massdhp_username": os.getenv("MASSDHP_USERNAME"),
|
"massdhp_username": os.getenv("MASSDHP_USERNAME"),
|
||||||
"massdhp_password": os.getenv("MASSDHP_PASSWORD"),
|
"massdhp_password": os.getenv("MASSDHP_PASSWORD"),
|
||||||
"memberId": os.getenv("memberId"),
|
"memberId": os.getenv("memberId"),
|
||||||
"dob":os.getenv("dob"),
|
"dateOfBirth":os.getenv("dob"),
|
||||||
"procedure_codes": procedure_codes,
|
"procedure_codes": procedure_codes,
|
||||||
"pdfs" : pdfs,
|
"pdfs" : pdfs,
|
||||||
"missingTeethStatus": "Yes_missing", # can be Yes_missing , No_missing, or endentulous
|
"missingTeethStatus": "Yes_missing", # can be Yes_missing , No_missing, or endentulous
|
||||||
@@ -111,7 +111,7 @@ class AutomationMassDHP:
|
|||||||
|
|
||||||
# Fill DOB parts
|
# Fill DOB parts
|
||||||
try:
|
try:
|
||||||
dob_parts = data["dob"].split("/")
|
dob_parts = data["dateOfBirth"].split("/")
|
||||||
month= dob_parts[0].zfill(2) # "12"
|
month= dob_parts[0].zfill(2) # "12"
|
||||||
day= dob_parts[1].zfill(2) # "13"
|
day= dob_parts[1].zfill(2) # "13"
|
||||||
year = dob_parts[2] # "1965"
|
year = dob_parts[2] # "1965"
|
||||||
@@ -319,7 +319,6 @@ class AutomationMassDHP:
|
|||||||
|
|
||||||
|
|
||||||
input("should Close?") # here it sholud get confirmation from the frontend,
|
input("should Close?") # here it sholud get confirmation from the frontend,
|
||||||
|
|
||||||
|
|
||||||
self.driver.close()
|
self.driver.close()
|
||||||
|
|
||||||
302
apps/SeleniumService/selenium_worker.py
Normal file
302
apps/SeleniumService/selenium_worker.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common import TimeoutException
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
from selenium.webdriver.support.ui import Select
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import tempfile
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationMassDHP:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
self.headless = False
|
||||||
|
self.driver = None
|
||||||
|
|
||||||
|
def config_driver(self):
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
if self.headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
s = Service(ChromeDriverManager().install())
|
||||||
|
driver = webdriver.Chrome(service=s, options=options)
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Enter email
|
||||||
|
email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='Email' and @type='text']")))
|
||||||
|
email_field.clear()
|
||||||
|
email_field.send_keys(self.data["claim"]["massdhp_username"])
|
||||||
|
|
||||||
|
# Enter password
|
||||||
|
password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='Pass' and @type='password']")))
|
||||||
|
password_field.clear()
|
||||||
|
password_field.send_keys(self.data["claim"]["massdhp_password"])
|
||||||
|
|
||||||
|
# Click login
|
||||||
|
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Login']")))
|
||||||
|
login_button.click()
|
||||||
|
|
||||||
|
print("Login submitted successfully.")
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while logging in: {e}")
|
||||||
|
return "ERROR:LOGIN FAILED"
|
||||||
|
|
||||||
|
def step1(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
try:
|
||||||
|
claim_upload_link = wait.until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//a[text()='Claim Upload']"))
|
||||||
|
)
|
||||||
|
claim_upload_link.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Fill Member ID
|
||||||
|
member_id_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text1"]')))
|
||||||
|
member_id_input.clear()
|
||||||
|
member_id_input.send_keys(self.data["claim"]["memberId"])
|
||||||
|
|
||||||
|
# Fill DOB parts
|
||||||
|
try:
|
||||||
|
dob_parts = self.data["claim"]["dateOfBirth"].split("/")
|
||||||
|
month= dob_parts[0].zfill(2) # "12"
|
||||||
|
day= dob_parts[1].zfill(2) # "13"
|
||||||
|
year = dob_parts[2] # "1965"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing DOB: {e}")
|
||||||
|
return "ERROR: PARSING DOB"
|
||||||
|
|
||||||
|
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text2"]'))).send_keys(month)
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text3"]'))).send_keys(day)
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text4"]'))).send_keys(year)
|
||||||
|
|
||||||
|
# Rendering Provider NPI dropdown
|
||||||
|
npi_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Select1"]')))
|
||||||
|
select_npi = Select(npi_dropdown)
|
||||||
|
select_npi.select_by_index(1)
|
||||||
|
|
||||||
|
# Office Location dropdown
|
||||||
|
location_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Select2"]')))
|
||||||
|
select_location = Select(location_dropdown)
|
||||||
|
select_location.select_by_index(1)
|
||||||
|
|
||||||
|
# Click Continue button
|
||||||
|
continue_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@type="submit" and @value="Continue"]')))
|
||||||
|
continue_btn.click()
|
||||||
|
|
||||||
|
|
||||||
|
# Check for error message
|
||||||
|
try:
|
||||||
|
error_msg = WebDriverWait(self.driver, 5).until(EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//td[@class='text_err_msg' and contains(text(), 'Invalid Member ID or Date of Birth')]")
|
||||||
|
))
|
||||||
|
if error_msg:
|
||||||
|
print("Error: Invalid Member ID or Date of Birth.")
|
||||||
|
input("HERE?")
|
||||||
|
return "ERROR: INVALID MEMBERID OR DOB"
|
||||||
|
except TimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while step1 i.e Cheking the MemberId and DOB in: {e}")
|
||||||
|
return "ERROR:STEP1"
|
||||||
|
|
||||||
|
def step2(self):
|
||||||
|
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
# already waiting in step1 last part, so no time sleep.
|
||||||
|
|
||||||
|
# 1 - Procedure Codes part
|
||||||
|
try:
|
||||||
|
for proc in self.data["procedure_codes"]:
|
||||||
|
# Wait for Procedure Code dropdown and select code
|
||||||
|
|
||||||
|
select_element = wait.until(EC.presence_of_element_located((By.XPATH, "//select[@id='Select3']")))
|
||||||
|
Select(select_element).select_by_value(proc['procedure_code'])
|
||||||
|
|
||||||
|
# Fill Procedure Date if present
|
||||||
|
if proc.get("procedure_date"):
|
||||||
|
try:
|
||||||
|
# Try to normalize date to MM/DD/YYYY format
|
||||||
|
parsed_date = datetime.strptime(proc["procedure_date"], "%m/%d/%Y")
|
||||||
|
formatted_date = parsed_date.strftime("%m/%d/%Y")
|
||||||
|
|
||||||
|
date_xpath = "//input[@name='ProcedureDate']"
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, date_xpath))).clear()
|
||||||
|
self.driver.find_element(By.XPATH, date_xpath).send_keys(formatted_date)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# Invalid date format - skip filling ProcedureDate field
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fill Oral Cavity Area if present
|
||||||
|
if proc.get("oralCavityArea"):
|
||||||
|
oral_xpath = "//input[@name='OralCavityArea']"
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, oral_xpath))).clear()
|
||||||
|
self.driver.find_element(By.XPATH, oral_xpath).send_keys(proc["oralCavityArea"])
|
||||||
|
|
||||||
|
# Fill Tooth Number if present
|
||||||
|
if proc.get("toothNumber"):
|
||||||
|
tooth_num_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, "//select[@name='ToothNumber']")))
|
||||||
|
select_tooth = Select(tooth_num_dropdown)
|
||||||
|
select_tooth.select_by_value(proc["toothNumber"])
|
||||||
|
|
||||||
|
|
||||||
|
# Fill Tooth Surface if present
|
||||||
|
if proc.get("toothSurface"):
|
||||||
|
surface = proc["toothSurface"]
|
||||||
|
checkbox_xpath = f"//input[@type='checkbox' and @name='TS_{surface}']"
|
||||||
|
checkbox = wait.until(EC.element_to_be_clickable((By.XPATH, checkbox_xpath)))
|
||||||
|
if not checkbox.is_selected():
|
||||||
|
checkbox.click()
|
||||||
|
|
||||||
|
|
||||||
|
# Fill Fees if present
|
||||||
|
if proc.get("fees"):
|
||||||
|
fees_xpath = "//input[@name='ProcedureFee']"
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, fees_xpath))).clear()
|
||||||
|
self.driver.find_element(By.XPATH, fees_xpath).send_keys(proc["fees"])
|
||||||
|
|
||||||
|
# Click "Add Procedure" button
|
||||||
|
add_proc_xpath = "//input[@type='submit' and @value='Add Procedure']"
|
||||||
|
wait.until(EC.element_to_be_clickable((By.XPATH, add_proc_xpath))).click()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
print("Procedure codes submitted successfully.")
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while filling Procedure Codes: {e}")
|
||||||
|
return "ERROR:PROCEDURE CODES"
|
||||||
|
|
||||||
|
# 2 - Upload PDFs:
|
||||||
|
try:
|
||||||
|
|
||||||
|
pdfs_abs = [proc for proc in self.data.get("pdfs", [])]
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
for pdf_obj in pdfs_abs:
|
||||||
|
base64_data = pdf_obj["buffer"]
|
||||||
|
file_name = pdf_obj.get("originalname", "tempfile.pdf")
|
||||||
|
|
||||||
|
# Full path with original filename inside temp dir
|
||||||
|
tmp_file_path = os.path.join(tmp_dir, file_name)
|
||||||
|
|
||||||
|
# Decode and save
|
||||||
|
with open(tmp_file_path, "wb") as tmp_file:
|
||||||
|
tmp_file.write(base64.b64decode(base64_data))
|
||||||
|
|
||||||
|
# Upload using Selenium
|
||||||
|
file_input = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='FileName' and @type='file']")))
|
||||||
|
file_input.send_keys(tmp_file_path)
|
||||||
|
|
||||||
|
upload_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Upload File']")))
|
||||||
|
upload_button.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while uploading PDFs: {e}")
|
||||||
|
return "ERROR:PDF FAILED"
|
||||||
|
|
||||||
|
# 3 - Indicate Missing Teeth Part
|
||||||
|
try:
|
||||||
|
# Handle the missing teeth section based on the status
|
||||||
|
missing_status = self.data.get("missingTeethStatus")
|
||||||
|
|
||||||
|
if missing_status == "No_missing":
|
||||||
|
missing_teeth_no = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@type='checkbox' and @name='PAU_Step3_Checkbox1']")))
|
||||||
|
missing_teeth_no.click()
|
||||||
|
|
||||||
|
elif missing_status == "endentulous":
|
||||||
|
missing_teeth_edentulous = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@type='checkbox' and @name='PAU_Step3_Checkbox2']")))
|
||||||
|
missing_teeth_edentulous.click()
|
||||||
|
|
||||||
|
elif missing_status == "Yes_missing":
|
||||||
|
missing_teeth_dict = self.data.get("missingTeeth", {})
|
||||||
|
|
||||||
|
# For each tooth in the missing teeth dict, select the dropdown option
|
||||||
|
for tooth_name, value in missing_teeth_dict.items():
|
||||||
|
if value: # only if there's a value to select
|
||||||
|
select_element = wait.until(EC.presence_of_element_located((By.XPATH, f"//select[@name='{tooth_name}']")))
|
||||||
|
select_obj = Select(select_element)
|
||||||
|
select_obj.select_by_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for upload button and click it
|
||||||
|
update_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Update Missing Teeth']")))
|
||||||
|
update_button.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while filling missing teeth: {e}")
|
||||||
|
return "ERROR:MISSING TEETH FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
# 4 - Update Remarks
|
||||||
|
try:
|
||||||
|
textarea = wait.until(EC.presence_of_element_located((By.XPATH, "//textarea[@name='Remarks']")))
|
||||||
|
textarea.clear()
|
||||||
|
textarea.send_keys(self.data["remarks"])
|
||||||
|
|
||||||
|
# Wait for update button and click it
|
||||||
|
update_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Update Remarks']")))
|
||||||
|
update_button.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while filling remarks: {e}")
|
||||||
|
return "ERROR:REMARKS FAILED"
|
||||||
|
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
def main_workflow(self, url):
|
||||||
|
self.config_driver()
|
||||||
|
print("Reaching Site :", url)
|
||||||
|
self.driver.maximize_window()
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
value = self.login()
|
||||||
|
if value.startswith("ERROR"):
|
||||||
|
self.driver.close()
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
value2 = self.step1()
|
||||||
|
if value2.startswith("ERROR"):
|
||||||
|
self.driver.close()
|
||||||
|
return value2
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
value3 = self.step2()
|
||||||
|
if value3.startswith("ERROR"):
|
||||||
|
self.driver.close()
|
||||||
|
return value3
|
||||||
|
|
||||||
|
|
||||||
|
input("should Close?") # here it sholud get confirmation from the frontend,
|
||||||
|
|
||||||
|
self.driver.close()
|
||||||
|
|
||||||
|
return {"status": "success", "message": "Successfully submitted the form."}
|
||||||
@@ -67,10 +67,11 @@ model Appointment {
|
|||||||
status String @default("scheduled") // "scheduled", "completed", "cancelled", "no-show"
|
status String @default("scheduled") // "scheduled", "completed", "cancelled", "no-show"
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
patient Patient @relation(fields: [patientId], references: [id])
|
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
staff Staff? @relation(fields: [staffId], references: [id])
|
staff Staff? @relation(fields: [staffId], references: [id])
|
||||||
claims Claim[]
|
claims Claim[]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Staff {
|
model Staff {
|
||||||
@@ -100,10 +101,11 @@ model Claim {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
status String @default("pending") // "pending", "completed", "cancelled", "no-show"
|
status String @default("pending") // "pending", "completed", "cancelled", "no-show"
|
||||||
|
|
||||||
patient Patient @relation(fields: [patientId], references: [id])
|
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||||
appointment Appointment @relation(fields: [appointmentId], references: [id])
|
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
|
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
|
||||||
|
|
||||||
serviceLines ServiceLine[]
|
serviceLines ServiceLine[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,5 +118,5 @@ model ServiceLine {
|
|||||||
toothNumber String?
|
toothNumber String?
|
||||||
toothSurface String?
|
toothSurface String?
|
||||||
billedAmount Float
|
billedAmount Float
|
||||||
claim Claim @relation(fields: [claimId], references: [id])
|
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user