Compare commits
12
Commits
26da3394aa
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34e9c83a63 | ||
|
|
a3d2960c98 | ||
|
|
00fd213291 | ||
|
|
191ed129d8 | ||
|
|
305a07874d | ||
|
|
6e779fc630 | ||
|
|
ce2e03e505 | ||
|
|
75ecd7cc20 | ||
|
|
abb2a0ac96 | ||
|
|
ac4d8c4c74 | ||
|
|
998ef0ac95 | ||
|
|
15a1091f35 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"is_task_list_visible": true,
|
||||
"active_task": "backend#dev"
|
||||
"active_task": "frontend#dev"
|
||||
}
|
||||
@@ -44,6 +44,8 @@ const ALIAS_MAP: Record<string, string> = {
|
||||
// Exams
|
||||
"perio exam": "periodic oral evaluation",
|
||||
"periodic exam": "periodic oral evaluation",
|
||||
"period exam": "periodic oral evaluation",
|
||||
"poe": "periodic oral evaluation",
|
||||
"recall exam": "periodic oral evaluation",
|
||||
"comp exam": "comprehensive oral evaluation",
|
||||
"comprehensive exam": "comprehensive oral evaluation",
|
||||
@@ -150,6 +152,8 @@ const ALIAS_MAP: Record<string, string> = {
|
||||
// or short abbreviations that need precise mapping (checked before keyword search)
|
||||
const DIRECT_CODE_MAP: Record<string, string> = {
|
||||
// Exams
|
||||
"poe": "D0120",
|
||||
"period exam": "D0120",
|
||||
"limited exam": "D0140",
|
||||
"emergency exam": "D0140",
|
||||
// X-rays
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
import { InProcessQueue } from "./inProcessQueue";
|
||||
import { io } from "../socket";
|
||||
import { runEligibilityProcessor } from "./processors/eligibilityProcessor";
|
||||
import { runClaimStatusProcessor } from "./processors/claimStatusProcessor";
|
||||
import { runClaimSubmitProcessor } from "./processors/claimSubmitProcessor";
|
||||
import { runOcrProcessor } from "./processors/ocrProcessor";
|
||||
@@ -23,8 +22,6 @@ import { runUnitedDHClaimProcessor } from "./processors/unitedDHClaimProcessor";
|
||||
import { runUnitedDHPreAuthProcessor } from "./processors/unitedDHPreAuthProcessor";
|
||||
import { runTuftsSCOPreAuthProcessor } from "./processors/tuftsSCOPreAuthProcessor";
|
||||
import { runTuftsSCOClaimProcessor } from "./processors/tuftsSCOClaimProcessor";
|
||||
import { runEligibilityHistoryProcessor } from "./processors/eligibilityHistoryProcessor";
|
||||
import { runCmspEligibilityHistoryRemainingProcessor } from "./processors/cmspEligibilityHistoryRemainingProcessor";
|
||||
import { runBcbsMaEligibilityProcessor } from "./processors/bcbsMaEligibilityProcessor";
|
||||
import type { SeleniumJobData, OcrJobData } from "./queues";
|
||||
|
||||
@@ -59,16 +56,6 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
||||
message: "Selenium browser starting…",
|
||||
});
|
||||
|
||||
if (jobType === "eligibility-check") {
|
||||
return runEligibilityProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
userId: data.userId,
|
||||
insuranceId: data.insuranceId!,
|
||||
formFirstName: data.formFirstName,
|
||||
formLastName: data.formLastName,
|
||||
formDob: data.formDob,
|
||||
});
|
||||
}
|
||||
if (jobType === "claim-status-check") {
|
||||
return runClaimStatusProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
@@ -231,26 +218,6 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
||||
job.id
|
||||
);
|
||||
}
|
||||
if (jobType === "cmsp-eligibility-history-remaining-check") {
|
||||
return runCmspEligibilityHistoryRemainingProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
userId: data.userId,
|
||||
insuranceId: data.insuranceId!,
|
||||
formFirstName: data.formFirstName,
|
||||
formLastName: data.formLastName,
|
||||
formDob: data.formDob,
|
||||
});
|
||||
}
|
||||
if (jobType === "mh-eligibility-history-check") {
|
||||
return runEligibilityHistoryProcessor({
|
||||
enrichedPayload: data.enrichedPayload,
|
||||
userId: data.userId,
|
||||
insuranceId: data.insuranceId!,
|
||||
formFirstName: data.formFirstName,
|
||||
formLastName: data.formLastName,
|
||||
formDob: data.formDob,
|
||||
});
|
||||
}
|
||||
if (jobType === "bcbs-ma-eligibility-check") {
|
||||
return runBcbsMaEligibilityProcessor(
|
||||
{
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/**
|
||||
* Processor for "cmsp-eligibility-history-remaining-check" jobs.
|
||||
* Saves eligibility PDF, service history PDF, and accumulator PDF
|
||||
* to the patient's ELIGIBILITY_STATUS document group.
|
||||
*/
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { storage } from "../../storage";
|
||||
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
|
||||
import forwardToPatientDataExtractorService from "../../services/patientDataExtractorService";
|
||||
import {
|
||||
callPythonSync,
|
||||
splitName,
|
||||
createOrUpdatePatientByInsuranceId,
|
||||
} from "./_shared";
|
||||
|
||||
export interface CmspEligibilityHistoryRemainingProcessorInput {
|
||||
enrichedPayload: any;
|
||||
userId: number;
|
||||
insuranceId: string;
|
||||
formFirstName?: string;
|
||||
formLastName?: string;
|
||||
formDob?: string;
|
||||
}
|
||||
|
||||
export interface CmspEligibilityHistoryRemainingProcessorResult {
|
||||
patientUpdateStatus?: string;
|
||||
pdfUploadStatus?: string;
|
||||
pdfFileId?: number | null;
|
||||
pdfFilename?: string | null;
|
||||
memberDetailsPdfFileId?: number | null;
|
||||
memberDetailsPdfFilename?: string | null;
|
||||
historyPdfFileId?: number | null;
|
||||
historyPdfFilename?: string | null;
|
||||
accumulatorPdfFileId?: number | null;
|
||||
accumulatorPdfFilename?: string | null;
|
||||
}
|
||||
|
||||
export async function runCmspEligibilityHistoryRemainingProcessor(
|
||||
input: CmspEligibilityHistoryRemainingProcessorInput
|
||||
): Promise<CmspEligibilityHistoryRemainingProcessorResult> {
|
||||
const { enrichedPayload, userId, insuranceId, formFirstName, formLastName, formDob } = input;
|
||||
|
||||
const seleniumResult = await callPythonSync("/cmsp-eligibility-history-remaining-check", {
|
||||
data: enrichedPayload,
|
||||
});
|
||||
|
||||
const outputResult: CmspEligibilityHistoryRemainingProcessorResult = {};
|
||||
|
||||
// Extract patient name
|
||||
const seleniumFirst = seleniumResult.firstName?.trim() || null;
|
||||
const seleniumLast = seleniumResult.lastName?.trim() || null;
|
||||
const seleniumName = seleniumResult.name?.trim() || null;
|
||||
const seleniumInsurance = seleniumResult.insurance?.trim() || null;
|
||||
|
||||
const extracted: { firstName?: string | null; lastName?: string | null } = {};
|
||||
|
||||
if (seleniumFirst || seleniumLast) {
|
||||
extracted.firstName = seleniumFirst;
|
||||
extracted.lastName = seleniumLast;
|
||||
} else if (seleniumName) {
|
||||
const parts = splitName(seleniumName);
|
||||
extracted.firstName = parts.firstName || null;
|
||||
extracted.lastName = parts.lastName || null;
|
||||
}
|
||||
|
||||
if (!extracted.firstName && !extracted.lastName && seleniumResult?.pdf_path?.endsWith(".pdf")) {
|
||||
try {
|
||||
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
||||
const extraction = await forwardToPatientDataExtractorService({
|
||||
buffer: pdfBuffer,
|
||||
originalname: path.basename(seleniumResult.pdf_path),
|
||||
mimetype: "application/pdf",
|
||||
} as any);
|
||||
if (extraction.name) {
|
||||
const parts = splitName(extraction.name);
|
||||
extracted.firstName = parts.firstName || null;
|
||||
extracted.lastName = parts.lastName || null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[cmspProcessor] PDF name extraction failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const preferFirst = extracted.firstName || formFirstName || null;
|
||||
const preferLast = extracted.lastName || formLastName || null;
|
||||
|
||||
let patient;
|
||||
try {
|
||||
patient = await createOrUpdatePatientByInsuranceId({
|
||||
insuranceId,
|
||||
firstName: preferFirst,
|
||||
lastName: preferLast,
|
||||
dob: formDob,
|
||||
userId,
|
||||
});
|
||||
} catch (e: any) {
|
||||
throw new Error(`Failed to create/update patient: ${e.message}`);
|
||||
}
|
||||
|
||||
if (patient && patient.id !== undefined) {
|
||||
let newStatus = "UNKNOWN";
|
||||
if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE";
|
||||
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE";
|
||||
|
||||
const updates: any = { status: newStatus };
|
||||
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
|
||||
await storage.updatePatient(patient.id, updates);
|
||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||
|
||||
// Helper: save a single PDF to the patient's ELIGIBILITY_STATUS group
|
||||
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
|
||||
try {
|
||||
const buffer = await fs.readFile(pdfPath);
|
||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||
const groupTitle = "Eligibility Status";
|
||||
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
|
||||
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
|
||||
if (!group?.id) throw new Error("PDF group creation failed");
|
||||
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
|
||||
if (created && typeof created === "object" && "id" in created) {
|
||||
return Number(created.id);
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
console.error("[cmspProcessor] saveToGroup failed:", e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Save eligibility PDF
|
||||
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
||||
outputResult.pdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
||||
outputResult.pdfFilename = path.basename(seleniumResult.pdf_path);
|
||||
outputResult.pdfUploadStatus = outputResult.pdfFileId ? "Eligibility PDF saved" : "Eligibility PDF upload failed";
|
||||
} else {
|
||||
outputResult.pdfUploadStatus = "No eligibility PDF returned by Selenium";
|
||||
}
|
||||
|
||||
// Save member details PDF
|
||||
if (seleniumResult.member_details_pdf_path?.endsWith(".pdf")) {
|
||||
outputResult.memberDetailsPdfFileId = await saveToGroup(seleniumResult.member_details_pdf_path);
|
||||
outputResult.memberDetailsPdfFilename = path.basename(seleniumResult.member_details_pdf_path);
|
||||
}
|
||||
|
||||
// Save history PDF
|
||||
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
||||
outputResult.historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
||||
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
||||
}
|
||||
|
||||
// Save accumulator PDF
|
||||
if (seleniumResult.accumulator_pdf_path?.endsWith(".pdf")) {
|
||||
outputResult.accumulatorPdfFileId = await saveToGroup(seleniumResult.accumulator_pdf_path);
|
||||
outputResult.accumulatorPdfFilename = path.basename(seleniumResult.accumulator_pdf_path);
|
||||
}
|
||||
} else {
|
||||
outputResult.patientUpdateStatus = "Patient not found or missing ID; no update performed";
|
||||
}
|
||||
|
||||
// Cleanup temp files
|
||||
try {
|
||||
if (seleniumResult.pdf_path) await emptyFolderContainingFile(seleniumResult.pdf_path);
|
||||
} catch (e) {
|
||||
console.error("[cmspProcessor] cleanup failed:", e);
|
||||
}
|
||||
|
||||
return outputResult;
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* Processor for "mh-eligibility-history-check" jobs.
|
||||
* Runs MH eligibility + service history selenium flow and saves both PDFs
|
||||
* to the patient's ELIGIBILITY_STATUS document group.
|
||||
*/
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { storage } from "../../storage";
|
||||
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
|
||||
import forwardToPatientDataExtractorService from "../../services/patientDataExtractorService";
|
||||
import {
|
||||
callPythonSync,
|
||||
splitName,
|
||||
createOrUpdatePatientByInsuranceId,
|
||||
} from "./_shared";
|
||||
|
||||
export interface EligibilityHistoryProcessorInput {
|
||||
enrichedPayload: any;
|
||||
userId: number;
|
||||
insuranceId: string;
|
||||
formFirstName?: string;
|
||||
formLastName?: string;
|
||||
formDob?: string;
|
||||
}
|
||||
|
||||
export interface EligibilityHistoryProcessorResult {
|
||||
patientUpdateStatus?: string;
|
||||
pdfUploadStatus?: string;
|
||||
pdfFileId?: number | null;
|
||||
pdfFilename?: string | null;
|
||||
memberDetailsPdfFileId?: number | null;
|
||||
memberDetailsPdfFilename?: string | null;
|
||||
historyPdfFileId?: number | null;
|
||||
historyPdfFilename?: string | null;
|
||||
}
|
||||
|
||||
export async function runEligibilityHistoryProcessor(
|
||||
input: EligibilityHistoryProcessorInput
|
||||
): Promise<EligibilityHistoryProcessorResult> {
|
||||
const { enrichedPayload, userId, insuranceId, formFirstName, formLastName, formDob } = input;
|
||||
|
||||
const seleniumResult = await callPythonSync("/mh-eligibility-history-check", {
|
||||
data: enrichedPayload,
|
||||
});
|
||||
|
||||
const outputResult: EligibilityHistoryProcessorResult = {};
|
||||
|
||||
// Extract patient name
|
||||
const seleniumFirst = seleniumResult.firstName?.trim() || null;
|
||||
const seleniumLast = seleniumResult.lastName?.trim() || null;
|
||||
const seleniumName = seleniumResult.name?.trim() || null;
|
||||
const seleniumInsurance = seleniumResult.insurance?.trim() || null;
|
||||
|
||||
const extracted: { firstName?: string | null; lastName?: string | null } = {};
|
||||
|
||||
if (seleniumFirst || seleniumLast) {
|
||||
extracted.firstName = seleniumFirst;
|
||||
extracted.lastName = seleniumLast;
|
||||
} else if (seleniumName) {
|
||||
const parts = splitName(seleniumName);
|
||||
extracted.firstName = parts.firstName || null;
|
||||
extracted.lastName = parts.lastName || null;
|
||||
}
|
||||
|
||||
if (!extracted.firstName && !extracted.lastName && seleniumResult?.pdf_path?.endsWith(".pdf")) {
|
||||
try {
|
||||
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
||||
const extraction = await forwardToPatientDataExtractorService({
|
||||
buffer: pdfBuffer,
|
||||
originalname: path.basename(seleniumResult.pdf_path),
|
||||
mimetype: "application/pdf",
|
||||
} as any);
|
||||
if (extraction.name) {
|
||||
const parts = splitName(extraction.name);
|
||||
extracted.firstName = parts.firstName || null;
|
||||
extracted.lastName = parts.lastName || null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[eligibilityHistoryProcessor] PDF name extraction failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const preferFirst = extracted.firstName || formFirstName || null;
|
||||
const preferLast = extracted.lastName || formLastName || null;
|
||||
|
||||
let patient;
|
||||
try {
|
||||
patient = await createOrUpdatePatientByInsuranceId({
|
||||
insuranceId,
|
||||
firstName: preferFirst,
|
||||
lastName: preferLast,
|
||||
dob: formDob,
|
||||
userId,
|
||||
});
|
||||
} catch (e: any) {
|
||||
throw new Error(`Failed to create/update patient: ${e.message}`);
|
||||
}
|
||||
|
||||
if (patient && patient.id !== undefined) {
|
||||
let newStatus = "UNKNOWN";
|
||||
if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE";
|
||||
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE";
|
||||
|
||||
const updates: any = { status: newStatus };
|
||||
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
|
||||
await storage.updatePatient(patient.id, updates);
|
||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||
|
||||
// Helper: save a PDF buffer to the patient's ELIGIBILITY_STATUS group
|
||||
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
|
||||
try {
|
||||
const buffer = await fs.readFile(pdfPath);
|
||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||
const groupTitle = "Eligibility Status";
|
||||
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
|
||||
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
|
||||
if (!group?.id) throw new Error("PDF group creation failed");
|
||||
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
|
||||
if (created && typeof created === "object" && "id" in created) {
|
||||
return Number(created.id);
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Save eligibility PDF
|
||||
let eligibilityPdfFileId: number | null = null;
|
||||
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
||||
eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
||||
outputResult.pdfUploadStatus = eligibilityPdfFileId
|
||||
? "Eligibility PDF saved"
|
||||
: "Eligibility PDF upload failed";
|
||||
outputResult.pdfFilename = path.basename(seleniumResult.pdf_path);
|
||||
} else {
|
||||
outputResult.pdfUploadStatus = "No eligibility PDF returned by Selenium";
|
||||
}
|
||||
outputResult.pdfFileId = eligibilityPdfFileId;
|
||||
|
||||
// Save member details PDF
|
||||
if (seleniumResult.member_details_pdf_path?.endsWith(".pdf")) {
|
||||
outputResult.memberDetailsPdfFileId = await saveToGroup(seleniumResult.member_details_pdf_path);
|
||||
outputResult.memberDetailsPdfFilename = path.basename(seleniumResult.member_details_pdf_path);
|
||||
}
|
||||
|
||||
// Save history PDF
|
||||
let historyPdfFileId: number | null = null;
|
||||
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
||||
historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
||||
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
||||
}
|
||||
outputResult.historyPdfFileId = historyPdfFileId;
|
||||
} else {
|
||||
outputResult.patientUpdateStatus = "Patient not found or missing ID; no update performed";
|
||||
}
|
||||
|
||||
// Cleanup temp files
|
||||
try {
|
||||
if (seleniumResult.pdf_path) await emptyFolderContainingFile(seleniumResult.pdf_path);
|
||||
} catch (e) {
|
||||
console.error("[eligibilityHistoryProcessor] cleanup failed:", e);
|
||||
}
|
||||
|
||||
return outputResult;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { redisConnection } from "./connection";
|
||||
|
||||
/** Job types dispatched to the selenium Python worker. */
|
||||
export type SeleniumJobType =
|
||||
| "eligibility-check"
|
||||
| "claim-status-check"
|
||||
| "claim-submit"
|
||||
| "claim-pre-auth"
|
||||
@@ -19,8 +18,6 @@ export type SeleniumJobType =
|
||||
| "uniteddh-preauth-submit"
|
||||
| "tuftssco-preauth-submit"
|
||||
| "tuftssco-eligibility-check"
|
||||
| "mh-eligibility-history-check"
|
||||
| "cmsp-eligibility-history-remaining-check"
|
||||
| "bcbs-ma-eligibility-check";
|
||||
|
||||
export interface SeleniumJobData {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Worker, Job } from "bullmq";
|
||||
import { redisConnection } from "../connection";
|
||||
import { SeleniumJobData } from "../queues";
|
||||
import { io } from "../../socket";
|
||||
import { runEligibilityProcessor } from "../processors/eligibilityProcessor";
|
||||
import { runClaimStatusProcessor } from "../processors/claimStatusProcessor";
|
||||
import { runClaimSubmitProcessor } from "../processors/claimSubmitProcessor";
|
||||
|
||||
@@ -27,7 +26,7 @@ function emitJobUpdate(
|
||||
}
|
||||
|
||||
async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
||||
const { jobType, userId, socketId, enrichedPayload } = job.data;
|
||||
const { jobType, socketId, enrichedPayload } = job.data;
|
||||
const jobId = job.id ?? job.name;
|
||||
|
||||
emitJobUpdate(socketId, jobId, jobType, "active", {
|
||||
@@ -37,16 +36,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
||||
try {
|
||||
let result: any;
|
||||
|
||||
if (jobType === "eligibility-check") {
|
||||
result = await runEligibilityProcessor({
|
||||
enrichedPayload,
|
||||
userId,
|
||||
insuranceId: job.data.insuranceId!,
|
||||
formFirstName: job.data.formFirstName,
|
||||
formLastName: job.data.formLastName,
|
||||
formDob: job.data.formDob,
|
||||
});
|
||||
} else if (jobType === "claim-status-check") {
|
||||
if (jobType === "claim-status-check") {
|
||||
result = await runClaimStatusProcessor({
|
||||
enrichedPayload,
|
||||
insuranceId: job.data.insuranceId!,
|
||||
|
||||
@@ -43,6 +43,8 @@ import shoppingVendorsRoutes from "./shopping-vendors";
|
||||
import feeScheduleRoutes from "./feeSchedule";
|
||||
import licenseRoutes from "./license";
|
||||
import insuranceStatusBcbsMaRoutes from "./insuranceStatusBcbsMa";
|
||||
import labRxRoutes from "./lab-rx";
|
||||
import seleniumSettingsRoutes from "./selenium-settings";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -90,5 +92,7 @@ router.use("/commissions", commissionsRoutes);
|
||||
router.use("/shopping-vendors", shoppingVendorsRoutes);
|
||||
router.use("/fee-schedule", feeScheduleRoutes);
|
||||
router.use("/license", licenseRoutes);
|
||||
router.use("/lab-rx", labRxRoutes);
|
||||
router.use("/selenium-settings", seleniumSettingsRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "../../../../packages/db/types/patient-types";
|
||||
import { formatDobForAgent } from "../utils/dateUtils";
|
||||
import { seleniumQueue } from "../queue/queues";
|
||||
import { enqueueSeleniumJob, getSeleniumJobStatus } from "../queue/jobRunner";
|
||||
import { getSeleniumJobStatus } from "../queue/jobRunner";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -113,160 +113,6 @@ async function createOrUpdatePatientByInsuranceId(options: {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /eligibility-check
|
||||
* - run selenium
|
||||
* - if pdf created -> call extractor -> get name
|
||||
* - create or update patient (by memberId)
|
||||
* - attach PDF to patient (create pdf group/file)
|
||||
* - return { patient, pdfFileId, extractedName ... }
|
||||
*/
|
||||
router.post(
|
||||
"/eligibility-check",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
if (!req.body.data) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Missing Insurance Eligibility data for selenium" });
|
||||
}
|
||||
if (!req.user || !req.user.id) {
|
||||
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||
}
|
||||
|
||||
const insuranceEligibilityData =
|
||||
typeof req.body.data === "string"
|
||||
? JSON.parse(req.body.data)
|
||||
: req.body.data;
|
||||
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(
|
||||
req.user.id,
|
||||
insuranceEligibilityData.insuranceSiteKey
|
||||
);
|
||||
if (!credentials) {
|
||||
return res.status(404).json({
|
||||
error:
|
||||
"No insurance credentials found for this provider, Kindly Update this at Settings Page.",
|
||||
});
|
||||
}
|
||||
|
||||
const insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
|
||||
if (!insuranceId) {
|
||||
return res.status(400).json({ error: "Missing memberId" });
|
||||
}
|
||||
|
||||
const enrichedData = {
|
||||
...insuranceEligibilityData,
|
||||
massdhpUsername: credentials.username,
|
||||
massdhpPassword: credentials.password,
|
||||
};
|
||||
|
||||
const job = await seleniumQueue.add("eligibility-check", {
|
||||
jobType: "eligibility-check",
|
||||
userId: req.user.id,
|
||||
socketId: req.body.socketId,
|
||||
enrichedPayload: enrichedData,
|
||||
insuranceId,
|
||||
formFirstName: insuranceEligibilityData.firstName,
|
||||
formLastName: insuranceEligibilityData.lastName,
|
||||
formDob: insuranceEligibilityData.dateOfBirth,
|
||||
});
|
||||
|
||||
return res.json({ jobId: job.id, status: "queued" });
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/eligibility-history-check",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
if (!req.body.data) {
|
||||
return res.status(400).json({ error: "Missing Insurance Eligibility data for selenium" });
|
||||
}
|
||||
if (!req.user || !req.user.id) {
|
||||
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||
}
|
||||
|
||||
const data =
|
||||
typeof req.body.data === "string" ? JSON.parse(req.body.data) : req.body.data;
|
||||
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, "MH");
|
||||
if (!credentials) {
|
||||
return res.status(404).json({
|
||||
error: "No MassHealth credentials found. Please update them in Settings.",
|
||||
});
|
||||
}
|
||||
|
||||
const insuranceId = String(data.memberId ?? "").trim();
|
||||
if (!insuranceId) {
|
||||
return res.status(400).json({ error: "Missing memberId" });
|
||||
}
|
||||
|
||||
const enrichedData = {
|
||||
...data,
|
||||
massdhpUsername: credentials.username,
|
||||
massdhpPassword: credentials.password,
|
||||
};
|
||||
|
||||
const jobId = enqueueSeleniumJob({
|
||||
jobType: "mh-eligibility-history-check",
|
||||
userId: req.user.id,
|
||||
socketId: req.body.socketId,
|
||||
enrichedPayload: enrichedData,
|
||||
insuranceId,
|
||||
formFirstName: data.firstName,
|
||||
formLastName: data.lastName,
|
||||
formDob: data.dateOfBirth,
|
||||
});
|
||||
|
||||
return res.json({ jobId, status: "queued" });
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/cmsp-eligibility-history-remaining-check",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
if (!req.body.data) {
|
||||
return res.status(400).json({ error: "Missing data for selenium" });
|
||||
}
|
||||
if (!req.user || !req.user.id) {
|
||||
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||
}
|
||||
|
||||
const data =
|
||||
typeof req.body.data === "string" ? JSON.parse(req.body.data) : req.body.data;
|
||||
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, "MH");
|
||||
if (!credentials) {
|
||||
return res.status(404).json({
|
||||
error: "No MassHealth credentials found. Please update them in Settings.",
|
||||
});
|
||||
}
|
||||
|
||||
const insuranceId = String(data.memberId ?? "").trim();
|
||||
if (!insuranceId) {
|
||||
return res.status(400).json({ error: "Missing memberId" });
|
||||
}
|
||||
|
||||
const enrichedData = {
|
||||
...data,
|
||||
massdhpUsername: credentials.username,
|
||||
massdhpPassword: credentials.password,
|
||||
};
|
||||
|
||||
const jobId = enqueueSeleniumJob({
|
||||
jobType: "cmsp-eligibility-history-remaining-check",
|
||||
userId: req.user.id,
|
||||
socketId: req.body.socketId,
|
||||
enrichedPayload: enrichedData,
|
||||
insuranceId,
|
||||
formFirstName: data.firstName,
|
||||
formLastName: data.lastName,
|
||||
formDob: data.dateOfBirth,
|
||||
});
|
||||
|
||||
return res.json({ jobId, status: "queued" });
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/claim-status-check",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { storage } from "../storage";
|
||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||
import { forwardOtpToSeleniumUnitedDHClaimAgent } from "../services/seleniumUnitedDHClaimClient";
|
||||
import { io } from "../socket";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -40,11 +41,14 @@ router.post("/uniteddh-claim", async (req: Request, res: Response): Promise<any>
|
||||
});
|
||||
}
|
||||
|
||||
const seleniumSettings = await db.seleniumSettings.findUnique({ where: { userId: req.user.id } });
|
||||
|
||||
const enrichedPayload = {
|
||||
claim: {
|
||||
...claimData,
|
||||
uniteddhUsername: credentials.username,
|
||||
uniteddhPassword: credentials.password,
|
||||
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { storage } from "../storage";
|
||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||
import { forwardOtpToSeleniumUnitedDHPreAuthAgent } from "../services/seleniumUnitedDHPreAuthClient";
|
||||
import { io } from "../socket";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -40,11 +41,14 @@ router.post("/uniteddh-preauth", async (req: Request, res: Response): Promise<an
|
||||
});
|
||||
}
|
||||
|
||||
const seleniumSettings = await db.seleniumSettings.findUnique({ where: { userId: req.user.id } });
|
||||
|
||||
const enrichedPayload = {
|
||||
claim: {
|
||||
...claimData,
|
||||
uniteddhUsername: credentials.username,
|
||||
uniteddhPassword: credentials.password,
|
||||
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { storage } from "../storage";
|
||||
import { forwardOtpToSeleniumUnitedSCOAgent } from "../services/seleniumUnitedSCOEligibilityClient";
|
||||
import { io } from "../socket";
|
||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -62,10 +63,13 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const seleniumSettings = await db.seleniumSettings.findUnique({ where: { userId: req.user.id } });
|
||||
|
||||
const enrichedData = {
|
||||
...rawData,
|
||||
unitedscoUsername: credentials.username,
|
||||
unitedscoPassword: credentials.password,
|
||||
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||
};
|
||||
|
||||
const socketId: string | undefined = req.body.socketId;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { storage } from "../storage";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/templates", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
const templates = await storage.getLabRxTemplates(userId);
|
||||
return res.json(templates);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to fetch lab RX templates", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/templates", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
const { name, labName, labPhone, labFax, labAddress, labAccount, caseType, material, instructions } = req.body;
|
||||
if (!name?.trim()) return res.status(400).json({ message: "Template name is required" });
|
||||
const template = await storage.createLabRxTemplate(userId, {
|
||||
name: name.trim(),
|
||||
labName: labName?.trim() || undefined,
|
||||
labPhone: labPhone?.trim() || undefined,
|
||||
labFax: labFax?.trim() || undefined,
|
||||
labAddress: labAddress?.trim() || undefined,
|
||||
labAccount: labAccount?.trim() || undefined,
|
||||
caseType: caseType?.trim() || undefined,
|
||||
material: material?.trim() || undefined,
|
||||
instructions: instructions?.trim() || undefined,
|
||||
});
|
||||
return res.status(201).json(template);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to create lab RX template", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/templates/:id", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
const id = Number(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).json({ message: "Invalid ID" });
|
||||
const { name, labName, labPhone, labFax, labAddress, labAccount, caseType, material, instructions } = req.body;
|
||||
if (name !== undefined && !name?.trim()) return res.status(400).json({ message: "Name cannot be empty" });
|
||||
const template = await storage.updateLabRxTemplate(userId, id, {
|
||||
name: name?.trim(),
|
||||
labName: labName?.trim() || undefined,
|
||||
labPhone: labPhone?.trim() || undefined,
|
||||
labFax: labFax?.trim() || undefined,
|
||||
labAddress: labAddress?.trim() || undefined,
|
||||
labAccount: labAccount?.trim() || undefined,
|
||||
caseType: caseType?.trim() || undefined,
|
||||
material: material?.trim() || undefined,
|
||||
instructions: instructions?.trim() || undefined,
|
||||
});
|
||||
return res.json(template);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to update lab RX template", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/templates/reorder", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
const { orderedIds } = req.body;
|
||||
if (!Array.isArray(orderedIds)) return res.status(400).json({ message: "orderedIds must be an array" });
|
||||
await storage.reorderLabRxTemplates(userId, orderedIds);
|
||||
return res.status(204).send();
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to reorder templates", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/templates/:id", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
const id = Number(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).json({ message: "Invalid ID" });
|
||||
const ok = await storage.deleteLabRxTemplate(userId, id);
|
||||
if (!ok) return res.status(404).json({ message: "Template not found" });
|
||||
return res.status(204).send();
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to delete lab RX template", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/selenium-settings
|
||||
router.get("/", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const settings = await db.seleniumSettings.findUnique({ where: { userId } });
|
||||
return res.status(200).json({ paymentGroupId: settings?.paymentGroupId ?? "" });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to fetch Selenium settings", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/selenium-settings
|
||||
router.put("/", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const { paymentGroupId } = req.body;
|
||||
if (typeof paymentGroupId !== "string") {
|
||||
return res.status(400).json({ message: "paymentGroupId must be a string" });
|
||||
}
|
||||
|
||||
const settings = await db.seleniumSettings.upsert({
|
||||
where: { userId },
|
||||
update: { paymentGroupId: paymentGroupId.trim() },
|
||||
create: { userId, paymentGroupId: paymentGroupId.trim() },
|
||||
});
|
||||
|
||||
return res.status(200).json({ paymentGroupId: settings.paymentGroupId });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to save Selenium settings", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -25,6 +25,7 @@ import { procedureTimeslotStorage } from "./procedure-timeslot-storage";
|
||||
import { insuranceContactStorage } from "./insurance-contact-storage";
|
||||
import { commissionsStorage } from "./commissions-storage";
|
||||
import { shoppingVendorStorage } from "./shopping-vendor-storage";
|
||||
import { labRxStorage } from "./lab-rx-storage";
|
||||
|
||||
|
||||
export const storage = {
|
||||
@@ -53,6 +54,7 @@ export const storage = {
|
||||
...insuranceContactStorage,
|
||||
...commissionsStorage,
|
||||
...shoppingVendorStorage,
|
||||
...labRxStorage,
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
export type LabRxTemplateInput = {
|
||||
name: string;
|
||||
labName?: string;
|
||||
labPhone?: string;
|
||||
labFax?: string;
|
||||
labAddress?: string;
|
||||
labAccount?: string;
|
||||
caseType?: string;
|
||||
material?: string;
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_TEMPLATES: LabRxTemplateInput[] = [
|
||||
{
|
||||
name: "Highland Dental Crown",
|
||||
labName: "Highland Dental Studio",
|
||||
labPhone: "781-552-5198",
|
||||
labAddress: "87 Cambridge Street, Burlington, MA 01803",
|
||||
caseType: "Crown",
|
||||
material: "Zirconia",
|
||||
},
|
||||
{
|
||||
name: "Highland Dental Full Denture",
|
||||
labName: "Highland Dental Studio",
|
||||
labPhone: "781-552-5198",
|
||||
labAddress: "87 Cambridge Street, Burlington, MA 01803",
|
||||
caseType: "Full Denture",
|
||||
},
|
||||
{
|
||||
name: "Highland Dental Partial Denture",
|
||||
labName: "Highland Dental Studio",
|
||||
labPhone: "781-552-5198",
|
||||
labAddress: "87 Cambridge Street, Burlington, MA 01803",
|
||||
caseType: "Partial Denture",
|
||||
},
|
||||
];
|
||||
|
||||
export const labRxStorage = {
|
||||
async getLabRxTemplates(userId: number) {
|
||||
const existing = await db.labRxTemplate.findMany({
|
||||
where: { userId },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
|
||||
if (existing.length === 0) {
|
||||
await db.labRxTemplate.createMany({
|
||||
data: DEFAULT_TEMPLATES.map((t, index) => ({ userId, sortOrder: index, ...t })),
|
||||
});
|
||||
return db.labRxTemplate.findMany({
|
||||
where: { userId },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
}
|
||||
|
||||
return existing;
|
||||
},
|
||||
|
||||
async reorderLabRxTemplates(userId: number, orderedIds: number[]) {
|
||||
await db.$transaction(
|
||||
orderedIds.map((id, index) =>
|
||||
db.labRxTemplate.updateMany({
|
||||
where: { id, userId },
|
||||
data: { sortOrder: index },
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async createLabRxTemplate(userId: number, data: LabRxTemplateInput) {
|
||||
return db.labRxTemplate.create({
|
||||
data: { userId, ...data },
|
||||
});
|
||||
},
|
||||
|
||||
async updateLabRxTemplate(userId: number, id: number, data: Partial<LabRxTemplateInput>) {
|
||||
return db.labRxTemplate.update({
|
||||
where: { id, userId },
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
async deleteLabRxTemplate(userId: number, id: number) {
|
||||
const existing = await db.labRxTemplate.findFirst({ where: { id, userId } });
|
||||
if (!existing) return false;
|
||||
await db.labRxTemplate.delete({ where: { id } });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>My Dental Office Management</title>
|
||||
<title>Wellspring Acupuncture Office Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"postcss": "^8.4.47",
|
||||
"react": "^19.1.0",
|
||||
"react-barcode": "^1.6.1",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
|
||||
@@ -29,6 +29,7 @@ const ReportsPage = lazy(() => import("./pages/reports-page"));
|
||||
const CloudStoragePage = lazy(() => import("./pages/cloud-storage-page"));
|
||||
const JobMonitorPage = lazy(() => import("./pages/job-monitor-page"));
|
||||
const ChartPage = lazy(() => import("./pages/chart-page"));
|
||||
const DentalEducationPage = lazy(() => import("./pages/dental-education-page"));
|
||||
const AiInputAgentPage = lazy(() => import("./pages/ai-input-agent-page"));
|
||||
const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page"));
|
||||
const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page"));
|
||||
@@ -65,6 +66,7 @@ function Router() {
|
||||
/>
|
||||
<ProtectedRoute path="/reports" component={() => <ReportsPage />} />
|
||||
<ProtectedRoute path="/cloud-storage" component={() => <CloudStoragePage />} />
|
||||
<ProtectedRoute path="/dental-education" component={() => <DentalEducationPage />} />
|
||||
<ProtectedRoute path="/ai-input-agent" component={() => <AiInputAgentPage />} />
|
||||
<ProtectedRoute path="/dental-shopping/search-tag" component={() => <DentalShoppingSearchTagPage />} />
|
||||
<ProtectedRoute path="/dental-shopping/login-info" component={() => <DentalShoppingLoginInfoPage />} />
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useRef } from "react";
|
||||
import Barcode from "react-barcode";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Download, Printer } from "lucide-react";
|
||||
import { Patient } from "@repo/db/types";
|
||||
|
||||
interface BarcodeModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
patient: Patient;
|
||||
}
|
||||
|
||||
export function BarcodeModal({ open, onClose, patient }: BarcodeModalProps) {
|
||||
const barcodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const patientLabel = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
|
||||
const barcodeValue = patientLabel || "PATIENT";
|
||||
|
||||
const handlePrint = () => {
|
||||
const svg = barcodeRef.current?.querySelector("svg");
|
||||
if (!svg) return;
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const printWindow = window.open("", "_blank", "width=400,height=300");
|
||||
if (!printWindow) return;
|
||||
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Barcode — ${patientLabel}</title>
|
||||
<style>
|
||||
body { margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; }
|
||||
p { margin: 4px 0 0; font-size: 13px; color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${svgData}
|
||||
<p>${patientLabel}</p>
|
||||
<script>window.onload = () => { window.print(); window.close(); }<\/script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const svg = barcodeRef.current?.querySelector("svg");
|
||||
if (!svg) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = `barcode-${patientLabel.replace(/\s+/g, "-")}.png`;
|
||||
link.href = canvas.toDataURL("image/png");
|
||||
link.click();
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Patient Barcode</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col items-center gap-3 py-4">
|
||||
<div ref={barcodeRef}>
|
||||
<Barcode
|
||||
value={barcodeValue}
|
||||
format="CODE128"
|
||||
width={2}
|
||||
height={80}
|
||||
displayValue={true}
|
||||
fontSize={13}
|
||||
margin={10}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={handlePrint} className="gap-2">
|
||||
<Printer className="h-4 w-4" />
|
||||
Print
|
||||
</Button>
|
||||
<Button onClick={handleDownload} className="gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
Download PNG
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,258 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog";
|
||||
import { Plus, Pencil, Trash2, Package } from "lucide-react";
|
||||
const STATUS_COLORS = {
|
||||
pending: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
||||
"in-lab": "bg-blue-100 text-blue-700 border-blue-200",
|
||||
received: "bg-green-100 text-green-700 border-green-200",
|
||||
delivered: "bg-gray-100 text-gray-600 border-gray-200",
|
||||
cancelled: "bg-red-100 text-red-600 border-red-200",
|
||||
};
|
||||
const STATUS_LABELS = {
|
||||
pending: "Pending",
|
||||
"in-lab": "In Lab",
|
||||
received: "Received",
|
||||
delivered: "Delivered",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
const CASE_TYPES = [
|
||||
"Crown – PFM",
|
||||
"Crown – All Ceramic",
|
||||
"Crown – Zirconia",
|
||||
"Crown – Gold",
|
||||
"Bridge – PFM",
|
||||
"Bridge – Zirconia",
|
||||
"Implant Crown",
|
||||
"Implant Abutment",
|
||||
"Veneer",
|
||||
"Inlay / Onlay",
|
||||
"Full Denture (Upper)",
|
||||
"Full Denture (Lower)",
|
||||
"Partial Denture",
|
||||
"Night Guard",
|
||||
"Bleaching Tray",
|
||||
"Retainer",
|
||||
"Diagnostic Model",
|
||||
"Other",
|
||||
];
|
||||
const COMMON_LABS = [
|
||||
"Dental Arts Lab",
|
||||
"National Dentex",
|
||||
"Glidewell Dental",
|
||||
"Henry Schein Lab",
|
||||
"Affordable Dentures Lab",
|
||||
"Local Lab",
|
||||
];
|
||||
const SHADES = [
|
||||
"A1", "A2", "A3", "A3.5", "A4",
|
||||
"B1", "B2", "B3", "B4",
|
||||
"C1", "C2", "C3", "C4",
|
||||
"D2", "D3", "D4",
|
||||
"BL1", "BL2", "BL3", "BL4",
|
||||
"Custom",
|
||||
];
|
||||
let nextId = 1;
|
||||
const newOrder = () => ({
|
||||
id: nextId++,
|
||||
orderDate: new Date().toISOString().substring(0, 10),
|
||||
dueDate: "",
|
||||
tooth: "",
|
||||
caseType: "",
|
||||
lab: "",
|
||||
shade: "",
|
||||
status: "pending",
|
||||
rush: false,
|
||||
notes: "",
|
||||
trackingNumber: "",
|
||||
});
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { BarcodeModal } from "@/components/chart/barcode-modal";
|
||||
import { LabRxModal } from "@/components/chart/lab-rx-modal";
|
||||
import { Barcode, FileText } from "lucide-react";
|
||||
|
||||
export function LabManagementTab() {
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(newOrder());
|
||||
const openAdd = () => {
|
||||
setEditing(newOrder());
|
||||
setDialogOpen(true);
|
||||
};
|
||||
const openEdit = (order) => {
|
||||
setEditing({ ...order });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
const handleSave = () => {
|
||||
setOrders((prev) => {
|
||||
const idx = prev.findIndex((o) => o.id === editing.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = editing;
|
||||
return next;
|
||||
}
|
||||
return [...prev, editing];
|
||||
});
|
||||
setDialogOpen(false);
|
||||
};
|
||||
const handleDelete = (id) => {
|
||||
setOrders((prev) => prev.filter((o) => o.id !== id));
|
||||
};
|
||||
const pending = orders.filter((o) => o.status === "pending" || o.status === "in-lab").length;
|
||||
const rush = orders.filter((o) => o.rush && o.status !== "delivered" && o.status !== "cancelled").length;
|
||||
return (<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-4 text-sm text-gray-600">
|
||||
{pending > 0 && <span>Open orders: <strong>{pending}</strong></span>}
|
||||
{rush > 0 && <span className="text-red-600">Rush: <strong>{rush}</strong></span>}
|
||||
{orders.length === 0 && <span>No lab orders yet</span>}
|
||||
</div>
|
||||
<Button size="sm" onClick={openAdd} className="gap-1.5">
|
||||
<Plus className="h-4 w-4"/>
|
||||
New Lab Order
|
||||
</Button>
|
||||
</div>
|
||||
const [selectedPatient, setSelectedPatient] = useState(null);
|
||||
const [barcodeOpen, setBarcodeOpen] = useState(false);
|
||||
const [labRxOpen, setLabRxOpen] = useState(false);
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="w-24">Order Date</TableHead>
|
||||
<TableHead className="w-16">Tooth</TableHead>
|
||||
<TableHead>Case Type</TableHead>
|
||||
<TableHead>Lab</TableHead>
|
||||
<TableHead className="w-16 text-center">Shade</TableHead>
|
||||
<TableHead className="w-24">Due</TableHead>
|
||||
<TableHead className="w-28">Status</TableHead>
|
||||
<TableHead className="w-20 text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{orders.length === 0 ? (<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-gray-400 py-10">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Package className="h-8 w-8 text-gray-300"/>
|
||||
<span>No lab orders yet. Click "New Lab Order" to add one.</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>) : (orders.map((order) => (<TableRow key={order.id} className={order.rush ? "bg-red-50/40" : ""}>
|
||||
<TableCell className="text-sm">{order.orderDate}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{order.tooth || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium">{order.caseType}</span>
|
||||
{order.rush && (<Badge className="text-[10px] bg-red-100 text-red-600 border-red-200 px-1 py-0" variant="outline">
|
||||
RUSH
|
||||
</Badge>)}
|
||||
</div>
|
||||
{order.notes && (<p className="text-xs text-gray-400 truncate max-w-xs mt-0.5">{order.notes}</p>)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{order.lab || "—"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{order.shade || "—"}</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{order.dueDate || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`text-xs border ${STATUS_COLORS[order.status]}`} variant="outline">
|
||||
{STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEdit(order)}>
|
||||
<Pencil className="h-3.5 w-3.5"/>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50" onClick={() => handleDelete(order.id)}>
|
||||
<Trash2 className="h-3.5 w-3.5"/>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lab Order</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 py-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Order Date</Label>
|
||||
<Input type="date" value={editing.orderDate} onChange={(e) => setEditing((d) => ({ ...d, orderDate: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Due Date</Label>
|
||||
<Input type="date" value={editing.dueDate} onChange={(e) => setEditing((d) => ({ ...d, dueDate: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Tooth #</Label>
|
||||
<Input placeholder="e.g. 14" value={editing.tooth} onChange={(e) => setEditing((d) => ({ ...d, tooth: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Shade</Label>
|
||||
<Select value={editing.shade} onValueChange={(v) => setEditing((d) => ({ ...d, shade: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select..."/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SHADES.map((s) => <SelectItem key={s} value={s}>{s}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Case Type</Label>
|
||||
<Select value={editing.caseType} onValueChange={(v) => setEditing((d) => ({ ...d, caseType: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select case type..."/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CASE_TYPES.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Lab</Label>
|
||||
<Select value={editing.lab} onValueChange={(v) => setEditing((d) => ({ ...d, lab: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select lab..."/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_LABS.map((l) => <SelectItem key={l} value={l}>{l}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Status</Label>
|
||||
<Select value={editing.status} onValueChange={(v) => setEditing((d) => ({ ...d, status: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(STATUS_LABELS).map((s) => (<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Tracking #</Label>
|
||||
<Input placeholder="Optional" value={editing.trackingNumber} onChange={(e) => setEditing((d) => ({ ...d, trackingNumber: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<input type="checkbox" id="rush" checked={editing.rush} onChange={(e) => setEditing((d) => ({ ...d, rush: e.target.checked }))} className="h-4 w-4 rounded border-gray-300"/>
|
||||
<Label htmlFor="rush" className="text-sm cursor-pointer text-red-600 font-medium">
|
||||
Rush Order
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Notes</Label>
|
||||
<Textarea placeholder="Special instructions, shade details..." value={editing.notes} onChange={(e) => setEditing((d) => ({ ...d, notes: e.target.value }))} className="text-sm resize-none" rows={2}/>
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lab Actions</CardTitle>
|
||||
<CardDescription>
|
||||
{selectedPatient
|
||||
? `Selected patient: ${selectedPatient.firstName} ${selectedPatient.lastName}`
|
||||
: "Select a patient below to get started."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!selectedPatient}
|
||||
onClick={() => setBarcodeOpen(true)}
|
||||
>
|
||||
<Barcode className="h-4 w-4" />
|
||||
Create a Barcode
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!selectedPatient}
|
||||
onClick={() => setLabRxOpen(true)}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Lab RX
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSave} disabled={!editing.caseType}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>);
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>Select a patient by clicking the checkbox.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowView={true}
|
||||
allowDelete={false}
|
||||
allowCheckbox={true}
|
||||
allowEdit={false}
|
||||
onSelectPatient={setSelectedPatient}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{selectedPatient && (
|
||||
<>
|
||||
<BarcodeModal
|
||||
open={barcodeOpen}
|
||||
onClose={() => setBarcodeOpen(false)}
|
||||
patient={selectedPatient}
|
||||
/>
|
||||
<LabRxModal
|
||||
open={labRxOpen}
|
||||
onClose={() => setLabRxOpen(false)}
|
||||
patient={selectedPatient}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,364 +1,84 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Plus, Pencil, Trash2, Package } from "lucide-react";
|
||||
|
||||
type LabStatus = "pending" | "in-lab" | "received" | "delivered" | "cancelled";
|
||||
|
||||
interface LabOrder {
|
||||
id: number;
|
||||
orderDate: string;
|
||||
dueDate: string;
|
||||
tooth: string;
|
||||
caseType: string;
|
||||
lab: string;
|
||||
shade: string;
|
||||
status: LabStatus;
|
||||
rush: boolean;
|
||||
notes: string;
|
||||
trackingNumber: string;
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<LabStatus, string> = {
|
||||
pending: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
||||
"in-lab": "bg-blue-100 text-blue-700 border-blue-200",
|
||||
received: "bg-green-100 text-green-700 border-green-200",
|
||||
delivered: "bg-gray-100 text-gray-600 border-gray-200",
|
||||
cancelled: "bg-red-100 text-red-600 border-red-200",
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<LabStatus, string> = {
|
||||
pending: "Pending",
|
||||
"in-lab": "In Lab",
|
||||
received: "Received",
|
||||
delivered: "Delivered",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
|
||||
const CASE_TYPES = [
|
||||
"Crown – PFM",
|
||||
"Crown – All Ceramic",
|
||||
"Crown – Zirconia",
|
||||
"Crown – Gold",
|
||||
"Bridge – PFM",
|
||||
"Bridge – Zirconia",
|
||||
"Implant Crown",
|
||||
"Implant Abutment",
|
||||
"Veneer",
|
||||
"Inlay / Onlay",
|
||||
"Full Denture (Upper)",
|
||||
"Full Denture (Lower)",
|
||||
"Partial Denture",
|
||||
"Night Guard",
|
||||
"Bleaching Tray",
|
||||
"Retainer",
|
||||
"Diagnostic Model",
|
||||
"Other",
|
||||
];
|
||||
|
||||
const COMMON_LABS = [
|
||||
"Dental Arts Lab",
|
||||
"National Dentex",
|
||||
"Glidewell Dental",
|
||||
"Henry Schein Lab",
|
||||
"Affordable Dentures Lab",
|
||||
"Local Lab",
|
||||
];
|
||||
|
||||
const SHADES = [
|
||||
"A1", "A2", "A3", "A3.5", "A4",
|
||||
"B1", "B2", "B3", "B4",
|
||||
"C1", "C2", "C3", "C4",
|
||||
"D2", "D3", "D4",
|
||||
"BL1", "BL2", "BL3", "BL4",
|
||||
"Custom",
|
||||
];
|
||||
|
||||
let nextId = 1;
|
||||
const newOrder = (): LabOrder => ({
|
||||
id: nextId++,
|
||||
orderDate: new Date().toISOString().substring(0, 10),
|
||||
dueDate: "",
|
||||
tooth: "",
|
||||
caseType: "",
|
||||
lab: "",
|
||||
shade: "",
|
||||
status: "pending",
|
||||
rush: false,
|
||||
notes: "",
|
||||
trackingNumber: "",
|
||||
});
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { BarcodeModal } from "@/components/chart/barcode-modal";
|
||||
import { LabRxModal } from "@/components/chart/lab-rx-modal";
|
||||
import { Barcode, FileText } from "lucide-react";
|
||||
import { Patient } from "@repo/db/types";
|
||||
|
||||
export function LabManagementTab() {
|
||||
const [orders, setOrders] = useState<LabOrder[]>([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<LabOrder>(newOrder());
|
||||
|
||||
const openAdd = () => {
|
||||
setEditing(newOrder());
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (order: LabOrder) => {
|
||||
setEditing({ ...order });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setOrders((prev) => {
|
||||
const idx = prev.findIndex((o) => o.id === editing.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = editing;
|
||||
return next;
|
||||
}
|
||||
return [...prev, editing];
|
||||
});
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setOrders((prev) => prev.filter((o) => o.id !== id));
|
||||
};
|
||||
|
||||
const pending = orders.filter((o) => o.status === "pending" || o.status === "in-lab").length;
|
||||
const rush = orders.filter((o) => o.rush && o.status !== "delivered" && o.status !== "cancelled").length;
|
||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||
const [barcodeOpen, setBarcodeOpen] = useState(false);
|
||||
const [labRxOpen, setLabRxOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-4 text-sm text-gray-600">
|
||||
{pending > 0 && <span>Open orders: <strong>{pending}</strong></span>}
|
||||
{rush > 0 && <span className="text-red-600">Rush: <strong>{rush}</strong></span>}
|
||||
{orders.length === 0 && <span>No lab orders yet</span>}
|
||||
</div>
|
||||
<Button size="sm" onClick={openAdd} className="gap-1.5">
|
||||
<Plus className="h-4 w-4" />
|
||||
New Lab Order
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="w-24">Order Date</TableHead>
|
||||
<TableHead className="w-16">Tooth</TableHead>
|
||||
<TableHead>Case Type</TableHead>
|
||||
<TableHead>Lab</TableHead>
|
||||
<TableHead className="w-16 text-center">Shade</TableHead>
|
||||
<TableHead className="w-24">Due</TableHead>
|
||||
<TableHead className="w-28">Status</TableHead>
|
||||
<TableHead className="w-20 text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{orders.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-gray-400 py-10">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Package className="h-8 w-8 text-gray-300" />
|
||||
<span>No lab orders yet. Click "New Lab Order" to add one.</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
orders.map((order) => (
|
||||
<TableRow key={order.id} className={order.rush ? "bg-red-50/40" : ""}>
|
||||
<TableCell className="text-sm">{order.orderDate}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{order.tooth || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium">{order.caseType}</span>
|
||||
{order.rush && (
|
||||
<Badge className="text-[10px] bg-red-100 text-red-600 border-red-200 px-1 py-0" variant="outline">
|
||||
RUSH
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{order.notes && (
|
||||
<p className="text-xs text-gray-400 truncate max-w-xs mt-0.5">{order.notes}</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{order.lab || "—"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{order.shade || "—"}</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{order.dueDate || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`text-xs border ${STATUS_COLORS[order.status]}`} variant="outline">
|
||||
{STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEdit(order)}>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50" onClick={() => handleDelete(order.id)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lab Order</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 py-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Order Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={editing.orderDate}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, orderDate: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Due Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={editing.dueDate}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, dueDate: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Tooth #</Label>
|
||||
<Input
|
||||
placeholder="e.g. 14"
|
||||
value={editing.tooth}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, tooth: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Shade</Label>
|
||||
<Select
|
||||
value={editing.shade}
|
||||
onValueChange={(v) => setEditing((d) => ({ ...d, shade: v }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SHADES.map((s) => <SelectItem key={s} value={s}>{s}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Case Type</Label>
|
||||
<Select
|
||||
value={editing.caseType}
|
||||
onValueChange={(v) => setEditing((d) => ({ ...d, caseType: v }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select case type..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CASE_TYPES.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Lab</Label>
|
||||
<Select
|
||||
value={editing.lab}
|
||||
onValueChange={(v) => setEditing((d) => ({ ...d, lab: v }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select lab..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_LABS.map((l) => <SelectItem key={l} value={l}>{l}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Status</Label>
|
||||
<Select
|
||||
value={editing.status}
|
||||
onValueChange={(v) => setEditing((d) => ({ ...d, status: v as LabStatus }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(STATUS_LABELS) as LabStatus[]).map((s) => (
|
||||
<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Tracking #</Label>
|
||||
<Input
|
||||
placeholder="Optional"
|
||||
value={editing.trackingNumber}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, trackingNumber: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rush"
|
||||
checked={editing.rush}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, rush: e.target.checked }))}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<Label htmlFor="rush" className="text-sm cursor-pointer text-red-600 font-medium">
|
||||
Rush Order
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Notes</Label>
|
||||
<Textarea
|
||||
placeholder="Special instructions, shade details..."
|
||||
value={editing.notes}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, notes: e.target.value }))}
|
||||
className="text-sm resize-none"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{/* Action buttons */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lab Actions</CardTitle>
|
||||
<CardDescription>
|
||||
{selectedPatient
|
||||
? `Selected patient: ${selectedPatient.firstName} ${selectedPatient.lastName}`
|
||||
: "Select a patient below to get started."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!selectedPatient}
|
||||
onClick={() => setBarcodeOpen(true)}
|
||||
>
|
||||
<Barcode className="h-4 w-4" />
|
||||
Create a Barcode
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!selectedPatient}
|
||||
onClick={() => setLabRxOpen(true)}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Lab RX
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSave} disabled={!editing.caseType}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Patient search / selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>Select a patient by clicking the checkbox.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowView={true}
|
||||
allowDelete={false}
|
||||
allowCheckbox={true}
|
||||
allowEdit={false}
|
||||
onSelectPatient={setSelectedPatient}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{selectedPatient && (
|
||||
<>
|
||||
<BarcodeModal
|
||||
open={barcodeOpen}
|
||||
onClose={() => setBarcodeOpen(false)}
|
||||
patient={selectedPatient}
|
||||
/>
|
||||
<LabRxModal
|
||||
open={labRxOpen}
|
||||
onClose={() => setLabRxOpen(false)}
|
||||
patient={selectedPatient}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Plus, Pencil, Trash2, Printer, X, Check, FlaskConical, ChevronUp, ChevronDown } from "lucide-react";
|
||||
|
||||
const EMPTY_TEMPLATE = {
|
||||
name: "", labName: "", labPhone: "", labFax: "",
|
||||
labAddress: "", labAccount: "", caseType: "", material: "", instructions: "Please make\nThank you!",
|
||||
};
|
||||
const EMPTY_RX = { toothNumbers: "", shade: "", dueDate: "", doctorName: "", age: "", officeName: "", additionalNotes: "Please make\nThank you!" };
|
||||
|
||||
const calcAge = (dob) => {
|
||||
if (!dob) return "";
|
||||
const birth = new Date(dob);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
const m = today.getMonth() - birth.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
|
||||
return String(age);
|
||||
};
|
||||
|
||||
const CASE_TYPES = ["Crown", "Bridge", "Veneer", "Inlay/Onlay", "Full Denture", "Partial Denture", "Night Guard", "Retainer", "Implant Crown", "Other"];
|
||||
const MATERIALS = ["Zirconia", "PFM (Porcelain-Fused-to-Metal)", "E-Max", "Acrylic", "Cast Metal", "Composite", "PMMA", "Other"];
|
||||
|
||||
export function LabRxModal({ open, onClose, patient }) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const printRef = useRef(null);
|
||||
|
||||
const [rxTemplate, setRxTemplate] = useState(null);
|
||||
const [rx, setRx] = useState(EMPTY_RX);
|
||||
|
||||
const [renamingId, setRenamingId] = useState(null);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
|
||||
const [showTemplateForm, setShowTemplateForm] = useState(false);
|
||||
const [editingTemplateId, setEditingTemplateId] = useState(null);
|
||||
const [templateForm, setTemplateForm] = useState(EMPTY_TEMPLATE);
|
||||
|
||||
const { data: providers = [] } = useQuery({
|
||||
queryKey: ["/api/npiProviders"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/npiProviders");
|
||||
if (!res.ok) throw new Error("Failed to fetch providers");
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: officeContact } = useQuery({
|
||||
queryKey: ["/api/office-contact"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/office-contact");
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: templates = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/lab-rx/templates"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/lab-rx/templates");
|
||||
if (!res.ok) throw new Error("Failed to fetch");
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["/api/lab-rx/templates"] });
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data) => {
|
||||
const res = await apiRequest("POST", "/api/lab-rx/templates", data);
|
||||
if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to save"); }
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => { invalidate(); setShowTemplateForm(false); setTemplateForm(EMPTY_TEMPLATE); toast({ title: "Template saved" }); },
|
||||
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async ({ id, data }) => {
|
||||
const res = await apiRequest("PUT", `/api/lab-rx/templates/${id}`, data);
|
||||
if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to update"); }
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => { invalidate(); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); setShowTemplateForm(false); toast({ title: "Template updated" }); },
|
||||
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: async ({ id, name }) => {
|
||||
const res = await apiRequest("PUT", `/api/lab-rx/templates/${id}`, { name });
|
||||
if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to rename"); }
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => { invalidate(); setRenamingId(null); },
|
||||
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id) => {
|
||||
const res = await apiRequest("DELETE", `/api/lab-rx/templates/${id}`);
|
||||
if (!res.ok) throw new Error("Failed to delete");
|
||||
},
|
||||
onSuccess: () => { invalidate(); toast({ title: "Template deleted" }); },
|
||||
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const reorderMutation = useMutation({
|
||||
mutationFn: async (orderedIds) => {
|
||||
const res = await apiRequest("POST", "/api/lab-rx/templates/reorder", { orderedIds });
|
||||
if (!res.ok) throw new Error("Failed to reorder");
|
||||
},
|
||||
onSuccess: () => invalidate(),
|
||||
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const moveTemplate = (index, direction) => {
|
||||
const swapIndex = direction === "up" ? index - 1 : index + 1;
|
||||
const reordered = [...templates];
|
||||
const a = reordered[index];
|
||||
const b = reordered[swapIndex];
|
||||
reordered[index] = b;
|
||||
reordered[swapIndex] = a;
|
||||
queryClient.setQueryData(["/api/lab-rx/templates"], reordered);
|
||||
reorderMutation.mutate(reordered.map((t) => t.id));
|
||||
};
|
||||
|
||||
const openRx = (t) => {
|
||||
if (renamingId !== null) return;
|
||||
setRxTemplate(t);
|
||||
setRx({ ...EMPTY_RX, doctorName: providers[0]?.providerName ?? "", age: calcAge(patient.dateOfBirth), officeName: officeContact?.officeName ?? "" });
|
||||
};
|
||||
|
||||
const startRename = (e, t) => {
|
||||
e.stopPropagation();
|
||||
setRenamingId(t.id);
|
||||
setRenameValue(t.name);
|
||||
};
|
||||
|
||||
const commitRename = () => {
|
||||
if (!renameValue.trim()) { toast({ title: "Name cannot be empty", variant: "destructive" }); return; }
|
||||
if (renamingId !== null) renameMutation.mutate({ id: renamingId, name: renameValue.trim() });
|
||||
};
|
||||
|
||||
const cancelRename = () => setRenamingId(null);
|
||||
|
||||
const openAddTemplate = () => { setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); setShowTemplateForm(true); };
|
||||
const openEditTemplate = (t) => {
|
||||
setEditingTemplateId(t.id);
|
||||
setTemplateForm({ name: t.name, labName: t.labName ?? "", labPhone: t.labPhone ?? "", labFax: t.labFax ?? "", labAddress: t.labAddress ?? "", labAccount: t.labAccount ?? "", caseType: t.caseType ?? "", material: t.material ?? "", instructions: t.instructions ?? "" });
|
||||
setShowTemplateForm(true);
|
||||
};
|
||||
const handleSaveTemplate = () => {
|
||||
if (!templateForm.name.trim()) { toast({ title: "Template name is required", variant: "destructive" }); return; }
|
||||
if (editingTemplateId !== null) updateMutation.mutate({ id: editingTemplateId, data: templateForm });
|
||||
else createMutation.mutate(templateForm);
|
||||
};
|
||||
const isSaving = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
const handlePrint = () => {
|
||||
const content = printRef.current;
|
||||
if (!content || !rxTemplate) return;
|
||||
const win = window.open("", "_blank", "width=800,height=900");
|
||||
if (!win) return;
|
||||
win.document.write(`<!DOCTYPE html><html><head><title>Lab RX</title>
|
||||
<style>
|
||||
body{font-family:Arial,sans-serif;margin:0;padding:24px;font-size:13px;color:#111}
|
||||
h1{font-size:22px;margin:0 0 4px;text-align:center}
|
||||
.subtitle{text-align:center;color:#555;font-size:12px;margin-bottom:12px}
|
||||
hr{border:none;border-top:2px solid #000;margin:10px 0}
|
||||
.section{margin-bottom:14px}
|
||||
.section-title{font-weight:bold;font-size:11px;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #bbb;margin-bottom:8px;padding-bottom:3px;color:#333}
|
||||
.row{display:flex;gap:24px;margin-bottom:6px}
|
||||
.field{flex:1}
|
||||
.field label{display:block;font-size:10px;color:#777;margin-bottom:1px}
|
||||
.field span{display:block;border-bottom:1px solid #aaa;padding-bottom:2px;min-height:18px;font-size:13px}
|
||||
.instructions-box{border:1px solid #bbb;padding:8px;min-height:64px;white-space:pre-wrap;font-size:12px}
|
||||
.sig-area{display:flex;gap:48px;margin-top:32px}
|
||||
.sig-line{border-top:1px solid #000;padding-top:3px;font-size:10px;color:#777;min-width:180px}
|
||||
@media print{body{margin:0}}
|
||||
</style>
|
||||
</head><body>${content.innerHTML}</body></html>`);
|
||||
win.document.close();
|
||||
win.focus();
|
||||
setTimeout(() => { win.print(); win.close(); }, 300);
|
||||
};
|
||||
|
||||
const patientName = `${patient.firstName} ${patient.lastName}`;
|
||||
const today = new Date().toLocaleDateString("en-US");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main modal */}
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FlaskConical className="h-5 w-5" />
|
||||
Lab RX — {patientName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="rx">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="rx">Create Lab RX</TabsTrigger>
|
||||
<TabsTrigger value="templates">Manage Templates</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab 1: pick template */}
|
||||
<TabsContent value="rx" className="space-y-4">
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Click a template to create a Lab RX
|
||||
</p>
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-gray-400">Loading templates…</p>
|
||||
) : templates.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No templates yet. Go to "Manage Templates" to add one.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{templates.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
onClick={() => openRx(t)}
|
||||
className="group relative border rounded-lg p-3 bg-white cursor-pointer hover:border-blue-400 hover:shadow-sm transition-all"
|
||||
>
|
||||
{renamingId === t.id ? (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") commitRename(); if (e.key === "Escape") cancelRename(); }}
|
||||
className="h-7 text-sm font-medium"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-green-600" onClick={commitRename} disabled={renameMutation.isPending}>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0" onClick={cancelRename}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-sm text-gray-900 truncate">{t.name}</p>
|
||||
{t.labName && <p className="text-xs text-gray-500 mt-0.5">{t.labName}</p>}
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-1 text-xs text-gray-400">
|
||||
{t.caseType && <span>{t.caseType}</span>}
|
||||
{t.material && <span>{t.material}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
title="Rename template"
|
||||
onClick={(e) => startRename(e, t)}
|
||||
className="shrink-0 p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-gray-100 transition-opacity"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab 2: manage templates */}
|
||||
<TabsContent value="templates" className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">{templates.length} template{templates.length !== 1 ? "s" : ""}</p>
|
||||
<Button size="sm" onClick={openAddTemplate} className="gap-1.5">
|
||||
<Plus className="h-4 w-4" /> Add Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showTemplateForm && (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-100 border-b px-4 py-2.5">
|
||||
<p className="text-sm font-semibold text-gray-800">
|
||||
{editingTemplateId !== null ? "Edit Template" : "New Template"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 space-y-5 bg-white">
|
||||
|
||||
{/* Template Name section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-600">Template Name</p>
|
||||
<div className="flex-1 h-px bg-blue-100" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Name <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
placeholder="e.g. Glidewell Crown, Argen Bridge…"
|
||||
value={templateForm.name}
|
||||
onChange={(e) => setTemplateForm((f) => ({ ...f, name: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-400">This name appears on the template picker button.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lab Information section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Lab Information</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Lab Name</Label>
|
||||
<Input placeholder="e.g. Glidewell Dental" value={templateForm.labName} onChange={(e) => setTemplateForm((f) => ({ ...f, labName: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Account #</Label>
|
||||
<Input placeholder="e.g. 123456" value={templateForm.labAccount} onChange={(e) => setTemplateForm((f) => ({ ...f, labAccount: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Phone</Label>
|
||||
<Input placeholder="(800) 000-0000" value={templateForm.labPhone} onChange={(e) => setTemplateForm((f) => ({ ...f, labPhone: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Fax</Label>
|
||||
<Input placeholder="(800) 000-0001" value={templateForm.labFax} onChange={(e) => setTemplateForm((f) => ({ ...f, labFax: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Address</Label>
|
||||
<Input placeholder="123 Lab St, City, State 00000" value={templateForm.labAddress} onChange={(e) => setTemplateForm((f) => ({ ...f, labAddress: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default Case Details section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Default Case Details</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Case Type</Label>
|
||||
<Select value={templateForm.caseType} onValueChange={(v) => setTemplateForm((f) => ({ ...f, caseType: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select…" /></SelectTrigger>
|
||||
<SelectContent>{CASE_TYPES.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Material</Label>
|
||||
<Select value={templateForm.material} onValueChange={(v) => setTemplateForm((f) => ({ ...f, material: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select…" /></SelectTrigger>
|
||||
<SelectContent>{MATERIALS.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Instructions</Label>
|
||||
<Textarea placeholder="Standard instructions for this lab…" value={templateForm.instructions} onChange={(e) => setTemplateForm((f) => ({ ...f, instructions: e.target.value }))} className="text-sm resize-none" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-t px-4 py-3 flex gap-2">
|
||||
<Button size="sm" onClick={handleSaveTemplate} disabled={isSaving}>
|
||||
<Check className="h-3.5 w-3.5 mr-1" />{isSaving ? "Saving…" : "Save Template"}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => { setShowTemplateForm(false); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); }} disabled={isSaving}>
|
||||
<X className="h-3.5 w-3.5 mr-1" /> Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-gray-400 py-4 text-center">Loading…</p>
|
||||
) : templates.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 py-8 text-center">No templates yet. Click "Add Template" to create one.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{templates.map((t, index) => (
|
||||
<div key={t.id} className="border rounded-lg p-3 bg-white flex items-center gap-2">
|
||||
<div className="flex flex-col shrink-0">
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-700 disabled:opacity-20"
|
||||
disabled={index === 0 || reorderMutation.isPending}
|
||||
onClick={() => moveTemplate(index, "up")}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-700 disabled:opacity-20"
|
||||
disabled={index === templates.length - 1 || reorderMutation.isPending}
|
||||
onClick={() => moveTemplate(index, "down")}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm text-gray-900">{t.name}</p>
|
||||
{t.labName && <p className="text-xs text-gray-500">{t.labName}</p>}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-0.5 text-xs text-gray-400 mt-0.5">
|
||||
{t.caseType && <span>Case: {t.caseType}</span>}
|
||||
{t.material && <span>Material: {t.material}</span>}
|
||||
{t.labPhone && <span>Ph: {t.labPhone}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEditTemplate(t)}>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-500" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => deleteMutation.mutate(t.id)} disabled={deleteMutation.isPending}>
|
||||
<Trash2 className="h-3.5 w-3.5 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* RX popup */}
|
||||
<Dialog open={!!rxTemplate} onOpenChange={(v) => { if (!v) setRxTemplate(null); }}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Printer className="h-5 w-5" />
|
||||
Lab RX — {rxTemplate?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{rxTemplate && (
|
||||
<>
|
||||
<div className="space-y-5 py-2">
|
||||
|
||||
{/* Tooth Number section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-600">Tooth Number</p>
|
||||
<div className="flex-1 h-px bg-blue-100" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Tooth Number(s)</Label>
|
||||
<Input placeholder="e.g. 3, 14, 19" value={rx.toothNumbers} onChange={(e) => setRx((r) => ({ ...r, toothNumbers: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Shade</Label>
|
||||
<Input placeholder="e.g. A2, B1" value={rx.shade} onChange={(e) => setRx((r) => ({ ...r, shade: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Age</Label>
|
||||
<Input placeholder="e.g. 72" value={rx.age} onChange={(e) => setRx((r) => ({ ...r, age: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Due Date</Label>
|
||||
<Input type="date" value={rx.dueDate} onChange={(e) => setRx((r) => ({ ...r, dueDate: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Doctor's Name</Label>
|
||||
<Select value={rx.doctorName} onValueChange={(v) => setRx((r) => ({ ...r, doctorName: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select provider…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers.map((p) => (
|
||||
<SelectItem key={p.id} value={p.providerName}>{p.providerName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Dental Office Name</Label>
|
||||
<Input placeholder="e.g. Summit Dental Care" value={rx.officeName} onChange={(e) => setRx((r) => ({ ...r, officeName: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Instructions</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
{rxTemplate.instructions && (
|
||||
<div className="rounded-md bg-gray-50 border px-3 py-2 text-xs text-gray-600 whitespace-pre-wrap">
|
||||
<span className="font-medium text-gray-400 block mb-1">From template</span>
|
||||
{rxTemplate.instructions}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Additional Notes</Label>
|
||||
<Textarea placeholder="Any extra instructions for this case…" value={rx.additionalNotes} onChange={(e) => setRx((r) => ({ ...r, additionalNotes: e.target.value }))} className="text-sm resize-none" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RX preview */}
|
||||
<div className="border rounded-lg p-4 bg-gray-50 text-sm space-y-3">
|
||||
<div className="text-center border-b pb-3">
|
||||
<p className="font-bold text-base">Dental Lab Prescription</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Date: {today}{rx.dueDate ? ` · Due: ${new Date(rx.dueDate + "T00:00").toLocaleDateString("en-US")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1">
|
||||
<div><span className="text-gray-400 text-xs">Patient</span><p className="font-medium">{patientName}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Age</span><p>{rx.age || "—"}</p></div>
|
||||
{rx.officeName && <div className="col-span-2"><span className="text-gray-400 text-xs">Dental Office</span><p className="font-medium">{rx.officeName}</p></div>}
|
||||
<div className="col-span-2 mt-1 border-t pt-2"><span className="text-gray-400 text-xs">Lab</span><p className="font-medium">{rxTemplate.labName || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Phone</span><p>{rxTemplate.labPhone || "—"}</p></div>
|
||||
{rxTemplate.labFax && <div><span className="text-gray-400 text-xs">Fax</span><p>{rxTemplate.labFax}</p></div>}
|
||||
{rxTemplate.labAddress && <div className="col-span-2"><span className="text-gray-400 text-xs">Address</span><p>{rxTemplate.labAddress}</p></div>}
|
||||
{rxTemplate.labAccount && <div><span className="text-gray-400 text-xs">Account #</span><p>{rxTemplate.labAccount}</p></div>}
|
||||
<div className="col-span-2 mt-1 border-t pt-2 grid grid-cols-2 gap-x-6">
|
||||
<div><span className="text-gray-400 text-xs">Case Type</span><p>{rxTemplate.caseType || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Material</span><p>{rxTemplate.material || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Tooth #</span><p>{rx.toothNumbers || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Shade</span><p>{rx.shade || "—"}</p></div>
|
||||
{rx.doctorName && <div><span className="text-gray-400 text-xs">Doctor</span><p>{rx.doctorName}</p></div>}
|
||||
</div>
|
||||
{(rxTemplate.instructions || rx.additionalNotes) && (
|
||||
<div className="col-span-2 mt-1 border-t pt-2">
|
||||
<span className="text-gray-400 text-xs">Instructions</span>
|
||||
<p className="whitespace-pre-wrap text-xs mt-1 text-gray-700">
|
||||
{[rxTemplate.instructions, rx.additionalNotes].filter(Boolean).join("\n\n")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Hidden print DOM */}
|
||||
<div className="hidden">
|
||||
<div ref={printRef}>
|
||||
<h1>Dental Lab Prescription</h1>
|
||||
<div className="subtitle">
|
||||
Date: {today}{rx.dueDate ? ` · Due: ${new Date(rx.dueDate + "T00:00").toLocaleDateString("en-US")}` : ""}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="section">
|
||||
<div className="section-title">Patient Information</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Patient Name</label><span>{patientName}</span></div>
|
||||
<div className="field"><label>Age</label><span>{rx.age}</span></div>
|
||||
<div className="field"><label>Dental Office</label><span>{rx.officeName}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Dental Lab</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Lab Name</label><span>{rxTemplate.labName ?? ""}</span></div>
|
||||
<div className="field"><label>Account #</label><span>{rxTemplate.labAccount ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Phone</label><span>{rxTemplate.labPhone ?? ""}</span></div>
|
||||
<div className="field"><label>Fax</label><span>{rxTemplate.labFax ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Address</label><span>{rxTemplate.labAddress ?? ""}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Prescription Details</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Case Type</label><span>{rxTemplate.caseType ?? ""}</span></div>
|
||||
<div className="field"><label>Material</label><span>{rxTemplate.material ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Tooth Number(s)</label><span>{rx.toothNumbers}</span></div>
|
||||
<div className="field"><label>Shade</label><span>{rx.shade}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Doctor's Name</label><span>{rx.doctorName}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Instructions</div>
|
||||
<div className="instructions-box">
|
||||
{[rxTemplate.instructions, rx.additionalNotes].filter(Boolean).join("\n\n")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sig-area">
|
||||
<div className="sig-line">Doctor Signature</div>
|
||||
<div className="sig-line">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRxTemplate(null)}>Close</Button>
|
||||
<Button onClick={handlePrint} className="gap-2">
|
||||
<Printer className="h-4 w-4" /> Print Lab RX
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Patient } from "@repo/db/types";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Plus, Pencil, Trash2, Printer, X, Check, FlaskConical, ChevronUp, ChevronDown } from "lucide-react";
|
||||
|
||||
type LabRxTemplate = {
|
||||
id: number;
|
||||
name: string;
|
||||
labName?: string | null;
|
||||
labPhone?: string | null;
|
||||
labFax?: string | null;
|
||||
labAddress?: string | null;
|
||||
labAccount?: string | null;
|
||||
caseType?: string | null;
|
||||
material?: string | null;
|
||||
instructions?: string | null;
|
||||
sortOrder: number;
|
||||
};
|
||||
|
||||
type TemplateFormState = {
|
||||
name: string;
|
||||
labName: string;
|
||||
labPhone: string;
|
||||
labFax: string;
|
||||
labAddress: string;
|
||||
labAccount: string;
|
||||
caseType: string;
|
||||
material: string;
|
||||
instructions: string;
|
||||
};
|
||||
|
||||
type RxFields = {
|
||||
toothNumbers: string;
|
||||
shade: string;
|
||||
dueDate: string;
|
||||
doctorName: string;
|
||||
age: string;
|
||||
officeName: string;
|
||||
additionalNotes: string;
|
||||
};
|
||||
|
||||
const EMPTY_TEMPLATE: TemplateFormState = {
|
||||
name: "", labName: "", labPhone: "", labFax: "",
|
||||
labAddress: "", labAccount: "", caseType: "", material: "", instructions: "Please make\nThank you!",
|
||||
};
|
||||
const EMPTY_RX: RxFields = { toothNumbers: "", shade: "", dueDate: "", doctorName: "", age: "", officeName: "", additionalNotes: "Please make\nThank you!" };
|
||||
|
||||
const calcAge = (dob: string | Date | null | undefined): string => {
|
||||
if (!dob) return "";
|
||||
const birth = new Date(dob);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
const m = today.getMonth() - birth.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
|
||||
return String(age);
|
||||
};
|
||||
|
||||
const CASE_TYPES = ["Crown", "Bridge", "Veneer", "Inlay/Onlay", "Full Denture", "Partial Denture", "Night Guard", "Retainer", "Implant Crown", "Other"];
|
||||
const MATERIALS = ["Zirconia", "PFM (Porcelain-Fused-to-Metal)", "E-Max", "Acrylic", "Cast Metal", "Composite", "PMMA", "Other"];
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
patient: Patient;
|
||||
}
|
||||
|
||||
export function LabRxModal({ open, onClose, patient }: Props) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const printRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// RX popup
|
||||
const [rxTemplate, setRxTemplate] = useState<LabRxTemplate | null>(null);
|
||||
const [rx, setRx] = useState<RxFields>(EMPTY_RX);
|
||||
|
||||
// Inline rename (Tab 1 cards)
|
||||
const [renamingId, setRenamingId] = useState<number | null>(null);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
|
||||
// Manage Templates form
|
||||
const [showTemplateForm, setShowTemplateForm] = useState(false);
|
||||
const [editingTemplateId, setEditingTemplateId] = useState<number | null>(null);
|
||||
const [templateForm, setTemplateForm] = useState<TemplateFormState>(EMPTY_TEMPLATE);
|
||||
|
||||
const { data: providers = [] } = useQuery<{ id: number; providerName: string }[]>({
|
||||
queryKey: ["/api/npiProviders"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/npiProviders");
|
||||
if (!res.ok) throw new Error("Failed to fetch providers");
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: officeContact } = useQuery<{ officeName?: string } | null>({
|
||||
queryKey: ["/api/office-contact"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/office-contact");
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: templates = [], isLoading } = useQuery<LabRxTemplate[]>({
|
||||
queryKey: ["/api/lab-rx/templates"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/lab-rx/templates");
|
||||
if (!res.ok) throw new Error("Failed to fetch");
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["/api/lab-rx/templates"] });
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data: TemplateFormState) => {
|
||||
const res = await apiRequest("POST", "/api/lab-rx/templates", data);
|
||||
if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to save"); }
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => { invalidate(); setShowTemplateForm(false); setTemplateForm(EMPTY_TEMPLATE); toast({ title: "Template saved" }); },
|
||||
onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async ({ id, data }: { id: number; data: Partial<TemplateFormState> }) => {
|
||||
const res = await apiRequest("PUT", `/api/lab-rx/templates/${id}`, data);
|
||||
if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to update"); }
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => { invalidate(); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); setShowTemplateForm(false); toast({ title: "Template updated" }); },
|
||||
onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: async ({ id, name }: { id: number; name: string }) => {
|
||||
const res = await apiRequest("PUT", `/api/lab-rx/templates/${id}`, { name });
|
||||
if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to rename"); }
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => { invalidate(); setRenamingId(null); },
|
||||
onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const res = await apiRequest("DELETE", `/api/lab-rx/templates/${id}`);
|
||||
if (!res.ok) throw new Error("Failed to delete");
|
||||
},
|
||||
onSuccess: () => { invalidate(); toast({ title: "Template deleted" }); },
|
||||
onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const reorderMutation = useMutation({
|
||||
mutationFn: async (orderedIds: number[]) => {
|
||||
const res = await apiRequest("POST", "/api/lab-rx/templates/reorder", { orderedIds });
|
||||
if (!res.ok) throw new Error("Failed to reorder");
|
||||
},
|
||||
onSuccess: () => invalidate(),
|
||||
onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const moveTemplate = (index: number, direction: "up" | "down") => {
|
||||
const swapIndex = direction === "up" ? index - 1 : index + 1;
|
||||
const reordered = [...templates];
|
||||
const a = reordered[index]!;
|
||||
const b = reordered[swapIndex]!;
|
||||
reordered[index] = b;
|
||||
reordered[swapIndex] = a;
|
||||
queryClient.setQueryData<LabRxTemplate[]>(["/api/lab-rx/templates"], reordered);
|
||||
reorderMutation.mutate(reordered.map((t) => t.id));
|
||||
};
|
||||
|
||||
const openRx = (t: LabRxTemplate) => {
|
||||
if (renamingId !== null) return;
|
||||
setRxTemplate(t);
|
||||
setRx({ ...EMPTY_RX, doctorName: providers[0]?.providerName ?? "", age: calcAge(patient.dateOfBirth), officeName: officeContact?.officeName ?? "" });
|
||||
};
|
||||
|
||||
const startRename = (e: React.MouseEvent, t: LabRxTemplate) => {
|
||||
e.stopPropagation();
|
||||
setRenamingId(t.id);
|
||||
setRenameValue(t.name);
|
||||
};
|
||||
|
||||
const commitRename = () => {
|
||||
if (!renameValue.trim()) { toast({ title: "Name cannot be empty", variant: "destructive" }); return; }
|
||||
if (renamingId !== null) renameMutation.mutate({ id: renamingId, name: renameValue.trim() });
|
||||
};
|
||||
|
||||
const cancelRename = () => setRenamingId(null);
|
||||
|
||||
const openAddTemplate = () => { setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); setShowTemplateForm(true); };
|
||||
const openEditTemplate = (t: LabRxTemplate) => {
|
||||
setEditingTemplateId(t.id);
|
||||
setTemplateForm({ name: t.name, labName: t.labName ?? "", labPhone: t.labPhone ?? "", labFax: t.labFax ?? "", labAddress: t.labAddress ?? "", labAccount: t.labAccount ?? "", caseType: t.caseType ?? "", material: t.material ?? "", instructions: t.instructions ?? "" });
|
||||
setShowTemplateForm(true);
|
||||
};
|
||||
const handleSaveTemplate = () => {
|
||||
if (!templateForm.name.trim()) { toast({ title: "Template name is required", variant: "destructive" }); return; }
|
||||
if (editingTemplateId !== null) updateMutation.mutate({ id: editingTemplateId, data: templateForm });
|
||||
else createMutation.mutate(templateForm);
|
||||
};
|
||||
const isSaving = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
const handlePrint = () => {
|
||||
const content = printRef.current;
|
||||
if (!content || !rxTemplate) return;
|
||||
const win = window.open("", "_blank", "width=800,height=900");
|
||||
if (!win) return;
|
||||
win.document.write(`<!DOCTYPE html><html><head><title>Lab RX</title>
|
||||
<style>
|
||||
body{font-family:Arial,sans-serif;margin:0;padding:24px;font-size:13px;color:#111}
|
||||
h1{font-size:22px;margin:0 0 4px;text-align:center}
|
||||
.subtitle{text-align:center;color:#555;font-size:12px;margin-bottom:12px}
|
||||
hr{border:none;border-top:2px solid #000;margin:10px 0}
|
||||
.section{margin-bottom:14px}
|
||||
.section-title{font-weight:bold;font-size:11px;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #bbb;margin-bottom:8px;padding-bottom:3px;color:#333}
|
||||
.row{display:flex;gap:24px;margin-bottom:6px}
|
||||
.field{flex:1}
|
||||
.field label{display:block;font-size:10px;color:#777;margin-bottom:1px}
|
||||
.field span{display:block;border-bottom:1px solid #aaa;padding-bottom:2px;min-height:18px;font-size:13px}
|
||||
.instructions-box{border:1px solid #bbb;padding:8px;min-height:64px;white-space:pre-wrap;font-size:12px}
|
||||
.sig-area{display:flex;gap:48px;margin-top:32px}
|
||||
.sig-line{border-top:1px solid #000;padding-top:3px;font-size:10px;color:#777;min-width:180px}
|
||||
@media print{body{margin:0}}
|
||||
</style>
|
||||
</head><body>${content.innerHTML}</body></html>`);
|
||||
win.document.close();
|
||||
win.focus();
|
||||
setTimeout(() => { win.print(); win.close(); }, 300);
|
||||
};
|
||||
|
||||
const patientName = `${patient.firstName} ${patient.lastName}`;
|
||||
const today = new Date().toLocaleDateString("en-US");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Main modal ── */}
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FlaskConical className="h-5 w-5" />
|
||||
Lab RX — {patientName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="rx">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="rx">Create Lab RX</TabsTrigger>
|
||||
<TabsTrigger value="templates">Manage Templates</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ── Tab 1: pick template ── */}
|
||||
<TabsContent value="rx" className="space-y-4">
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Click a template to create a Lab RX
|
||||
</p>
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-gray-400">Loading templates…</p>
|
||||
) : templates.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No templates yet. Go to "Manage Templates" to add one.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{templates.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
onClick={() => openRx(t)}
|
||||
className="group relative border rounded-lg p-3 bg-white cursor-pointer hover:border-blue-400 hover:shadow-sm transition-all"
|
||||
>
|
||||
{renamingId === t.id ? (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") commitRename(); if (e.key === "Escape") cancelRename(); }}
|
||||
className="h-7 text-sm font-medium"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-green-600" onClick={commitRename} disabled={renameMutation.isPending}>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0" onClick={cancelRename}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-sm text-gray-900 truncate">{t.name}</p>
|
||||
{t.labName && <p className="text-xs text-gray-500 mt-0.5">{t.labName}</p>}
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-1 text-xs text-gray-400">
|
||||
{t.caseType && <span>{t.caseType}</span>}
|
||||
{t.material && <span>{t.material}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
title="Rename template"
|
||||
onClick={(e) => startRename(e, t)}
|
||||
className="shrink-0 p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-gray-100 transition-opacity"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* ── Tab 2: manage templates ── */}
|
||||
<TabsContent value="templates" className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">{templates.length} template{templates.length !== 1 ? "s" : ""}</p>
|
||||
<Button size="sm" onClick={openAddTemplate} className="gap-1.5">
|
||||
<Plus className="h-4 w-4" /> Add Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showTemplateForm && (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-100 border-b px-4 py-2.5">
|
||||
<p className="text-sm font-semibold text-gray-800">
|
||||
{editingTemplateId !== null ? "Edit Template" : "New Template"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 space-y-5 bg-white">
|
||||
|
||||
{/* Template Name section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-600">Template Name</p>
|
||||
<div className="flex-1 h-px bg-blue-100" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Name <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
placeholder="e.g. Glidewell Crown, Argen Bridge…"
|
||||
value={templateForm.name}
|
||||
onChange={(e) => setTemplateForm((f) => ({ ...f, name: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-400">This name appears on the template picker button.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lab Information section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Lab Information</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Lab Name</Label>
|
||||
<Input placeholder="e.g. Glidewell Dental" value={templateForm.labName} onChange={(e) => setTemplateForm((f) => ({ ...f, labName: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Account #</Label>
|
||||
<Input placeholder="e.g. 123456" value={templateForm.labAccount} onChange={(e) => setTemplateForm((f) => ({ ...f, labAccount: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Phone</Label>
|
||||
<Input placeholder="(800) 000-0000" value={templateForm.labPhone} onChange={(e) => setTemplateForm((f) => ({ ...f, labPhone: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Fax</Label>
|
||||
<Input placeholder="(800) 000-0001" value={templateForm.labFax} onChange={(e) => setTemplateForm((f) => ({ ...f, labFax: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Address</Label>
|
||||
<Input placeholder="123 Lab St, City, State 00000" value={templateForm.labAddress} onChange={(e) => setTemplateForm((f) => ({ ...f, labAddress: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default Case Details section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Default Case Details</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Case Type</Label>
|
||||
<Select value={templateForm.caseType} onValueChange={(v) => setTemplateForm((f) => ({ ...f, caseType: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select…" /></SelectTrigger>
|
||||
<SelectContent>{CASE_TYPES.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Material</Label>
|
||||
<Select value={templateForm.material} onValueChange={(v) => setTemplateForm((f) => ({ ...f, material: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select…" /></SelectTrigger>
|
||||
<SelectContent>{MATERIALS.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Instructions</Label>
|
||||
<Textarea placeholder="Standard instructions for this lab…" value={templateForm.instructions} onChange={(e) => setTemplateForm((f) => ({ ...f, instructions: e.target.value }))} className="text-sm resize-none" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-t px-4 py-3 flex gap-2">
|
||||
<Button size="sm" onClick={handleSaveTemplate} disabled={isSaving}>
|
||||
<Check className="h-3.5 w-3.5 mr-1" />{isSaving ? "Saving…" : "Save Template"}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => { setShowTemplateForm(false); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); }} disabled={isSaving}>
|
||||
<X className="h-3.5 w-3.5 mr-1" /> Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-gray-400 py-4 text-center">Loading…</p>
|
||||
) : templates.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 py-8 text-center">No templates yet. Click "Add Template" to create one.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{templates.map((t, index) => (
|
||||
<div key={t.id} className="border rounded-lg p-3 bg-white flex items-center gap-2">
|
||||
<div className="flex flex-col shrink-0">
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-700 disabled:opacity-20"
|
||||
disabled={index === 0 || reorderMutation.isPending}
|
||||
onClick={() => moveTemplate(index, "up")}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-700 disabled:opacity-20"
|
||||
disabled={index === templates.length - 1 || reorderMutation.isPending}
|
||||
onClick={() => moveTemplate(index, "down")}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm text-gray-900">{t.name}</p>
|
||||
{t.labName && <p className="text-xs text-gray-500">{t.labName}</p>}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-0.5 text-xs text-gray-400 mt-0.5">
|
||||
{t.caseType && <span>Case: {t.caseType}</span>}
|
||||
{t.material && <span>Material: {t.material}</span>}
|
||||
{t.labPhone && <span>Ph: {t.labPhone}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEditTemplate(t)}>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-500" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => deleteMutation.mutate(t.id)} disabled={deleteMutation.isPending}>
|
||||
<Trash2 className="h-3.5 w-3.5 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* ── RX popup ── */}
|
||||
<Dialog open={!!rxTemplate} onOpenChange={(v) => { if (!v) setRxTemplate(null); }}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Printer className="h-5 w-5" />
|
||||
Lab RX — {rxTemplate?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{rxTemplate && (
|
||||
<>
|
||||
<div className="space-y-5 py-2">
|
||||
|
||||
{/* Tooth Number section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-600">Tooth Number</p>
|
||||
<div className="flex-1 h-px bg-blue-100" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Tooth Number(s)</Label>
|
||||
<Input placeholder="e.g. 3, 14, 19" value={rx.toothNumbers} onChange={(e) => setRx((r) => ({ ...r, toothNumbers: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Shade</Label>
|
||||
<Input placeholder="e.g. A2, B1" value={rx.shade} onChange={(e) => setRx((r) => ({ ...r, shade: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Age</Label>
|
||||
<Input placeholder="e.g. 72" value={rx.age} onChange={(e) => setRx((r) => ({ ...r, age: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Due Date</Label>
|
||||
<Input type="date" value={rx.dueDate} onChange={(e) => setRx((r) => ({ ...r, dueDate: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Doctor's Name</Label>
|
||||
<Select value={rx.doctorName} onValueChange={(v) => setRx((r) => ({ ...r, doctorName: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select provider…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers.map((p) => (
|
||||
<SelectItem key={p.id} value={p.providerName}>{p.providerName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Dental Office Name</Label>
|
||||
<Input placeholder="e.g. Summit Dental Care" value={rx.officeName} onChange={(e) => setRx((r) => ({ ...r, officeName: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Instructions</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
{rxTemplate.instructions && (
|
||||
<div className="rounded-md bg-gray-50 border px-3 py-2 text-xs text-gray-600 whitespace-pre-wrap">
|
||||
<span className="font-medium text-gray-400 block mb-1">From template</span>
|
||||
{rxTemplate.instructions}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Additional Notes</Label>
|
||||
<Textarea placeholder="Any extra instructions for this case…" value={rx.additionalNotes} onChange={(e) => setRx((r) => ({ ...r, additionalNotes: e.target.value }))} className="text-sm resize-none" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RX preview */}
|
||||
<div className="border rounded-lg p-4 bg-gray-50 text-sm space-y-3">
|
||||
<div className="text-center border-b pb-3">
|
||||
<p className="font-bold text-base">Dental Lab Prescription</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Date: {today}{rx.dueDate ? ` · Due: ${new Date(rx.dueDate + "T00:00").toLocaleDateString("en-US")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1">
|
||||
<div><span className="text-gray-400 text-xs">Patient</span><p className="font-medium">{patientName}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Age</span><p>{rx.age || "—"}</p></div>
|
||||
{rx.officeName && <div className="col-span-2"><span className="text-gray-400 text-xs">Dental Office</span><p className="font-medium">{rx.officeName}</p></div>}
|
||||
<div className="col-span-2 mt-1 border-t pt-2"><span className="text-gray-400 text-xs">Lab</span><p className="font-medium">{rxTemplate.labName || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Phone</span><p>{rxTemplate.labPhone || "—"}</p></div>
|
||||
{rxTemplate.labFax && <div><span className="text-gray-400 text-xs">Fax</span><p>{rxTemplate.labFax}</p></div>}
|
||||
{rxTemplate.labAddress && <div className="col-span-2"><span className="text-gray-400 text-xs">Address</span><p>{rxTemplate.labAddress}</p></div>}
|
||||
{rxTemplate.labAccount && <div><span className="text-gray-400 text-xs">Account #</span><p>{rxTemplate.labAccount}</p></div>}
|
||||
<div className="col-span-2 mt-1 border-t pt-2 grid grid-cols-2 gap-x-6">
|
||||
<div><span className="text-gray-400 text-xs">Case Type</span><p>{rxTemplate.caseType || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Material</span><p>{rxTemplate.material || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Tooth #</span><p>{rx.toothNumbers || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Shade</span><p>{rx.shade || "—"}</p></div>
|
||||
{rx.doctorName && <div><span className="text-gray-400 text-xs">Doctor</span><p>{rx.doctorName}</p></div>}
|
||||
</div>
|
||||
{(rxTemplate.instructions || rx.additionalNotes) && (
|
||||
<div className="col-span-2 mt-1 border-t pt-2">
|
||||
<span className="text-gray-400 text-xs">Instructions</span>
|
||||
<p className="whitespace-pre-wrap text-xs mt-1 text-gray-700">
|
||||
{[rxTemplate.instructions, rx.additionalNotes].filter(Boolean).join("\n\n")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Hidden print DOM */}
|
||||
<div className="hidden">
|
||||
<div ref={printRef}>
|
||||
<h1>Dental Lab Prescription</h1>
|
||||
<div className="subtitle">
|
||||
Date: {today}{rx.dueDate ? ` · Due: ${new Date(rx.dueDate + "T00:00").toLocaleDateString("en-US")}` : ""}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="section">
|
||||
<div className="section-title">Patient Information</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Patient Name</label><span>{patientName}</span></div>
|
||||
<div className="field"><label>Age</label><span>{rx.age}</span></div>
|
||||
<div className="field"><label>Dental Office</label><span>{rx.officeName}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Dental Lab</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Lab Name</label><span>{rxTemplate.labName ?? ""}</span></div>
|
||||
<div className="field"><label>Account #</label><span>{rxTemplate.labAccount ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Phone</label><span>{rxTemplate.labPhone ?? ""}</span></div>
|
||||
<div className="field"><label>Fax</label><span>{rxTemplate.labFax ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Address</label><span>{rxTemplate.labAddress ?? ""}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Prescription Details</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Case Type</label><span>{rxTemplate.caseType ?? ""}</span></div>
|
||||
<div className="field"><label>Material</label><span>{rxTemplate.material ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Tooth Number(s)</label><span>{rx.toothNumbers}</span></div>
|
||||
<div className="field"><label>Shade</label><span>{rx.shade}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Doctor's Name</label><span>{rx.doctorName}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Instructions</div>
|
||||
<div className="instructions-box">
|
||||
{[rxTemplate.instructions, rx.additionalNotes].filter(Boolean).join("\n\n")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sig-area">
|
||||
<div className="sig-line">Doctor Signature</div>
|
||||
<div className="sig-line">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRxTemplate(null)}>Close</Button>
|
||||
<Button onClick={handlePrint} className="gap-2">
|
||||
<Printer className="h-4 w-4" /> Print Lab RX
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -419,7 +419,7 @@ export function DdmaEligibilityButton({
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Delta MA
|
||||
Claim MD
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
Bot,
|
||||
X,
|
||||
ChevronRight,
|
||||
Stethoscope,
|
||||
Calendar,
|
||||
FileText,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
Paperclip,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useLocation } from "wouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
@@ -21,8 +19,6 @@ import { setChatbotPendingFiles } from "@/lib/chatbotFileStore";
|
||||
|
||||
type Step =
|
||||
| "menu"
|
||||
| "eligibility-input"
|
||||
| "eligibility-confirm"
|
||||
| "ai-loading"
|
||||
| "patient-found"
|
||||
| "eligibility-id-ready"
|
||||
@@ -53,12 +49,6 @@ interface PatientResult {
|
||||
dateOfBirth: string | null;
|
||||
}
|
||||
|
||||
interface EligibilityData {
|
||||
memberId: string;
|
||||
dob: string;
|
||||
dobISO: string;
|
||||
}
|
||||
|
||||
interface CheckAndClaimData {
|
||||
patient: PatientResult | null;
|
||||
memberId: string;
|
||||
@@ -75,40 +65,6 @@ function makeMsg(role: "bot" | "user", text: string, isLoading = false): Message
|
||||
return { id: ++msgCounter, role, text, isLoading };
|
||||
}
|
||||
|
||||
function getAutoCheck(dobISO: string): "mh" | "cmsp" {
|
||||
const [y, m, d] = dobISO.split("-").map(Number);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - (y ?? 0);
|
||||
const monthDiff = today.getMonth() + 1 - (m ?? 0);
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < (d ?? 0))) age--;
|
||||
return age >= 21 ? "mh" : "cmsp";
|
||||
}
|
||||
|
||||
function parseEligibilityInput(
|
||||
raw: string
|
||||
): { memberId: string; display: string; iso: string } | null {
|
||||
const dateMatch = raw.match(/\b(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})\b/);
|
||||
if (!dateMatch) return null;
|
||||
const m = dateMatch[1];
|
||||
const d = dateMatch[2];
|
||||
const y = dateMatch[3];
|
||||
if (!m || !d || !y) return null;
|
||||
const month = parseInt(m, 10);
|
||||
const day = parseInt(d, 10);
|
||||
const year = parseInt(y, 10);
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1900) return null;
|
||||
|
||||
const withoutDate = raw.replace(dateMatch[0], "");
|
||||
const memberId = (withoutDate.match(/[a-zA-Z0-9]/g) ?? []).join("");
|
||||
if (!memberId) return null;
|
||||
|
||||
return {
|
||||
memberId,
|
||||
display: `${m.padStart(2, "0")}/${d.padStart(2, "0")}/${y}`,
|
||||
iso: `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`,
|
||||
};
|
||||
}
|
||||
|
||||
const CHAT_STORAGE_KEY = "chatbot_messages";
|
||||
const CHATBOT_JOB_TS_KEY = "chatbot_job_started_at";
|
||||
|
||||
@@ -158,9 +114,6 @@ export function ChatbotButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [step, setStep] = useState<Step>("menu");
|
||||
const [messages, setMessages] = useState<Message[]>(loadSavedMessages);
|
||||
const [pasteInput, setPasteInput] = useState("");
|
||||
const [parseError, setParseError] = useState("");
|
||||
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
|
||||
const [freeTextInput, setFreeTextInput] = useState("");
|
||||
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
|
||||
const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null);
|
||||
@@ -206,7 +159,6 @@ export function ChatbotButton() {
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||
const [, setLocation] = useLocation();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const pasteRef = useRef<HTMLTextAreaElement>(null);
|
||||
const freeTextRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -244,9 +196,6 @@ export function ChatbotButton() {
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === "eligibility-input") {
|
||||
setTimeout(() => pasteRef.current?.focus(), 50);
|
||||
}
|
||||
if (step === "menu") {
|
||||
setTimeout(() => freeTextRef.current?.focus(), 50);
|
||||
}
|
||||
@@ -266,9 +215,6 @@ export function ChatbotButton() {
|
||||
// Resets step/data only — keeps message history
|
||||
const resetStep = () => {
|
||||
setStep("menu");
|
||||
setPasteInput("");
|
||||
setParseError("");
|
||||
setEligibilityData(null);
|
||||
setFreeTextInput("");
|
||||
setPatientResult(null);
|
||||
setEligibilityIdData(null);
|
||||
@@ -300,7 +246,7 @@ export function ChatbotButton() {
|
||||
resetStep();
|
||||
};
|
||||
|
||||
const handleOptionSelect = (option: "eligibility" | "schedule" | "claims") => {
|
||||
const handleOptionSelect = (option: "schedule" | "claims") => {
|
||||
if (option === "schedule") {
|
||||
addMsg("user", "Schedule an appointment");
|
||||
addMsg("bot", "Opening the appointments page...");
|
||||
@@ -309,67 +255,9 @@ export function ChatbotButton() {
|
||||
addMsg("user", "View claims");
|
||||
addMsg("bot", "Opening the claims page...");
|
||||
setTimeout(() => { setLocation("/claims"); setOpen(false); resetStep(); }, 600);
|
||||
} else if (option === "eligibility") {
|
||||
addMsg("user", "Check Eligibility");
|
||||
addMsg("bot", "Please enter the patient's Member ID and Date of Birth:");
|
||||
setStep("eligibility-input");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEligibilitySubmit = () => {
|
||||
const parsed = parseEligibilityInput(pasteInput);
|
||||
if (!parsed) {
|
||||
setParseError("Couldn't find both a Member ID and a date. Try: 123456789 01/15/1985");
|
||||
return;
|
||||
}
|
||||
setParseError("");
|
||||
const data: EligibilityData = { memberId: parsed.memberId, dob: parsed.display, dobISO: parsed.iso };
|
||||
setEligibilityData(data);
|
||||
addMsg("user", pasteInput.trim());
|
||||
addMsg("bot", `Ready to check MassHealth eligibility for:\n• Member ID: ${parsed.memberId}\n• Date of Birth: ${parsed.display}\n\nShall I proceed?`);
|
||||
setStep("eligibility-confirm");
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!eligibilityData) return;
|
||||
addMsg("user", "Yes, check now");
|
||||
addMsg("bot", "Opening the eligibility check page with this patient...");
|
||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
||||
memberId: eligibilityData.memberId,
|
||||
dob: eligibilityData.dobISO,
|
||||
autoCheck: getAutoCheck(eligibilityData.dobISO),
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
||||
};
|
||||
|
||||
const handleEligibilityFromPatient = () => {
|
||||
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
|
||||
addMsg("user", "Check eligibility now");
|
||||
addMsg("bot", "Opening the eligibility check page...");
|
||||
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
||||
};
|
||||
|
||||
const handleEligibilityAndAppointmentFromPatient = async () => {
|
||||
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
|
||||
addMsg("user", "Check eligibility & add to schedule (today)");
|
||||
addMsg("bot", "Creating appointment for today...", true);
|
||||
try {
|
||||
const res = await apiRequest("POST", "/api/ai/create-appointment-today", {
|
||||
patientId: patientResult.id,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
replaceLastMsg(data.message ?? "Could not create appointment.");
|
||||
return;
|
||||
}
|
||||
replaceLastMsg(`Appointment added at ${data.startTime} (${data.column ?? "Column A"}) — opening eligibility check page...`);
|
||||
} catch {
|
||||
replaceLastMsg("Could not create appointment — opening eligibility check page...");
|
||||
}
|
||||
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
||||
};
|
||||
|
||||
const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => {
|
||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck }));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
@@ -728,14 +616,7 @@ export function ChatbotButton() {
|
||||
|
||||
{/* Quick-action buttons (menu step) */}
|
||||
{step === "menu" && (
|
||||
<div className="grid grid-cols-2 gap-2 pt-1">
|
||||
<button
|
||||
onClick={() => handleOptionSelect("eligibility")}
|
||||
className="flex items-center gap-2 p-3 rounded-xl border border-primary/30 hover:bg-primary/5 text-sm text-left transition-colors"
|
||||
>
|
||||
<Stethoscope className="h-4 w-4 text-primary shrink-0" />
|
||||
<span>Check Eligibility</span>
|
||||
</button>
|
||||
<div className="grid grid-cols-1 gap-2 pt-1">
|
||||
<button
|
||||
onClick={() => handleOptionSelect("schedule")}
|
||||
className="flex items-center gap-2 p-3 rounded-xl border border-blue-300 hover:bg-blue-50 text-sm text-left transition-colors"
|
||||
@@ -745,7 +626,7 @@ export function ChatbotButton() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOptionSelect("claims")}
|
||||
className="flex items-center gap-2 p-3 rounded-xl border border-orange-300 hover:bg-orange-50 text-sm text-left transition-colors col-span-2"
|
||||
className="flex items-center gap-2 p-3 rounded-xl border border-orange-300 hover:bg-orange-50 text-sm text-left transition-colors"
|
||||
>
|
||||
<FileText className="h-4 w-4 text-orange-500 shrink-0" />
|
||||
<span>Claims</span>
|
||||
@@ -753,58 +634,6 @@ export function ChatbotButton() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Eligibility manual input */}
|
||||
{step === "eligibility-input" && (
|
||||
<div className="space-y-2 bg-gray-50 rounded-xl p-3 border">
|
||||
<Label className="text-xs text-gray-600">Paste Member ID and Date of Birth</Label>
|
||||
<textarea
|
||||
ref={pasteRef}
|
||||
rows={3}
|
||||
placeholder={"e.g. 123456789 01/15/1985\nor 01/15/1985 123456789"}
|
||||
value={pasteInput}
|
||||
onChange={(e) => { setPasteInput(e.target.value); setParseError(""); }}
|
||||
className={cn(
|
||||
"w-full rounded-md border bg-white px-3 py-2 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring",
|
||||
parseError && "border-red-400"
|
||||
)}
|
||||
/>
|
||||
{parseError && <p className="text-xs text-red-500">{parseError}</p>}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={handleEligibilitySubmit}
|
||||
disabled={!pasteInput.trim()}
|
||||
>
|
||||
Continue <ChevronRight className="h-3 w-3 ml-1" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Eligibility confirm */}
|
||||
{step === "eligibility-confirm" && (
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" className="flex-1 h-8 text-xs" onClick={handleConfirm}>
|
||||
Yes, Check Now
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 text-xs"
|
||||
onClick={() => {
|
||||
setStep("eligibility-input");
|
||||
setMessages((prev) => prev.slice(0, -2));
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI-found patient card */}
|
||||
{step === "patient-found" && patientResult && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 space-y-2">
|
||||
@@ -821,26 +650,6 @@ export function ChatbotButton() {
|
||||
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
{patientResult.insuranceId && patientResult.dateOfBirth && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full h-8 text-xs bg-primary hover:bg-primary/90"
|
||||
onClick={handleEligibilityFromPatient}
|
||||
>
|
||||
<Stethoscope className="h-3 w-3 mr-1" />
|
||||
Check Eligibility
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full h-8 text-xs bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||
onClick={handleEligibilityAndAppointmentFromPatient}
|
||||
>
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
Eligibility & Appointment Today
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
Building2,
|
||||
Timer,
|
||||
BookOpen,
|
||||
MonitorCog,
|
||||
GraduationCap,
|
||||
ShoppingCart,
|
||||
Search,
|
||||
KeyRound,
|
||||
@@ -169,6 +171,11 @@ export function Sidebar() {
|
||||
path: "/cloud-storage",
|
||||
icon: <Cloud className="h-5 w-5 text-sky-500" />,
|
||||
},
|
||||
{
|
||||
name: "Dental Education",
|
||||
path: "/dental-education",
|
||||
icon: <GraduationCap className="h-5 w-5 text-lime-600" />,
|
||||
},
|
||||
{
|
||||
name: "AI Input Agent",
|
||||
path: "/ai-input-agent",
|
||||
@@ -285,6 +292,11 @@ export function Sidebar() {
|
||||
path: "/settings/aichat",
|
||||
icon: <Bot className="h-4 w-4 text-gray-400" />,
|
||||
},
|
||||
{
|
||||
name: "Selenium Settings",
|
||||
path: "/settings/selenium",
|
||||
icon: <MonitorCog className="h-4 w-4 text-gray-400" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -46,7 +46,7 @@ export function TopAppBar() {
|
||||
</svg>
|
||||
|
||||
<h1 className="text-lg font-medium text-primary">
|
||||
My Dental Office Management
|
||||
Wellspring Acupuncture Office Management
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
|
||||
export function SeleniumSettingsCard() {
|
||||
const { toast } = useToast();
|
||||
const [paymentGroupId, setPaymentGroupId] = useState("");
|
||||
|
||||
const { data: settings, isLoading } = useQuery({
|
||||
queryKey: ["/api/selenium-settings"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/selenium-settings");
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setPaymentGroupId(settings.paymentGroupId ?? "");
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async (data) => {
|
||||
const res = await apiRequest("PUT", "/api/selenium-settings", data);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => null);
|
||||
throw new Error(err?.message || "Failed to save Selenium settings");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/selenium-settings"] });
|
||||
toast({ title: "Selenium Settings Saved" });
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({ title: "Error", description: err?.message || "Failed to save settings", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-6">
|
||||
<p className="text-sm text-gray-500">Loading...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="space-y-6 py-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold">Selenium Settings</h3>
|
||||
{settings?.paymentGroupId && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-600 font-medium">
|
||||
<CheckCircle className="h-3.5 w-3.5" /> Configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Configure values used by Selenium automation workers (United eligibility, claim, and pre-auth).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveMutation.mutate({ paymentGroupId });
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Payment Group ID
|
||||
</label>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
The office name as it appears in the United/DentalHub portal dropdown (e.g. "Summit Dental Care" or "Broadway Dental").
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={paymentGroupId}
|
||||
onChange={(e) => setPaymentGroupId(e.target.value)}
|
||||
className="mt-1 p-2 border rounded w-full"
|
||||
placeholder="e.g. Summit Dental Care"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-teal-600 text-white px-4 py-2 rounded hover:bg-teal-700 disabled:opacity-50"
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? "Saving..." : "Save Settings"}
|
||||
</button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
|
||||
type SeleniumSettings = {
|
||||
paymentGroupId: string;
|
||||
};
|
||||
|
||||
export function SeleniumSettingsCard() {
|
||||
const { toast } = useToast();
|
||||
const [paymentGroupId, setPaymentGroupId] = useState("");
|
||||
|
||||
const { data: settings, isLoading } = useQuery<SeleniumSettings | null>({
|
||||
queryKey: ["/api/selenium-settings"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/selenium-settings");
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setPaymentGroupId(settings.paymentGroupId ?? "");
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async (data: SeleniumSettings) => {
|
||||
const res = await apiRequest("PUT", "/api/selenium-settings", data);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => null);
|
||||
throw new Error(err?.message || "Failed to save Selenium settings");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/selenium-settings"] });
|
||||
toast({ title: "Selenium Settings Saved" });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({ title: "Error", description: err?.message || "Failed to save settings", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-6">
|
||||
<p className="text-sm text-gray-500">Loading...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="space-y-6 py-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold">Selenium Settings</h3>
|
||||
{settings?.paymentGroupId && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-600 font-medium">
|
||||
<CheckCircle className="h-3.5 w-3.5" /> Configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Configure values used by Selenium automation workers (United eligibility, claim, and pre-auth).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveMutation.mutate({ paymentGroupId });
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Payment Group ID
|
||||
</label>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
The office name as it appears in the United/DentalHub portal dropdown (e.g. "Summit Dental Care" or "Broadway Dental").
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={paymentGroupId}
|
||||
onChange={(e) => setPaymentGroupId(e.target.value)}
|
||||
className="mt-1 p-2 border rounded w-full"
|
||||
placeholder="e.g. Summit Dental Care"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-teal-600 text-white px-4 py-2 rounded hover:bg-teal-700 disabled:opacity-50"
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? "Saving..." : "Save Settings"}
|
||||
</button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,27 @@ export function DateInput({
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleBlur = () => {
|
||||
// If the user finished typing a 2-digit year (e.g. "02/12/03") the live-typing
|
||||
// logic below never commits it, since it waits for a 4-digit year. On blur,
|
||||
// expand a trailing 2-digit year so the date isn't silently dropped.
|
||||
const match = inputValue.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/);
|
||||
if (!match) return;
|
||||
|
||||
const [, mmStr, ddStr, yyStr] = match;
|
||||
const mm = mmStr.padStart(2, "0");
|
||||
const dd = ddStr.padStart(2, "0");
|
||||
const currentYearLastTwo = new Date().getFullYear() % 100;
|
||||
const yy = parseInt(yyStr, 10);
|
||||
const yyyy = yy <= currentYearLastTwo ? 2000 + yy : 1900 + yy;
|
||||
|
||||
const parsed = new Date(yyyy, +mm - 1, +dd);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
setInputValue(format(parsed, "MM/dd/yyyy"));
|
||||
onChange(parsed);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const raw = e.target.value;
|
||||
|
||||
@@ -148,6 +169,7 @@ export function DateInput({
|
||||
placeholder="MM/DD/YYYY"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export type LabRxTemplateConfig = {
|
||||
id: string;
|
||||
name: string;
|
||||
labName?: string;
|
||||
labPhone?: string;
|
||||
labFax?: string;
|
||||
labAddress?: string;
|
||||
labAccount?: string;
|
||||
caseType?: string;
|
||||
material?: string;
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
export const LAB_RX_TEMPLATES: LabRxTemplateConfig[] = [
|
||||
{
|
||||
id: "highland-crown",
|
||||
name: "Highland Dental Crown",
|
||||
labName: "Highland Dental Studio",
|
||||
labPhone: "781-552-5198",
|
||||
labAddress: "87 Cambridge Street, Burlington, MA 01803",
|
||||
caseType: "Crown",
|
||||
material: "Zirconia",
|
||||
},
|
||||
{
|
||||
id: "highland-full-denture",
|
||||
name: "Highland Dental Full Denture",
|
||||
labName: "Highland Dental Studio",
|
||||
labPhone: "781-552-5198",
|
||||
labAddress: "87 Cambridge Street, Burlington, MA 01803",
|
||||
caseType: "Full Denture",
|
||||
},
|
||||
{
|
||||
id: "highland-partial-denture",
|
||||
name: "Highland Dental Partial Denture",
|
||||
labName: "Highland Dental Studio",
|
||||
labPhone: "781-552-5198",
|
||||
labAddress: "87 Cambridge Street, Burlington, MA 01803",
|
||||
caseType: "Partial Denture",
|
||||
},
|
||||
];
|
||||
@@ -1120,22 +1120,10 @@ export default function AppointmentsPage() {
|
||||
const patient = apt ? patientsFromDay.find((p) => p.id === apt.patientId) : null;
|
||||
const insuranceProvider = (patient as any)?.insuranceProvider as string | null ?? null;
|
||||
const p = insuranceProvider?.toLowerCase() ?? "";
|
||||
const isMassHealth = p.includes("masshealth");
|
||||
|
||||
let autoCheck = "other-providers";
|
||||
|
||||
if (isMassHealth) {
|
||||
const dob = (patient as any)?.dateOfBirth;
|
||||
let age: number | null = null;
|
||||
if (dob) {
|
||||
const dobDate = typeof dob === "string" ? new Date(dob) : dob as Date;
|
||||
const today = new Date();
|
||||
age = today.getFullYear() - dobDate.getFullYear();
|
||||
const m = today.getMonth() - dobDate.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < dobDate.getDate())) age--;
|
||||
}
|
||||
autoCheck = age !== null && age >= 21 ? "mh-history" : "cmsp";
|
||||
} else if (p.includes("delta dental ma")) {
|
||||
if (p.includes("delta dental ma")) {
|
||||
autoCheck = "ddma";
|
||||
} else if (p.includes("delta dental ins")) {
|
||||
autoCheck = "delta-ins";
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function AuthPage() {
|
||||
<Card className="p-8 bg-white">
|
||||
<div className="mb-10 text-center">
|
||||
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
|
||||
My Dental Office Management
|
||||
Wellspring Acupuncture Office Management
|
||||
</h1>
|
||||
<p className="text-sm font-semibold tracking-[0.15em] bg-clip-text text-transparent animate-gradient-flash"
|
||||
style={{ backgroundImage: "linear-gradient(90deg, #7c3aed, #d946ef, #06b6d4, #7c3aed)", backgroundSize: "200% 100%" }}>
|
||||
|
||||
@@ -143,10 +143,12 @@ export default function ClaimsPage() {
|
||||
// CCA result: pdfFileId is already saved by the processor — open preview directly
|
||||
if (jobResult.pdfFileId) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
setPreviewPdfId(jobResult.pdfFileId);
|
||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
const advancedToNext = advanceChatbotClaimQueue();
|
||||
if (!advancedToNext) {
|
||||
setPreviewPdfId(jobResult.pdfFileId);
|
||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
dispatch(setTaskStatus({
|
||||
key: "claimSubmit",
|
||||
status: "success",
|
||||
@@ -336,7 +338,9 @@ export default function ClaimsPage() {
|
||||
|
||||
// Advance the chatbot_claim_queue / chatbot_check_claim_queue after selenium PDF is saved.
|
||||
// NOT called from closeClaim — called only after the selenium job completes and PDF is downloaded.
|
||||
const advanceChatbotClaimQueue = () => {
|
||||
// Returns true if it navigated on to another queued patient (caller should skip the PDF
|
||||
// preview popup in that case — the form is about to be replaced by the next patient's claim).
|
||||
const advanceChatbotClaimQueue = (): boolean => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
||||
if (raw) {
|
||||
@@ -369,7 +373,7 @@ export default function ClaimsPage() {
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setWouterLocation("/insurance-status");
|
||||
}, 500);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||
}
|
||||
@@ -403,11 +407,13 @@ export default function ClaimsPage() {
|
||||
setTimeout(() => {
|
||||
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
||||
}, 500);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
sessionStorage.removeItem("chatbot_claim_queue");
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Advance the ai_claim_queue by removing the first item (current patient).
|
||||
@@ -447,10 +453,13 @@ export default function ClaimsPage() {
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
||||
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
||||
if (!isDraft) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
}
|
||||
// NOTE: advanceChatbotClaimQueue() is NOT called here. This function only creates
|
||||
// the claim DB record; for selenium-based insurances (MH/CCA/DDMA/Tufts/United) the
|
||||
// actual submission + PDF happens afterward. Advancing the queue here would move to
|
||||
// the next patient before the current patient's selenium job (and PDF) finishes.
|
||||
// The queue is advanced later, once the selenium job completes (see
|
||||
// handleMHSeleniumPdfDownload and the CCA branch in the job-status useEffect).
|
||||
if (!isDraft) advanceAiClaimQueue();
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -964,13 +973,15 @@ export default function ClaimsPage() {
|
||||
duration: isPreAuth ? 10000 : 5000,
|
||||
});
|
||||
|
||||
let advancedToNext = false;
|
||||
if (!isPreAuth) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
advancedToNext = advanceChatbotClaimQueue();
|
||||
}
|
||||
|
||||
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
||||
if (result.pdfFileId) {
|
||||
// (skip if we're about to navigate to the next queued patient's claim)
|
||||
if (result.pdfFileId && !advancedToNext) {
|
||||
setPreviewPdfId(result.pdfFileId);
|
||||
setPreviewFallbackFilename(result.fileName ?? null);
|
||||
setPreviewOpen(true);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
|
||||
export default function DentalEducationPage() {
|
||||
return (
|
||||
<div className="container mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Dental Education</h1>
|
||||
<p className="text-muted-foreground">Educational resources for dental staff and patients.</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GraduationCap className="h-5 w-5 text-lime-600" />
|
||||
Coming Soon
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Dental education content will be available here.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,13 +28,12 @@ import {
|
||||
clearTaskStatus,
|
||||
} from "@/redux/slices/seleniumTaskSlice";
|
||||
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
||||
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
||||
import { parseLocalDate } from "@/utils/dateUtils";
|
||||
import { socket } from "@/lib/socket";
|
||||
import { InsertPatient, Patient } from "@repo/db/types";
|
||||
import { DateInput } from "@/components/ui/dateInput";
|
||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||
import { DualPdfPreviewModal } from "@/components/insurance-status/dual-pdf-preview-modal";
|
||||
import { useLocation } from "wouter";
|
||||
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
||||
import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltains-button-modal";
|
||||
@@ -44,6 +43,26 @@ import { CCAEligibilityButton } from "@/components/insurance-status/cca-button-m
|
||||
import { BcbsMaEligibilityButton } from "@/components/insurance-status/bcbs-ma-button-modal";
|
||||
import { useLicense } from "@/hooks/use-license";
|
||||
|
||||
const INSURANCE_PROVIDERS = [
|
||||
"Aetna",
|
||||
"BMC HealthNet Plan",
|
||||
"CIGNA",
|
||||
"Cenpatico Massachusetts",
|
||||
"Commonwealth Care Alliance",
|
||||
"Harvard Pilgrim Health Care",
|
||||
"Humana",
|
||||
"MA BCBS",
|
||||
"MA Medicaid-MassHealth",
|
||||
"Mass Advantage",
|
||||
"Mass General Brigham Health Plan",
|
||||
"Senior Whole Health of Massachusetts",
|
||||
"Tricare East",
|
||||
"Tufts Health Plan",
|
||||
"UMR - Wausau",
|
||||
"United Health Care",
|
||||
"Wellcare",
|
||||
];
|
||||
|
||||
/**
|
||||
* Waits for a Selenium job to complete by racing socket.io events against
|
||||
* HTTP polling every 3 seconds. This ensures the result is received even
|
||||
@@ -110,16 +129,8 @@ export default function InsuranceStatusPage() {
|
||||
const [dateOfBirth, setDateOfBirth] = useState<Date | null>(null);
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [insuranceProvider, setInsuranceProvider] = useState("");
|
||||
const isFormIncomplete = !memberId || !dateOfBirth;
|
||||
const [isCheckingEligibilityStatus, setIsCheckingEligibilityStatus] =
|
||||
useState(false);
|
||||
const [isCheckingEligibilityAppointment, setIsCheckingEligibilityAppointment] =
|
||||
useState(false);
|
||||
const [isCheckingEligibilityClaimsPreAuth, setIsCheckingEligibilityClaimsPreAuth] =
|
||||
useState(false);
|
||||
const [isCheckingEligibilityHistory, setIsCheckingEligibilityHistory] =
|
||||
useState(false);
|
||||
const [isCheckingCMSP, setIsCheckingCMSP] = useState(false);
|
||||
|
||||
// AI Call Insurance section
|
||||
const [aiCallOpen, setAiCallOpen] = useState(false);
|
||||
@@ -147,27 +158,7 @@ export default function InsuranceStatusPage() {
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Dual PDF modal state (used by MH Eligibility & History)
|
||||
const [dualPreviewOpen, setDualPreviewOpen] = useState(false);
|
||||
const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState<number | null>(null);
|
||||
const [dualEligibilityFilename, setDualEligibilityFilename] = useState<string | null>(null);
|
||||
const [dualMemberDetailsPdfId, setDualMemberDetailsPdfId] = useState<number | null>(null);
|
||||
const [dualMemberDetailsFilename, setDualMemberDetailsFilename] = useState<string | null>(null);
|
||||
const [dualHistoryPdfId, setDualHistoryPdfId] = useState<number | null>(null);
|
||||
const [dualHistoryFilename, setDualHistoryFilename] = useState<string | null>(null);
|
||||
|
||||
// CMSP PDF modal state (used by CMSP Eligibility & History & Remaining)
|
||||
const [cmspPreviewOpen, setCmspPreviewOpen] = useState(false);
|
||||
const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState<number | null>(null);
|
||||
const [cmspEligibilityFilename, setCmspEligibilityFilename] = useState<string | null>(null);
|
||||
const [cmspMemberDetailsPdfId, setCmspMemberDetailsPdfId] = useState<number | null>(null);
|
||||
const [cmspMemberDetailsFilename, setCmspMemberDetailsFilename] = useState<string | null>(null);
|
||||
const [cmspHistoryPdfId, setCmspHistoryPdfId] = useState<number | null>(null);
|
||||
const [cmspHistoryFilename, setCmspHistoryFilename] = useState<string | null>(null);
|
||||
const [cmspAccumulatorPdfId, setCmspAccumulatorPdfId] = useState<number | null>(null);
|
||||
const [cmspAccumulatorFilename, setCmspAccumulatorFilename] = useState<string | null>(null);
|
||||
|
||||
const pendingAutoCheck = useRef<"mh" | "mh-history" | "cmsp" | "ddma" | "delta-ins" | "united-sco" | "tufts-sco" | "cca" | null>(null);
|
||||
const pendingAutoCheck = useRef<"ddma" | "delta-ins" | "united-sco" | "tufts-sco" | "cca" | null>(null);
|
||||
const [triggerTarget, setTriggerTarget] = useState<string | null>(null);
|
||||
const pendingScrollTo = useRef<string | null>(null);
|
||||
const [prefillTick, setPrefillTick] = useState(0);
|
||||
@@ -206,6 +197,11 @@ export default function InsuranceStatusPage() {
|
||||
setMemberId(selectedPatient.insuranceId ?? "");
|
||||
setFirstName(selectedPatient.firstName ?? "");
|
||||
setLastName(selectedPatient.lastName ?? "");
|
||||
setInsuranceProvider(
|
||||
INSURANCE_PROVIDERS.includes(selectedPatient.insuranceProvider ?? "")
|
||||
? selectedPatient.insuranceProvider!
|
||||
: "",
|
||||
);
|
||||
|
||||
const dob =
|
||||
typeof selectedPatient.dateOfBirth === "string"
|
||||
@@ -217,9 +213,34 @@ export default function InsuranceStatusPage() {
|
||||
setFirstName("");
|
||||
setLastName("");
|
||||
setDateOfBirth(null);
|
||||
setInsuranceProvider("");
|
||||
}
|
||||
}, [selectedPatient]);
|
||||
|
||||
// Save the chosen insurance provider on the patient record once an
|
||||
// eligibility check succeeds, so it shows in the patient table.
|
||||
const saveInsuranceProvider = async () => {
|
||||
if (!insuranceProvider) return;
|
||||
try {
|
||||
let patientId = selectedPatient?.id ?? null;
|
||||
if (!patientId && memberId) {
|
||||
const res = await apiRequest(
|
||||
"GET",
|
||||
`/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(memberId)}`,
|
||||
);
|
||||
if (res.ok) patientId = (await res.json())?.id ?? null;
|
||||
}
|
||||
if (!patientId) return;
|
||||
const res = await apiRequest("PUT", `/api/patients/${patientId}`, {
|
||||
insuranceProvider,
|
||||
});
|
||||
if (!res.ok) return;
|
||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
} catch {
|
||||
// non-critical: eligibility result is already available
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-lookup patient by member ID when typed manually
|
||||
useEffect(() => {
|
||||
if (selectedPatient && memberId === (selectedPatient.insuranceId ?? "")) return;
|
||||
@@ -280,56 +301,6 @@ export default function InsuranceStatusPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const isUnder14 = (dob: Date): boolean => {
|
||||
const cutoff = new Date(dob);
|
||||
cutoff.setFullYear(cutoff.getFullYear() + 14);
|
||||
return new Date() < cutoff;
|
||||
};
|
||||
|
||||
// Shared: run MH eligibility selenium job, return jobResult or throw
|
||||
const runMHEligibilitySelenium = async (): Promise<any> => {
|
||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||
|
||||
const data = {
|
||||
memberId,
|
||||
dateOfBirth: formattedDob,
|
||||
insuranceSiteKey: "MH",
|
||||
firstName: firstName || undefined,
|
||||
lastName: lastName || undefined,
|
||||
};
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
key: "eligibilityCheck",
|
||||
status: "pending",
|
||||
message: "Sending Data to Selenium...",
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await apiRequest(
|
||||
"POST",
|
||||
"/api/insurance-status/eligibility-check",
|
||||
{ data: data, socketId: socket.id },
|
||||
);
|
||||
const enqueueResult = await response.json();
|
||||
if (enqueueResult.error) throw new Error(enqueueResult.error);
|
||||
|
||||
const jobId = enqueueResult.jobId;
|
||||
if (!jobId) throw new Error("No jobId returned from server");
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
key: "eligibilityCheck",
|
||||
status: "pending",
|
||||
message: "Selenium browser starting...",
|
||||
}),
|
||||
);
|
||||
|
||||
return waitForSeleniumJob(jobId, (msg) =>
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: msg }))
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddPatient = async () => {
|
||||
const newPatient: InsertPatient = {
|
||||
firstName,
|
||||
@@ -343,347 +314,13 @@ export default function InsuranceStatusPage() {
|
||||
await addPatientMutation.mutateAsync(newPatient);
|
||||
};
|
||||
|
||||
// MH Eligibility — check eligibility, save to DB, open PDF preview
|
||||
const handleMHEligibilityButton = async () => {
|
||||
if (!memberId || !dateOfBirth) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description: "Please fill in all the required fields: Member ID, Date of Birth.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUnder14(dateOfBirth)) {
|
||||
toast({ title: "CMSP Patient", description: "Patient is under 14 — running CMSP Eligibility & History & Remaining instead.", variant: "default" });
|
||||
return handleCMSPButton();
|
||||
}
|
||||
|
||||
setIsCheckingEligibilityStatus(true);
|
||||
try {
|
||||
const jobResult = await runMHEligibilitySelenium();
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
key: "eligibilityCheck",
|
||||
status: "success",
|
||||
message: "Patient status is updated, and its eligibility pdf is uploaded at Document Page.",
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Selenium service done.",
|
||||
description: "Your Patient Eligibility is fetched and updated, Kindly search through the patient.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setSelectedPatient(null);
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
void tryAppointmentFromChatbot();
|
||||
|
||||
if (jobResult.pdfFileId) {
|
||||
setPreviewPdfId(Number(jobResult.pdfFileId));
|
||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
processNextInQueue();
|
||||
} catch (error: any) {
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||
} finally {
|
||||
setIsCheckingEligibilityStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
// MH Eligibility & Appointment — check eligibility, save to DB, create appointment for today
|
||||
const handleMHEligibilityAppointmentButton = async () => {
|
||||
if (!memberId || !dateOfBirth) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description: "Please fill in all the required fields: Member ID, Date of Birth.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingEligibilityAppointment(true);
|
||||
try {
|
||||
const jobResult = await runMHEligibilitySelenium();
|
||||
|
||||
const isInactive = jobResult?.patientUpdateStatus?.includes("INACTIVE");
|
||||
|
||||
if (isInactive) {
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
key: "eligibilityCheck",
|
||||
status: "error",
|
||||
message: "Insurance is inactive. No appointment created.",
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Insurance Inactive",
|
||||
description: "Patient insurance is inactive. No appointment was created.",
|
||||
variant: "destructive",
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
|
||||
// Look up the patient (created/updated by the Selenium worker) then create today's appointment
|
||||
let apptTime: string | null = null;
|
||||
try {
|
||||
const lookupRes = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(memberId)}`);
|
||||
if (lookupRes.ok) {
|
||||
const patient = await lookupRes.json();
|
||||
if (patient?.id) {
|
||||
const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id });
|
||||
if (apptRes.ok) {
|
||||
const apptData = await apptRes.json();
|
||||
apptTime = apptData.startTime ? `${apptData.startTime} (${apptData.column ?? "Column A"})` : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
key: "eligibilityCheck",
|
||||
status: "success",
|
||||
message: apptTime
|
||||
? `Eligibility checked. Appointment created at ${apptTime}.`
|
||||
: "Eligibility checked. Could not create appointment — please add manually.",
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Eligibility checked.",
|
||||
description: apptTime
|
||||
? `Patient added to today's schedule at ${apptTime}.`
|
||||
: "Eligibility saved. Could not create appointment — Column A may be full.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setSelectedPatient(null);
|
||||
|
||||
if (jobResult.pdfFileId) {
|
||||
setPreviewPdfId(Number(jobResult.pdfFileId));
|
||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||
} finally {
|
||||
setIsCheckingEligibilityAppointment(false);
|
||||
}
|
||||
};
|
||||
|
||||
// MH Eligibility & Claims/PreAuth — check eligibility, save to DB, navigate to claims
|
||||
const handleMHEligibilityClaimsPreAuthButton = async () => {
|
||||
if (!memberId || !dateOfBirth) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description: "Please fill in all the required fields: Member ID, Date of Birth.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingEligibilityClaimsPreAuth(true);
|
||||
try {
|
||||
await runMHEligibilitySelenium();
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
key: "eligibilityCheck",
|
||||
status: "success",
|
||||
message: "Eligibility checked and saved. Redirecting to Claims/PreAuth...",
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Eligibility checked.",
|
||||
description: "Patient eligibility saved. Redirecting to Claims/PreAuth.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setSelectedPatient(null);
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
setLocation("/claims");
|
||||
} catch (error: any) {
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||
} finally {
|
||||
setIsCheckingEligibilityClaimsPreAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
// MH Eligibility & History — eligibility check + service history, saves both PDFs, auto-downloads eligibility PDF only
|
||||
const handleMHEligibilityHistoryButton = async () => {
|
||||
if (!memberId || !dateOfBirth) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description: "Please fill in all the required fields: Member ID, Date of Birth.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUnder14(dateOfBirth)) {
|
||||
toast({ title: "CMSP Patient", description: "Patient is under 14 — running CMSP Eligibility & History & Remaining instead.", variant: "default" });
|
||||
return handleCMSPButton();
|
||||
}
|
||||
|
||||
setIsCheckingEligibilityHistory(true);
|
||||
try {
|
||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||
const data = {
|
||||
memberId,
|
||||
dateOfBirth: formattedDob,
|
||||
insuranceSiteKey: "MH",
|
||||
firstName: firstName || undefined,
|
||||
lastName: lastName || undefined,
|
||||
};
|
||||
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Sending Data to Selenium..." }));
|
||||
|
||||
const response = await apiRequest(
|
||||
"POST",
|
||||
"/api/insurance-status/eligibility-history-check",
|
||||
{ data, socketId: socket.id },
|
||||
);
|
||||
const enqueueResult = await response.json();
|
||||
if (enqueueResult.error) throw new Error(enqueueResult.error);
|
||||
|
||||
const jobId = enqueueResult.jobId;
|
||||
if (!jobId) throw new Error("No jobId returned from server");
|
||||
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Selenium browser starting..." }));
|
||||
|
||||
const jobResult = await waitForSeleniumJob(jobId, (msg) =>
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: msg }))
|
||||
);
|
||||
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "Eligibility and service history PDFs saved to Documents." }));
|
||||
toast({
|
||||
title: "Done",
|
||||
description: "Eligibility and service history PDFs saved. Eligibility PDF downloading now.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
void tryAppointmentFromChatbot();
|
||||
if (claimed) return;
|
||||
setSelectedPatient(null);
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
processNextInQueue();
|
||||
|
||||
// Open all PDFs side by side in the modal
|
||||
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
|
||||
setDualEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
|
||||
setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||
setDualMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
|
||||
setDualMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `eligibility_member_details_${memberId}.pdf`);
|
||||
setDualHistoryPdfId(jobResult.historyPdfFileId ? Number(jobResult.historyPdfFileId) : null);
|
||||
setDualHistoryFilename(jobResult.historyPdfFilename ?? `eligibility_history_${memberId}.pdf`);
|
||||
setDualPreviewOpen(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||
} finally {
|
||||
setIsCheckingEligibilityHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
// CMSP Eligibility & History & Remaining — eligibility + service history + accumulator PDFs
|
||||
const handleCMSPButton = async () => {
|
||||
if (!memberId || !dateOfBirth) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description: "Please fill in Member ID and Date of Birth.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingCMSP(true);
|
||||
try {
|
||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||
const data = {
|
||||
memberId,
|
||||
dateOfBirth: formattedDob,
|
||||
insuranceSiteKey: "MH",
|
||||
firstName: firstName || undefined,
|
||||
lastName: lastName || undefined,
|
||||
};
|
||||
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Sending Data to Selenium..." }));
|
||||
|
||||
const response = await apiRequest(
|
||||
"POST",
|
||||
"/api/insurance-status/cmsp-eligibility-history-remaining-check",
|
||||
{ data, socketId: socket.id },
|
||||
);
|
||||
const enqueueResult = await response.json();
|
||||
if (enqueueResult.error) throw new Error(enqueueResult.error);
|
||||
|
||||
const jobId = enqueueResult.jobId;
|
||||
if (!jobId) throw new Error("No jobId returned from server");
|
||||
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Selenium browser starting..." }));
|
||||
|
||||
const jobResult = await waitForSeleniumJob(jobId, (msg) =>
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: msg }))
|
||||
);
|
||||
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "CMSP PDFs saved to Documents." }));
|
||||
toast({
|
||||
title: "Done",
|
||||
description: "Eligibility, history, and accumulator PDFs saved.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setSelectedPatient(null);
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
processNextInQueue();
|
||||
|
||||
// Open 4-panel modal
|
||||
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
|
||||
setCmspEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
|
||||
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
|
||||
setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
|
||||
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);
|
||||
setCmspHistoryPdfId(jobResult.historyPdfFileId ? Number(jobResult.historyPdfFileId) : null);
|
||||
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `cmsp_history_${memberId}.pdf`);
|
||||
setCmspAccumulatorPdfId(jobResult.accumulatorPdfFileId ? Number(jobResult.accumulatorPdfFileId) : null);
|
||||
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `cmsp_accumulator_${memberId}.pdf`);
|
||||
setCmspPreviewOpen(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||
} finally {
|
||||
setIsCheckingCMSP(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-trigger after prefill (from chatbot or schedule page "Check Eligibility")
|
||||
useEffect(() => {
|
||||
if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return;
|
||||
const check = pendingAutoCheck.current;
|
||||
pendingAutoCheck.current = null;
|
||||
if (check === "mh" || check === "mh-history") {
|
||||
if (isUnder14(dateOfBirth)) {
|
||||
handleCMSPButton();
|
||||
} else {
|
||||
handleMHEligibilityHistoryButton();
|
||||
}
|
||||
} else if (check === "cmsp") {
|
||||
handleCMSPButton();
|
||||
} else {
|
||||
// Button-component-handled providers: pass target to their autoTrigger prop
|
||||
setTriggerTarget(check);
|
||||
}
|
||||
// Button-component-handled providers: pass target to their autoTrigger prop
|
||||
setTriggerTarget(check);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [memberId, dateOfBirth, prefillTick]);
|
||||
|
||||
@@ -820,7 +457,7 @@ export default function InsuranceStatusPage() {
|
||||
const autoCheck = params.get("autoCheck") as typeof pendingAutoCheck.current;
|
||||
const scrollTo = params.get("scrollTo");
|
||||
|
||||
const knownAutoChecks = ["mh", "mh-history", "cmsp", "ddma", "delta-ins", "united-sco", "tufts-sco", "cca"] as const;
|
||||
const knownAutoChecks = ["ddma", "delta-ins", "united-sco", "tufts-sco", "cca"] as const;
|
||||
if (autoCheck && (knownAutoChecks as readonly string[]).includes(autoCheck)) {
|
||||
pendingAutoCheck.current = autoCheck;
|
||||
} else if (scrollTo) {
|
||||
@@ -932,100 +569,25 @@ export default function InsuranceStatusPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-2 gap-4">
|
||||
<Button
|
||||
onClick={() => handleMHEligibilityButton()}
|
||||
className="w-full"
|
||||
disabled={isCheckingEligibilityStatus}
|
||||
>
|
||||
{isCheckingEligibilityStatus ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
MH Eligibility
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleMHEligibilityAppointmentButton()}
|
||||
className="w-full"
|
||||
disabled={isCheckingEligibilityAppointment}
|
||||
>
|
||||
{isCheckingEligibilityAppointment ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
MH Eligibility&Appointment
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleMHEligibilityClaimsPreAuthButton()}
|
||||
className="w-full"
|
||||
disabled={isCheckingEligibilityClaimsPreAuth}
|
||||
>
|
||||
{isCheckingEligibilityClaimsPreAuth ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
MH Eligibility&Claims/PreAuth
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-2 gap-4 mt-4">
|
||||
<Button
|
||||
id="eligibility-mh-history"
|
||||
className="w-full"
|
||||
disabled={isCheckingEligibilityHistory}
|
||||
onClick={() => handleMHEligibilityHistoryButton()}
|
||||
>
|
||||
{isCheckingEligibilityHistory ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
MH Eligibility & History
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
id="eligibility-cmsp"
|
||||
className="w-full"
|
||||
disabled={isCheckingCMSP}
|
||||
onClick={() => handleCMSPButton()}
|
||||
>
|
||||
{isCheckingCMSP ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
CMSP Eligibility & History & Remaining
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="grid grid-cols-4 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="insuranceProvider">Insurance Provider</Label>
|
||||
<Select
|
||||
value={insuranceProvider}
|
||||
onValueChange={setInsuranceProvider}
|
||||
>
|
||||
<SelectTrigger id="insuranceProvider">
|
||||
<SelectValue placeholder="Select insurance provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INSURANCE_PROVIDERS.map((name) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TEMP PROVIDER BUTTONS */}
|
||||
@@ -1046,6 +608,7 @@ export default function InsuranceStatusPage() {
|
||||
autoTrigger={triggerTarget === "ddma"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
void saveInsuranceProvider();
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
void tryAppointmentFromChatbot();
|
||||
if (!claimed) {
|
||||
@@ -1068,6 +631,7 @@ export default function InsuranceStatusPage() {
|
||||
autoTrigger={triggerTarget === "delta-ins"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
void saveInsuranceProvider();
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
void tryAppointmentFromChatbot();
|
||||
if (!claimed) {
|
||||
@@ -1088,6 +652,7 @@ export default function InsuranceStatusPage() {
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
void saveInsuranceProvider();
|
||||
void tryAppointmentFromChatbot();
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
@@ -1112,6 +677,7 @@ export default function InsuranceStatusPage() {
|
||||
autoTrigger={triggerTarget === "tufts-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
void saveInsuranceProvider();
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
void tryAppointmentFromChatbot();
|
||||
if (!claimed) {
|
||||
@@ -1134,6 +700,7 @@ export default function InsuranceStatusPage() {
|
||||
autoTrigger={triggerTarget === "united-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
void saveInsuranceProvider();
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
void tryAppointmentFromChatbot();
|
||||
if (!claimed) {
|
||||
@@ -1156,6 +723,7 @@ export default function InsuranceStatusPage() {
|
||||
autoTrigger={triggerTarget === "cca"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
void saveInsuranceProvider();
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
void tryAppointmentFromChatbot();
|
||||
if (!claimed) {
|
||||
@@ -1169,81 +737,6 @@ export default function InsuranceStatusPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
United AAPR
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Aetna
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Altus
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Metlife Dental
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Cigna
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Delta WA
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Row 5 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Delta IL
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Others
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1394,78 +887,6 @@ export default function InsuranceStatusPage() {
|
||||
autoDownload
|
||||
/>
|
||||
|
||||
{/* 4-panel modal for CMSP — eligibility, member details, history, accumulator */}
|
||||
<DualPdfPreviewModal
|
||||
open={cmspPreviewOpen}
|
||||
onClose={() => {
|
||||
setCmspPreviewOpen(false);
|
||||
setCmspEligibilityPdfId(null);
|
||||
setCmspEligibilityFilename(null);
|
||||
setCmspMemberDetailsPdfId(null);
|
||||
setCmspMemberDetailsFilename(null);
|
||||
setCmspHistoryPdfId(null);
|
||||
setCmspHistoryFilename(null);
|
||||
setCmspAccumulatorPdfId(null);
|
||||
setCmspAccumulatorFilename(null);
|
||||
}}
|
||||
title="CMSP Eligibility, History & Remaining"
|
||||
panels={[
|
||||
{
|
||||
pdfId: cmspEligibilityPdfId,
|
||||
fallbackFilename: cmspEligibilityFilename,
|
||||
label: "Eligibility",
|
||||
autoDownload: true,
|
||||
},
|
||||
{
|
||||
pdfId: cmspMemberDetailsPdfId,
|
||||
fallbackFilename: cmspMemberDetailsFilename,
|
||||
label: "Member Details",
|
||||
},
|
||||
{
|
||||
pdfId: cmspHistoryPdfId,
|
||||
fallbackFilename: cmspHistoryFilename,
|
||||
label: "Service History",
|
||||
},
|
||||
{
|
||||
pdfId: cmspAccumulatorPdfId,
|
||||
fallbackFilename: cmspAccumulatorFilename,
|
||||
label: "Accumulator (Remaining)",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 3-panel modal for MH Eligibility & History */}
|
||||
<DualPdfPreviewModal
|
||||
open={dualPreviewOpen}
|
||||
onClose={() => {
|
||||
setDualPreviewOpen(false);
|
||||
setDualEligibilityPdfId(null);
|
||||
setDualEligibilityFilename(null);
|
||||
setDualMemberDetailsPdfId(null);
|
||||
setDualMemberDetailsFilename(null);
|
||||
setDualHistoryPdfId(null);
|
||||
setDualHistoryFilename(null);
|
||||
}}
|
||||
title="MH Eligibility & Service History"
|
||||
panels={[
|
||||
{
|
||||
pdfId: dualEligibilityPdfId,
|
||||
fallbackFilename: dualEligibilityFilename,
|
||||
label: "Eligibility",
|
||||
autoDownload: true,
|
||||
},
|
||||
{
|
||||
pdfId: dualMemberDetailsPdfId,
|
||||
fallbackFilename: dualMemberDetailsFilename,
|
||||
label: "Member Details",
|
||||
},
|
||||
{
|
||||
pdfId: dualHistoryPdfId,
|
||||
fallbackFilename: dualHistoryFilename,
|
||||
label: "Service History",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
||||
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
||||
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
||||
import { AiChatSettingsCard } from "@/components/settings/ai-chat-settings-card";
|
||||
import { SeleniumSettingsCard } from "@/components/settings/selenium-settings-card";
|
||||
export default function SettingsPage() {
|
||||
const { toast } = useToast();
|
||||
const { user: currentUser } = useAuth();
|
||||
@@ -232,6 +233,8 @@ export default function SettingsPage() {
|
||||
return <ProcedureTimeslotCard />;
|
||||
case "insurancecontact":
|
||||
return <InsuranceContactCard />;
|
||||
case "selenium":
|
||||
return <SeleniumSettingsCard />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
||||
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
||||
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
||||
import { AiChatSettingsCard } from "@/components/settings/ai-chat-settings-card";
|
||||
import { SeleniumSettingsCard } from "@/components/settings/selenium-settings-card";
|
||||
|
||||
type SectionId =
|
||||
| "staff"
|
||||
@@ -34,7 +35,8 @@ type SectionId =
|
||||
| "officehours"
|
||||
| "officecontact"
|
||||
| "proceduretimeslot"
|
||||
| "insurancecontact";
|
||||
| "insurancecontact"
|
||||
| "selenium";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { toast } = useToast();
|
||||
@@ -276,6 +278,9 @@ export default function SettingsPage() {
|
||||
case "insurancecontact":
|
||||
return <InsuranceContactCard />;
|
||||
|
||||
case "selenium":
|
||||
return <SeleniumSettingsCard />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,15 +6,13 @@ import uvicorn
|
||||
import asyncio
|
||||
from selenium_claimSubmitWorker import AutomationMassHealthClaimsLogin
|
||||
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
||||
from selenium_MH_eligibilityHistoryCheckWorker import AutomationMassHealthEligibilityHistoryCheck
|
||||
from selenium_CMSP_eligibilityHistoryRemainingCheckWorker import AutomationCMSPEligibilityHistoryRemainingCheck
|
||||
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
||||
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
||||
from selenium_MHSinglePaymentCheckWorker import AutomationMassHealthSinglePaymentCheck
|
||||
from selenium_MHBatchPaymentCheckWorker import AutomationMassHealthBatchPaymentCheck
|
||||
import os
|
||||
import time
|
||||
import helpers_ddma_eligibility as hddma
|
||||
import helpers_claimmd_eligibility as hddma
|
||||
import helpers_deltains_eligibility as hdeltains
|
||||
import helpers_unitedsco_eligibility as hunitedsco
|
||||
import helpers_dentaquest_eligibility as hdentaquest
|
||||
@@ -29,7 +27,7 @@ import helpers_tuftssco_preauth as htuftssco_preauth
|
||||
import helpers_bcbs_ma_eligibility as hbcbs_ma
|
||||
|
||||
# Import startup session-clear functions
|
||||
from ddma_browser_manager import clear_ddma_session_on_startup
|
||||
from claimmd_browser_manager import clear_ddma_session_on_startup
|
||||
from deltains_browser_manager import clear_deltains_session_on_startup
|
||||
from unitedsco_browser_manager import clear_unitedsco_session_on_startup
|
||||
from dentaquest_browser_manager import clear_dentaquest_session_on_startup
|
||||
@@ -146,83 +144,6 @@ async def start_workflow(request: Request):
|
||||
async with lock:
|
||||
active_jobs -= 1
|
||||
|
||||
# Endpoint: 2a — MH Eligibility + Service History
|
||||
@app.post("/mh-eligibility-history-check")
|
||||
async def mh_eligibility_history_check(request: Request):
|
||||
global active_jobs, waiting_jobs
|
||||
data = await request.json()
|
||||
|
||||
async with lock:
|
||||
waiting_jobs += 1
|
||||
|
||||
async with semaphore:
|
||||
async with lock:
|
||||
waiting_jobs -= 1
|
||||
active_jobs += 1
|
||||
try:
|
||||
bot = AutomationMassHealthEligibilityHistoryCheck(data)
|
||||
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
|
||||
|
||||
if result.get("status") == "error":
|
||||
return {"status": "error", "message": result.get("message")}
|
||||
|
||||
port = os.getenv("PORT", "5002")
|
||||
url_host = os.getenv("HOST", "localhost")
|
||||
|
||||
if result.get("pdf_path"):
|
||||
filename = os.path.basename(result["pdf_path"])
|
||||
result["pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}"
|
||||
|
||||
if result.get("history_pdf_path"):
|
||||
filename = os.path.basename(result["history_pdf_path"])
|
||||
result["history_pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}"
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
finally:
|
||||
async with lock:
|
||||
active_jobs -= 1
|
||||
|
||||
# Endpoint: 2b — CMSP Eligibility + Service History + Accumulator (Remaining)
|
||||
@app.post("/cmsp-eligibility-history-remaining-check")
|
||||
async def cmsp_eligibility_history_remaining_check(request: Request):
|
||||
global active_jobs, waiting_jobs
|
||||
data = await request.json()
|
||||
|
||||
async with lock:
|
||||
waiting_jobs += 1
|
||||
|
||||
async with semaphore:
|
||||
async with lock:
|
||||
waiting_jobs -= 1
|
||||
active_jobs += 1
|
||||
try:
|
||||
bot = AutomationCMSPEligibilityHistoryRemainingCheck(data)
|
||||
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
|
||||
|
||||
if result.get("status") == "error":
|
||||
return {"status": "error", "message": result.get("message")}
|
||||
|
||||
port = os.getenv("PORT", "5002")
|
||||
url_host = os.getenv("HOST", "localhost")
|
||||
|
||||
for key, url_key in [
|
||||
("pdf_path", "pdf_url"),
|
||||
("history_pdf_path", "history_pdf_url"),
|
||||
("accumulator_pdf_path", "accumulator_pdf_url"),
|
||||
]:
|
||||
if result.get(key):
|
||||
filename = os.path.basename(result[key])
|
||||
result[url_key] = f"http://{url_host}:{port}/downloads/{filename}"
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
finally:
|
||||
async with lock:
|
||||
active_jobs -= 1
|
||||
|
||||
# Endpoint: 2.1 — Start the automation for Claims login (open browser and log in)
|
||||
@app.post("/claims-login")
|
||||
async def start_claims_login(request: Request):
|
||||
@@ -410,7 +331,7 @@ async def ddma_eligibility(request: Request):
|
||||
waiting_jobs += 1
|
||||
|
||||
# run in background (queued under semaphore)
|
||||
asyncio.create_task(_ddma_worker_wrapper(sid, data, url="https://providers.deltadentalma.com/onboarding/start/"))
|
||||
asyncio.create_task(_ddma_worker_wrapper(sid, data, url="https://www.deltadentalma.com/sign-in"))
|
||||
|
||||
return {"status": "started", "session_id": sid}
|
||||
|
||||
|
||||
+9
-13
@@ -7,7 +7,7 @@ from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||
|
||||
from selenium_DDMA_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
||||
from selenium_ClaimMD_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
||||
|
||||
# In-memory session store
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
@@ -146,13 +146,11 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
||||
if otp_value:
|
||||
print(f"[OTP poll {poll+1}] OTP received from app, typing it in...")
|
||||
try:
|
||||
otp_input = driver.find_element(By.XPATH,
|
||||
"//input[contains(@aria-label,'Verification') or contains(@placeholder,'verification') or @type='tel' or contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code')]"
|
||||
)
|
||||
otp_input = driver.find_element(By.XPATH, "//input[@data-test-id='MfaCode']")
|
||||
otp_input.clear()
|
||||
otp_input.send_keys(otp_value)
|
||||
try:
|
||||
verify_btn = driver.find_element(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
|
||||
verify_btn = driver.find_element(By.XPATH, "//button[@data-test-id='ConfirmSubmitButton']")
|
||||
verify_btn.click()
|
||||
except Exception:
|
||||
otp_input.send_keys("\n")
|
||||
@@ -169,7 +167,7 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
||||
if "member" in current_url or "dashboard" in current_url or "eligibility" in current_url:
|
||||
try:
|
||||
member_search = WebDriverWait(driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
print("[OTP] Member search found — login successful!")
|
||||
login_success = True
|
||||
@@ -179,16 +177,14 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
||||
|
||||
# Check if OTP input is still visible (user hasn't finished)
|
||||
try:
|
||||
otp_input_elem = driver.find_element(By.XPATH,
|
||||
"//input[contains(@aria-label,'Verification') or contains(@placeholder,'verification') or @type='tel']"
|
||||
)
|
||||
otp_input_elem = driver.find_element(By.XPATH, "//input[@data-test-id='MfaCode']")
|
||||
print(f"[OTP poll {poll+1}] OTP input still visible - waiting...")
|
||||
except Exception:
|
||||
# OTP input gone — may mean login is completing; try members page
|
||||
if "onboarding" in current_url or "start" in current_url:
|
||||
if "sign-in" in current_url:
|
||||
print("[OTP] OTP input gone, trying to navigate to members page...")
|
||||
try:
|
||||
driver.get("https://providers.deltadentalma.com/members")
|
||||
driver.get("https://www.deltadentalma.com/private/provider/dashboard")
|
||||
await asyncio.sleep(2)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -200,10 +196,10 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
||||
# Final check — navigate directly to members page
|
||||
try:
|
||||
print("[OTP] Final attempt - navigating to members page...")
|
||||
driver.get("https://providers.deltadentalma.com/members")
|
||||
driver.get("https://www.deltadentalma.com/private/provider/dashboard")
|
||||
await asyncio.sleep(3)
|
||||
member_search = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
print("[OTP] Member search found — login successful!")
|
||||
login_success = True
|
||||
@@ -8,7 +8,7 @@ from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||
|
||||
from selenium_DDMA_claimSubmitWorker import AutomationDDMAClaimSubmit
|
||||
from ddma_browser_manager import get_browser_manager
|
||||
from claimmd_browser_manager import get_browser_manager
|
||||
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
@@ -56,7 +56,7 @@ async def _remove_session_later(sid: str, delay: int = 20):
|
||||
|
||||
async def start_ddma_claim_run(sid: str, data: dict, url: str):
|
||||
"""
|
||||
Run the DDMA claim workflow — mirrors helpers_ddma_eligibility exactly for
|
||||
Run the DDMA claim workflow — mirrors helpers_claimmd_eligibility exactly for
|
||||
login/OTP, then continues into claim-specific steps.
|
||||
|
||||
OTP handling uses two complementary strategies:
|
||||
@@ -321,7 +321,7 @@ async def start_ddma_claim_run(sid: str, data: dict, url: str):
|
||||
|
||||
# Close the browser window after claim (session preserved in profile)
|
||||
try:
|
||||
from ddma_browser_manager import get_browser_manager
|
||||
from claimmd_browser_manager import get_browser_manager
|
||||
get_browser_manager().quit_driver()
|
||||
print("[DDMA Claim] Browser closed - session preserved in profile")
|
||||
except Exception as close_err:
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
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, Select
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
import time
|
||||
import os
|
||||
import base64
|
||||
|
||||
|
||||
class AutomationCMSPEligibilityHistoryRemainingCheck:
|
||||
def __init__(self, data):
|
||||
self.headless = False
|
||||
self.driver = None
|
||||
self.extracted_data = {}
|
||||
self.eligibility_tab = None
|
||||
self.member_details_tab = None
|
||||
self.accumulator_tab = None
|
||||
|
||||
self.data = data.get("data")
|
||||
|
||||
self.massdhp_username = self.data.get("massdhpUsername", "")
|
||||
self.massdhp_password = self.data.get("massdhpPassword", "")
|
||||
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||
self.memberId = self.data.get("memberId", "")
|
||||
self.firstName = self.data.get("firstName", "")
|
||||
self.lastName = self.data.get("lastName", "")
|
||||
|
||||
# Convert dateOfBirth from YYYY-MM-DD to MMDDYYYY
|
||||
if self.dateOfBirth and "-" in self.dateOfBirth:
|
||||
parts = self.dateOfBirth.split("-")
|
||||
if len(parts) == 3:
|
||||
year, month, day = parts
|
||||
self.dateOfBirth = f"{month.zfill(2)}{day.zfill(2)}{year}"
|
||||
|
||||
self.download_dir = os.path.abspath("downloads")
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
|
||||
# ── driver ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def config_driver(self):
|
||||
options = webdriver.ChromeOptions()
|
||||
if self.headless:
|
||||
options.add_argument("--headless")
|
||||
|
||||
prefs = {
|
||||
"download.default_directory": self.download_dir,
|
||||
"plugins.always_open_pdf_externally": False,
|
||||
"download.prompt_for_download": False,
|
||||
"download.directory_upgrade": True,
|
||||
}
|
||||
options.add_experimental_option("prefs", prefs)
|
||||
|
||||
s = Service(ChromeDriverManager().install())
|
||||
self.driver = webdriver.Chrome(service=s, options=options)
|
||||
|
||||
# ── login (same MassHealth credentials) ─────────────────────────────────────
|
||||
|
||||
def login(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
try:
|
||||
signin_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a.btn.btn-block.btn-primary[href='https://connectsso.masshealth-dental.org/mhprovider/index.html']")
|
||||
)
|
||||
)
|
||||
signin_button.click()
|
||||
time.sleep(3)
|
||||
|
||||
email_field = wait.until(EC.presence_of_element_located((By.ID, "User")))
|
||||
email_field.clear()
|
||||
email_field.send_keys(self.massdhp_username)
|
||||
|
||||
password_field = wait.until(EC.presence_of_element_located((By.ID, "Password")))
|
||||
password_field.clear()
|
||||
password_field.send_keys(self.massdhp_password)
|
||||
|
||||
login_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "input[type='submit'][name='submit'][value='Login']")
|
||||
)
|
||||
)
|
||||
login_button.click()
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[login] Error: {e}")
|
||||
return "ERROR:LOGIN FAILED"
|
||||
|
||||
# ── step 1 — search member, extract eligibility data ────────────────────────
|
||||
|
||||
def step1(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
substep = "init"
|
||||
try:
|
||||
substep = "patient_management"
|
||||
patient_mgmt = wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//strong[@translate='Patient Management']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView(true);", patient_mgmt)
|
||||
self.driver.execute_script("arguments[0].click();", patient_mgmt)
|
||||
time.sleep(2)
|
||||
|
||||
substep = "member_eligibility_link"
|
||||
eligibility_link = wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//a[@translate='Member Eligibility']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].click();", eligibility_link)
|
||||
time.sleep(2)
|
||||
|
||||
substep = "provider_dropdown"
|
||||
provider_dropdown = wait.until(
|
||||
EC.presence_of_element_located((By.NAME, "provider"))
|
||||
)
|
||||
select_provider = Select(provider_dropdown)
|
||||
first_option = next(
|
||||
(o for o in select_provider.options if o.get_attribute("value").strip()),
|
||||
select_provider.options[0]
|
||||
)
|
||||
select_provider.select_by_value(first_option.get_attribute("value"))
|
||||
time.sleep(2)
|
||||
|
||||
substep = "member_dob"
|
||||
member_dob = wait.until(
|
||||
EC.presence_of_all_elements_located((By.NAME, "dateInput"))
|
||||
)[1]
|
||||
member_dob.clear()
|
||||
member_dob.send_keys(self.dateOfBirth)
|
||||
|
||||
substep = "member_number"
|
||||
member_number = wait.until(
|
||||
EC.presence_of_element_located((By.NAME, "memberNumber"))
|
||||
)
|
||||
member_number.clear()
|
||||
member_number.send_keys(self.memberId)
|
||||
|
||||
substep = "search_button"
|
||||
search_button = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary')]"))
|
||||
)
|
||||
search_button.click()
|
||||
|
||||
substep = "wait_results"
|
||||
wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']")
|
||||
)
|
||||
)
|
||||
|
||||
self.extracted_data = self._extract_data_from_page()
|
||||
print(f"[step1] data extracted: {self.extracted_data}")
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[step1] FAILED at substep='{substep}': {e}")
|
||||
return f"ERROR:STEP1:{substep}"
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _cell_text(self, cell):
|
||||
text = cell.text.strip()
|
||||
if not text:
|
||||
try:
|
||||
text = (self.driver.execute_script("return arguments[0].innerText;", cell) or "").strip()
|
||||
except Exception:
|
||||
pass
|
||||
return text
|
||||
|
||||
def _normalize_id(self, s):
|
||||
return "".join(c for c in str(s) if c.isalnum()).lower()
|
||||
|
||||
def _extract_data_from_page(self):
|
||||
wait = WebDriverWait(self.driver, 15)
|
||||
try:
|
||||
wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr")
|
||||
)
|
||||
)
|
||||
for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]:
|
||||
rows = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
f"//h4[text()='{status_label}']/following::table[1]/tbody/tr"
|
||||
)
|
||||
for row in rows:
|
||||
cells = row.find_elements(By.TAG_NAME, "td")
|
||||
if len(cells) < 3:
|
||||
continue
|
||||
norm_cell = self._normalize_id(self._cell_text(cells[2]))
|
||||
norm_self = self._normalize_id(self.memberId)
|
||||
if norm_self and norm_cell and (norm_self in norm_cell or norm_cell in norm_self):
|
||||
full_name = self._cell_text(cells[4]) if len(cells) > 4 else ""
|
||||
plan_name = (
|
||||
self._cell_text(cells[6]) if len(cells) > 6
|
||||
else (self._cell_text(cells[-1]) if len(cells) > 4 else "")
|
||||
)
|
||||
name_parts = full_name.split()
|
||||
return {
|
||||
"eligibility": elig_flag,
|
||||
"firstName": name_parts[0] if name_parts else "",
|
||||
"lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "",
|
||||
"insurance": plan_name,
|
||||
}
|
||||
return {"eligibility": None}
|
||||
except Exception as e:
|
||||
print("Extraction error:", e)
|
||||
return {"eligibility": None}
|
||||
|
||||
# ── step 2 — print eligibility PDF, return to results tab ───────────────────
|
||||
|
||||
def step2_eligibility_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
self.eligibility_tab = self.driver.current_window_handle
|
||||
try:
|
||||
download_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.XPATH, "//button[contains(.,'Printer Friendly Format')]")
|
||||
)
|
||||
)
|
||||
download_button.click()
|
||||
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||
new_tabs = [t for t in self.driver.window_handles if t != self.eligibility_tab]
|
||||
self.driver.switch_to.window(new_tabs[0])
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||
time.sleep(2)
|
||||
except TimeoutException:
|
||||
print("No new tab for eligibility; printing current page directly")
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"cmsp_eligibility_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("Eligibility PDF saved at:", pdf_path)
|
||||
|
||||
if self.driver.current_window_handle != self.eligibility_tab:
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.eligibility_tab)
|
||||
time.sleep(1)
|
||||
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step2_eligibility_pdf] failed: {e}")
|
||||
if self.eligibility_tab and self.driver.current_window_handle != self.eligibility_tab:
|
||||
try:
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.eligibility_tab)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
# ── step 3 — click member ID link → member details ──────────────────────────
|
||||
|
||||
def step3_click_member_id(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
substep = "ng_bind_link"
|
||||
try:
|
||||
member_link = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a[ng-bind='member.memberNumber']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].click();", member_link)
|
||||
time.sleep(2)
|
||||
print(f"[step3] clicked member ID link, URL: {self.driver.current_url}")
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[step3] FAILED at substep='{substep}': {e}")
|
||||
return f"ERROR:STEP3:{substep}"
|
||||
|
||||
# ── step 3b — print member details page as PDF via CDP ───────────────────────
|
||||
|
||||
def step3b_member_details_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
try:
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
time.sleep(2)
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"cmsp_member_details_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("Member details PDF saved at:", pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step3b_member_details_pdf] failed: {e}")
|
||||
return None
|
||||
|
||||
# ── step 4 — duplicate member details tab ───────────────────────────────────
|
||||
# Opens the same member details URL in a new tab so that Tab A can navigate
|
||||
# to service history while Tab B navigates to accumulator independently.
|
||||
|
||||
def step4_open_duplicate_tab(self):
|
||||
try:
|
||||
self.member_details_tab = self.driver.current_window_handle
|
||||
member_details_url = self.driver.current_url
|
||||
|
||||
self.driver.execute_script("window.open(arguments[0]);", member_details_url)
|
||||
time.sleep(3)
|
||||
|
||||
all_tabs = self.driver.window_handles
|
||||
self.accumulator_tab = next(
|
||||
t for t in all_tabs
|
||||
if t not in [self.eligibility_tab, self.member_details_tab]
|
||||
)
|
||||
print(f"[step4] duplicate tab opened: {self.accumulator_tab}")
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[step4] FAILED: {e}")
|
||||
return f"ERROR:STEP4:open_duplicate_tab"
|
||||
|
||||
# ── step 5 — Tab A: service history → CDP print ──────────────────────────────
|
||||
|
||||
def step5_service_history_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
try:
|
||||
self.driver.switch_to.window(self.member_details_tab)
|
||||
|
||||
history_link = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a.btn.btn-primary[href*='service-history']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView(true);", history_link)
|
||||
self.driver.execute_script("arguments[0].click();", history_link)
|
||||
time.sleep(2)
|
||||
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
time.sleep(3)
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"cmsp_history_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("History PDF saved at:", pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step5_service_history_pdf] failed: {e}")
|
||||
return None
|
||||
|
||||
# ── step 6 — Tab B: accumulator → wait for data → CDP print ─────────────────
|
||||
|
||||
def step6_accumulator_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
try:
|
||||
self.driver.switch_to.window(self.accumulator_tab)
|
||||
|
||||
# Wait for the duplicate member details page to fully load
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
time.sleep(2)
|
||||
|
||||
# Click View Accumulator
|
||||
accumulator_link = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a.btn.btn-primary[ng-if='vm.showAccumulator']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView(true);", accumulator_link)
|
||||
self.driver.execute_script("arguments[0].click();", accumulator_link)
|
||||
time.sleep(2)
|
||||
|
||||
# Wait for accumulator page to load then print whatever is on screen —
|
||||
# member info is always present even when there are no dollar amounts
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
time.sleep(4)
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"cmsp_accumulator_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("Accumulator PDF saved at:", pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step6_accumulator_pdf] failed: {e}")
|
||||
return None
|
||||
|
||||
# ── main workflow ────────────────────────────────────────────────────────────
|
||||
|
||||
def main_workflow(self, url):
|
||||
try:
|
||||
self.config_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.get(url)
|
||||
time.sleep(3)
|
||||
|
||||
login_result = self.login()
|
||||
if login_result.startswith("ERROR"):
|
||||
return {"status": "error", "message": login_result}
|
||||
|
||||
step1_result = self.step1()
|
||||
if step1_result.startswith("ERROR"):
|
||||
return {"status": "error", "message": step1_result}
|
||||
|
||||
eligibility_pdf_path = self.step2_eligibility_pdf()
|
||||
|
||||
step3_result = self.step3_click_member_id()
|
||||
if step3_result.startswith("ERROR"):
|
||||
return {"status": "partial", "message": step3_result,
|
||||
"pdf_path": eligibility_pdf_path, "file_type": "pdf"}
|
||||
|
||||
member_details_pdf_path = self.step3b_member_details_pdf()
|
||||
|
||||
# Duplicate the member details tab so service history and accumulator
|
||||
# can be opened independently without back-navigation
|
||||
step4_result = self.step4_open_duplicate_tab()
|
||||
if step4_result.startswith("ERROR"):
|
||||
return {"status": "partial", "message": step4_result,
|
||||
"pdf_path": eligibility_pdf_path,
|
||||
"member_details_pdf_path": member_details_pdf_path,
|
||||
"file_type": "pdf"}
|
||||
|
||||
# Tab A: service history PDF
|
||||
history_pdf_path = self.step5_service_history_pdf()
|
||||
|
||||
# Tab B: accumulator PDF
|
||||
accumulator_pdf_path = self.step6_accumulator_pdf()
|
||||
|
||||
result = {
|
||||
"status": "success",
|
||||
"pdf_path": eligibility_pdf_path,
|
||||
"member_details_pdf_path": member_details_pdf_path,
|
||||
"history_pdf_path": history_pdf_path,
|
||||
"accumulator_pdf_path": accumulator_pdf_path,
|
||||
"file_type": "pdf",
|
||||
"message": "Eligibility, member details, service history, and accumulator PDFs captured",
|
||||
}
|
||||
if self.extracted_data:
|
||||
result.update(self.extracted_data)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
finally:
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
+238
-247
@@ -10,8 +10,9 @@ import time
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
import calendar
|
||||
|
||||
from ddma_browser_manager import get_browser_manager
|
||||
from claimmd_browser_manager import get_browser_manager
|
||||
|
||||
class AutomationDeltaDentalMAEligibilityCheck:
|
||||
def __init__(self, data):
|
||||
@@ -45,7 +46,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
|
||||
# Try to click logout button if visible
|
||||
try:
|
||||
self.driver.get("https://providers.deltadentalma.com/")
|
||||
self.driver.get("https://www.deltadentalma.com/")
|
||||
time.sleep(2)
|
||||
|
||||
logout_selectors = [
|
||||
@@ -99,27 +100,27 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
current_url = self.driver.current_url
|
||||
print(f"[login] Current URL: {current_url}")
|
||||
|
||||
logged_in_patterns = ["member", "dashboard", "eligibility", "search", "patients"]
|
||||
logged_in_patterns = ["member", "dashboard", "eligibility", "search", "patients", "private/provider"]
|
||||
is_logged_in_url = any(pattern in current_url.lower() for pattern in logged_in_patterns)
|
||||
|
||||
if is_logged_in_url and "onboarding" not in current_url.lower():
|
||||
if is_logged_in_url and "sign-in" not in current_url.lower():
|
||||
print("[login] Already on logged-in page - skipping login entirely")
|
||||
if "member" not in current_url.lower():
|
||||
try:
|
||||
member_search = WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
print("[login] Found member search input - returning ALREADY_LOGGED_IN")
|
||||
return "ALREADY_LOGGED_IN"
|
||||
except TimeoutException:
|
||||
members_url = "https://providers.deltadentalma.com/members"
|
||||
members_url = "https://www.deltadentalma.com/private/provider/dashboard"
|
||||
print(f"[login] Navigating to members page: {members_url}")
|
||||
self.driver.get(members_url)
|
||||
time.sleep(2)
|
||||
|
||||
try:
|
||||
member_search = WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
print("[login] Member search found - ALREADY_LOGGED_IN")
|
||||
return "ALREADY_LOGGED_IN"
|
||||
@@ -137,9 +138,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
current_url = self.driver.current_url
|
||||
print(f"[login] URL after navigation: {current_url}")
|
||||
|
||||
if "onboarding" not in current_url.lower():
|
||||
if "sign-in" not in current_url.lower():
|
||||
member_search = WebDriverWait(self.driver, 3).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
if member_search:
|
||||
print("[login] Session valid - skipping login")
|
||||
@@ -172,7 +173,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
try:
|
||||
otp_candidate = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//input[contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code') or contains(@aria-label,'Verification code')]")
|
||||
(By.XPATH, "//input[@data-test-id='MfaCode']")
|
||||
)
|
||||
)
|
||||
if otp_candidate:
|
||||
@@ -189,7 +190,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
time.sleep(2)
|
||||
try:
|
||||
member_search = WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
if member_search:
|
||||
print("[login] Already authenticated after modal dismiss")
|
||||
@@ -200,30 +201,21 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
# Fill login form
|
||||
try:
|
||||
email_field = WebDriverWait(self.driver, 10).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//input[@name='username' and @type='text']"))
|
||||
EC.element_to_be_clickable((By.XPATH, "//input[@data-test-id='Username']"))
|
||||
)
|
||||
except TimeoutException:
|
||||
print("[login] Could not find login form - page may have changed")
|
||||
return "ERROR: Login form not found"
|
||||
|
||||
email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='username' and @type='text']")))
|
||||
email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@data-test-id='Username']")))
|
||||
email_field.clear()
|
||||
email_field.send_keys(self.massddma_username)
|
||||
|
||||
password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='password' and @type='password']")))
|
||||
password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@data-test-id='Password']")))
|
||||
password_field.clear()
|
||||
password_field.send_keys(self.massddma_password)
|
||||
|
||||
# Remember me
|
||||
try:
|
||||
remember_me_checkbox = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//label[.//span[contains(text(),'Remember me')]]")
|
||||
))
|
||||
remember_me_checkbox.click()
|
||||
except Exception:
|
||||
print("[login] Remember me checkbox not found (continuing).")
|
||||
|
||||
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@type='submit' and @aria-label='Sign in']")))
|
||||
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@data-test-id='SigninSubmitButton']")))
|
||||
login_button.click()
|
||||
|
||||
# Save credentials hash after login attempt
|
||||
@@ -234,7 +226,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
try:
|
||||
otp_candidate = WebDriverWait(self.driver, 30).until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//input[contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code')]")
|
||||
(By.XPATH, "//input[@data-test-id='MfaCode']")
|
||||
)
|
||||
)
|
||||
if otp_candidate:
|
||||
@@ -247,7 +239,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
current_url = self.driver.current_url.lower()
|
||||
if "member" in current_url or "dashboard" in current_url:
|
||||
member_search = WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
|
||||
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
|
||||
)
|
||||
print("[login] Login successful - now on member search page")
|
||||
return "SUCCESS"
|
||||
@@ -264,7 +256,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
except TimeoutException:
|
||||
pass
|
||||
|
||||
if "onboarding" in self.driver.current_url.lower() or "login" in self.driver.current_url.lower():
|
||||
if "sign-in" in self.driver.current_url.lower():
|
||||
print("[login] Login failed - still on login page")
|
||||
return "ERROR:LOGIN FAILED: Still on login page"
|
||||
|
||||
@@ -275,103 +267,66 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
print("[login] Exception during login:", e)
|
||||
return f"ERROR:LOGIN FAILED: {e}"
|
||||
|
||||
def _select_provider_dropdown(self):
|
||||
"""
|
||||
Click the provider dropdown on the member search page and select the option
|
||||
matching self.providerName (case-insensitive). Falls back to the first option.
|
||||
The button data-testid is 'member-search_provider_select-btn'.
|
||||
"""
|
||||
try:
|
||||
short_wait = WebDriverWait(self.driver, 5)
|
||||
try:
|
||||
provider_btn = short_wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.XPATH, '//button[@data-testid="member-search_provider_select-btn"]')
|
||||
)
|
||||
)
|
||||
except TimeoutException:
|
||||
print("[DDMA step1] No provider dropdown found — skipping")
|
||||
return
|
||||
|
||||
provider_btn.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
try:
|
||||
WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, "//*[@role='option']"))
|
||||
)
|
||||
except TimeoutException:
|
||||
print("[DDMA step1] Provider listbox did not open")
|
||||
return
|
||||
|
||||
options = self.driver.find_elements(By.XPATH, "//*[@role='option']")
|
||||
print(f"[DDMA step1] Provider options: {[o.text.strip() for o in options]}")
|
||||
|
||||
target = (self.providerName or "").strip().lower()
|
||||
selected = False
|
||||
|
||||
if target:
|
||||
for opt in options:
|
||||
if target in opt.text.lower():
|
||||
opt.click()
|
||||
print(f"[DDMA step1] Selected provider: '{opt.text.strip()}'")
|
||||
selected = True
|
||||
break
|
||||
|
||||
if not selected and options:
|
||||
options[0].click()
|
||||
print(f"[DDMA step1] No match for '{self.providerName}', selected first: '{options[0].text.strip()}'")
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Provider selection error (non-fatal): {e}")
|
||||
|
||||
def step1(self):
|
||||
"""Fill search form with all available fields (flexible search)."""
|
||||
"""Open the Group/Health Connector search page and search by Subscriber ID + Birthdate."""
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
|
||||
try:
|
||||
fields = []
|
||||
if self.memberId:
|
||||
fields.append(f"ID: {self.memberId}")
|
||||
if self.firstName:
|
||||
fields.append(f"FirstName: {self.firstName}")
|
||||
if self.lastName:
|
||||
fields.append(f"LastName: {self.lastName}")
|
||||
fields.append(f"SubscriberID: {self.memberId}")
|
||||
if self.dateOfBirth:
|
||||
fields.append(f"DOB: {self.dateOfBirth}")
|
||||
print(f"[DDMA step1] Starting search with: {', '.join(fields)}")
|
||||
|
||||
# Wait for the search page to be fully loaded before interacting
|
||||
wait.until(EC.presence_of_element_located(
|
||||
(By.XPATH, '//input[@placeholder="Search by member ID"]')
|
||||
))
|
||||
time.sleep(1)
|
||||
search_page_url = "https://www.deltadentalma.com/private/provider/check-patient-benefits-search"
|
||||
|
||||
# Select provider from dropdown based on NPI settings
|
||||
self._select_provider_dropdown()
|
||||
# Try navigating straight to the search page first — avoids any click/visibility issues
|
||||
print(f"[DDMA step1] Navigating directly to search page: {search_page_url}")
|
||||
self.driver.get(search_page_url)
|
||||
time.sleep(2)
|
||||
|
||||
def replace_with_sendkeys(el, value):
|
||||
el.click()
|
||||
el.send_keys(Keys.CONTROL, "a")
|
||||
el.send_keys(Keys.BACKSPACE)
|
||||
el.send_keys(value)
|
||||
if "check-patient-benefits-search" not in self.driver.current_url.lower():
|
||||
# Direct deep-link didn't stick (e.g. bounced back to sign-in/dashboard) —
|
||||
# fall back to clicking the Group/Health Connector link from the dashboard.
|
||||
print(f"[DDMA step1] Direct navigation redirected to: {self.driver.current_url} — falling back to clicking the link")
|
||||
|
||||
# 1. Fill Member ID if provided
|
||||
if self.memberId:
|
||||
if "private/provider/dashboard" not in self.driver.current_url.lower():
|
||||
dashboard_url = "https://www.deltadentalma.com/private/provider/dashboard"
|
||||
print(f"[DDMA step1] Navigating to dashboard: {dashboard_url}")
|
||||
self.driver.get(dashboard_url)
|
||||
time.sleep(2)
|
||||
|
||||
link_xpath = "//a[contains(@href, 'check-patient-benefits-search')]"
|
||||
try:
|
||||
member_id_input = wait.until(EC.presence_of_element_located(
|
||||
(By.XPATH, '//input[@placeholder="Search by member ID"]')
|
||||
))
|
||||
member_id_input.clear()
|
||||
member_id_input.send_keys(self.memberId)
|
||||
print(f"[DDMA step1] Entered Member ID: {self.memberId}")
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Warning: Could not fill Member ID: {e}")
|
||||
connector_link = WebDriverWait(self.driver, 20).until(
|
||||
EC.element_to_be_clickable((By.XPATH, link_xpath))
|
||||
)
|
||||
connector_link.click()
|
||||
except TimeoutException:
|
||||
# Element may exist but be hidden (e.g. a responsive duplicate) — try any match via JS click
|
||||
candidates = self.driver.find_elements(By.XPATH, link_xpath)
|
||||
print(f"[DDMA step1] element_to_be_clickable timed out; found {len(candidates)} matching link(s) in DOM")
|
||||
if candidates:
|
||||
try:
|
||||
self.driver.execute_script("arguments[0].click();", candidates[0])
|
||||
except Exception as js_err:
|
||||
print(f"[DDMA step1] JS click failed: {js_err}")
|
||||
print(f"[DDMA step1] Current URL: {self.driver.current_url}")
|
||||
return "ERROR: Could not open Group/Health Connector search page"
|
||||
else:
|
||||
print(f"[DDMA step1] No Group/Health Connector link found in DOM. Current URL: {self.driver.current_url}")
|
||||
return "ERROR: Could not open Group/Health Connector search page"
|
||||
|
||||
# 2. Fill DOB if provided
|
||||
try:
|
||||
wait.until(lambda d: "check-patient-benefits-search" in d.current_url)
|
||||
except TimeoutException:
|
||||
print(f"[DDMA step1] Click did not navigate to search page. Current URL: {self.driver.current_url}")
|
||||
return "ERROR: Could not open Group/Health Connector search page"
|
||||
|
||||
print(f"[DDMA step1] On Group/Health Connector search page: {self.driver.current_url}")
|
||||
|
||||
# Fill Birthdate (masked mm/dd/yyyy input — send digits only, mask inserts slashes)
|
||||
if self.dateOfBirth:
|
||||
try:
|
||||
dob_parts = self.dateOfBirth.split("-")
|
||||
@@ -379,54 +334,57 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
month = dob_parts[1].zfill(2)
|
||||
day = dob_parts[2].zfill(2)
|
||||
|
||||
dob_container = wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//div[@data-testid='member-search_date-of-birth']")
|
||||
)
|
||||
)
|
||||
|
||||
month_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']")
|
||||
day_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']")
|
||||
year_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='year' and @contenteditable='true']")
|
||||
|
||||
replace_with_sendkeys(month_elem, month)
|
||||
time.sleep(0.05)
|
||||
replace_with_sendkeys(day_elem, day)
|
||||
time.sleep(0.05)
|
||||
replace_with_sendkeys(year_elem, year)
|
||||
print(f"[DDMA step1] Filled DOB: {month}/{day}/{year}")
|
||||
birthdate_input = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//input[@data-test-id='InStateSearchBirthdate']")
|
||||
))
|
||||
birthdate_input.click()
|
||||
birthdate_input.send_keys(Keys.CONTROL, "a")
|
||||
birthdate_input.send_keys(Keys.BACKSPACE)
|
||||
birthdate_input.send_keys(f"{month}{day}{year}")
|
||||
print(f"[DDMA step1] Filled Birthdate: {month}/{day}/{year}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Warning: Could not fill DOB: {e}")
|
||||
print(f"[DDMA step1] Warning: Could not fill Birthdate: {e}")
|
||||
|
||||
# Fill Subscriber ID (== member ID)
|
||||
if self.memberId:
|
||||
try:
|
||||
subscriber_input = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//input[@data-test-id='InStateSearchSubscriberId']")
|
||||
))
|
||||
subscriber_input.clear()
|
||||
subscriber_input.send_keys(self.memberId)
|
||||
print(f"[DDMA step1] Filled Subscriber ID: {self.memberId}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Warning: Could not fill Subscriber ID: {e}")
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
# Click Search button
|
||||
continue_btn = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, '//button[@data-testid="member-search_search-button"]')
|
||||
search_btn = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//button[@data-test-id='InStateSearchButton']")
|
||||
))
|
||||
continue_btn.click()
|
||||
search_btn.click()
|
||||
print("[DDMA step1] Clicked Search button")
|
||||
|
||||
# Wait for either results row or no-results message (up to 15s)
|
||||
# Wait for either the Members results table to render, or (when the
|
||||
# Subscriber ID + DOB match exactly one family member) DDMA to skip
|
||||
# the results list and navigate straight to the patient detail page.
|
||||
# Note: the detail page URL is nested UNDER the search page path
|
||||
# (".../check-patient-benefits-search/check-patient-benefit-details"),
|
||||
# so "check-patient-benefits-search" alone can't distinguish them —
|
||||
# only the detail page's own "-benefit-details" segment can.
|
||||
try:
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
EC.any_of(
|
||||
EC.presence_of_element_located((By.XPATH, "//tbody//tr")),
|
||||
EC.presence_of_element_located((By.XPATH, '//div[@data-testid="member-search-result-no-results"]')),
|
||||
)
|
||||
lambda d: d.find_elements(By.XPATH, "//table//tbody//tr")
|
||||
or "check-patient-benefit-details" in d.current_url.lower()
|
||||
)
|
||||
time.sleep(3)
|
||||
time.sleep(2)
|
||||
except TimeoutException:
|
||||
pass # proceed and let step2 handle missing results
|
||||
print("[DDMA step1] Warning: No results table appeared within timeout")
|
||||
return "ERROR: INVALID SEARCH CRITERIA"
|
||||
|
||||
# Check for no-results error
|
||||
try:
|
||||
error_msg = self.driver.find_element(By.XPATH, '//div[@data-testid="member-search-result-no-results"]')
|
||||
if error_msg:
|
||||
print("[DDMA step1] Error: No results found")
|
||||
return "ERROR: INVALID SEARCH CRITERIA"
|
||||
except Exception:
|
||||
pass
|
||||
if "check-patient-benefit-details" in self.driver.current_url.lower():
|
||||
print(f"[DDMA step1] Search navigated directly to detail page: {self.driver.current_url}")
|
||||
|
||||
print("[DDMA step1] Search completed successfully")
|
||||
return "Success"
|
||||
@@ -442,112 +400,120 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
try:
|
||||
import re
|
||||
|
||||
# Wait for results table, then pause for full render
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
|
||||
)
|
||||
time.sleep(2) # Let the row content fully render after table appears
|
||||
except TimeoutException:
|
||||
print("[DDMA step2] Warning: Results table not found within timeout")
|
||||
|
||||
eligibilityText = "unknown"
|
||||
foundMemberId = self.memberId or ""
|
||||
patientName = ""
|
||||
# If the caller already knows this patient (e.g. an existing patient record
|
||||
# being rechecked), use that name directly — no need to scrape/parse it off
|
||||
# the DDMA page, only the eligibility status is actually needed.
|
||||
patientName = f"{self.firstName} {self.lastName}".strip() if self.firstName and self.lastName else ""
|
||||
known_patient_name = bool(patientName)
|
||||
|
||||
# Extract patient name from search results row
|
||||
try:
|
||||
first_row = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]")
|
||||
row_text = first_row.text.strip()
|
||||
print(f"[DDMA step2] First row text: {row_text[:150]}...")
|
||||
# DDMA now sometimes skips the family-members results table entirely and
|
||||
# navigates straight to the single-patient detail page (when Subscriber ID +
|
||||
# DOB match exactly one person). In that case there's no members table to
|
||||
# parse here — skip straight to the detail-page fallback extraction below.
|
||||
already_on_detail_page = "check-patient-benefit-details" in self.driver.current_url.lower()
|
||||
|
||||
lines = row_text.split('\n') if row_text else []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if re.match(r"^[A-Za-z\s\-']{2,60}$", line):
|
||||
patientName = line
|
||||
print(f"[DDMA step2] Extracted patient name from row: '{patientName}'")
|
||||
rows = []
|
||||
target_row = None
|
||||
|
||||
if not already_on_detail_page:
|
||||
# Wait for results table, then pause for full render
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
|
||||
)
|
||||
time.sleep(2) # Let the row content fully render after table appears
|
||||
except TimeoutException:
|
||||
print("[DDMA step2] Warning: Results table not found within timeout")
|
||||
|
||||
# The Members table lists the whole family sharing this subscriber ID —
|
||||
# find the row matching this patient's DOB (falls back to last name, then first row).
|
||||
rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr")
|
||||
print(f"[DDMA step2] Found {len(rows)} family member row(s)")
|
||||
else:
|
||||
print(f"[DDMA step2] Already on detail page: {self.driver.current_url}")
|
||||
|
||||
if self.dateOfBirth and rows:
|
||||
try:
|
||||
dob_parts = self.dateOfBirth.split("-")
|
||||
dob_year, dob_month, dob_day = dob_parts[0], int(dob_parts[1]), int(dob_parts[2])
|
||||
expected_dob = f"{calendar.month_abbr[dob_month]} {dob_day}, {dob_year}".lower()
|
||||
for row in rows:
|
||||
cells = row.find_elements(By.XPATH, "./td")
|
||||
if len(cells) >= 3 and cells[2].text.strip().lower() == expected_dob:
|
||||
target_row = row
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] DOB matching error (non-fatal): {e}")
|
||||
|
||||
if target_row is None and self.lastName and rows:
|
||||
for row in rows:
|
||||
if self.lastName.strip().lower() in row.text.lower():
|
||||
target_row = row
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Error reading first row: {e}")
|
||||
|
||||
if target_row is None and rows:
|
||||
target_row = rows[0]
|
||||
print("[DDMA step2] No DOB/name match — defaulting to first family member row")
|
||||
|
||||
if target_row is not None:
|
||||
try:
|
||||
cells = target_row.find_elements(By.XPATH, "./td")
|
||||
if cells and not known_patient_name:
|
||||
patientName = cells[0].text.strip()
|
||||
print(f"[DDMA step2] Extracted patient name from row: '{patientName}'")
|
||||
if len(cells) >= 2 and cells[1].text.strip():
|
||||
foundMemberId = cells[1].text.strip()
|
||||
if len(cells) >= 7:
|
||||
status_text = cells[6].text.strip().lower()
|
||||
if "inactive" in status_text or "termed" in status_text:
|
||||
eligibilityText = "inactive"
|
||||
elif "active" in status_text:
|
||||
eligibilityText = "active"
|
||||
else:
|
||||
eligibilityText = status_text
|
||||
print(f"[DDMA step2] Found eligibility status: {eligibilityText}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Error reading matched row cells: {e}")
|
||||
|
||||
# Click into the member's detail page
|
||||
try:
|
||||
name_link = target_row.find_element(By.XPATH, ".//a")
|
||||
name_link.click()
|
||||
print(f"[DDMA step2] Clicked into detail page for: {patientName}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Warning: Could not click patient name link: {e}")
|
||||
else:
|
||||
print("[DDMA step2] Warning: No family member rows found in search results")
|
||||
|
||||
# Wait for the detail page to load (URL gains the "-benefit-details" segment)
|
||||
try:
|
||||
short_wait = WebDriverWait(self.driver, 3)
|
||||
status_link = short_wait.until(EC.presence_of_element_located((
|
||||
By.XPATH,
|
||||
"(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]"
|
||||
)))
|
||||
eligibilityText = status_link.text.strip().lower()
|
||||
print(f"[DDMA step2] Found eligibility status: {eligibilityText}")
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
lambda d: "check-patient-benefit-details" in d.current_url.lower()
|
||||
)
|
||||
print(f"[DDMA step2] Navigated to detail page: {self.driver.current_url}")
|
||||
except Exception:
|
||||
try:
|
||||
alt_status = self.driver.find_element(By.XPATH, "//*[contains(text(),'Active') or contains(text(),'Inactive') or contains(text(),'Eligible')]")
|
||||
eligibilityText = alt_status.text.strip().lower()
|
||||
if "active" in eligibilityText or "eligible" in eligibilityText:
|
||||
eligibilityText = "active"
|
||||
elif "inactive" in eligibilityText:
|
||||
eligibilityText = "inactive"
|
||||
print(f"[DDMA step2] Found eligibility via alternative: {eligibilityText}")
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[DDMA step2] Warning: Still on search page: {self.driver.current_url}")
|
||||
|
||||
# Find the member-details URL from the first row
|
||||
print("[DDMA step2] Looking for patient detail link...")
|
||||
detail_url = None
|
||||
|
||||
link_selectors = [
|
||||
"(//table//tbody//tr)[1]//td[1]//a",
|
||||
"(//tbody//tr)[1]//a[contains(@href, 'member-details')]",
|
||||
"(//tbody//tr)[1]//a[contains(@href, 'member')]",
|
||||
"//a[contains(@href, 'member-details')]",
|
||||
]
|
||||
for selector in link_selectors:
|
||||
# Wait for meaningful content on the detail page
|
||||
for selector in [
|
||||
"//*[contains(text(),'Date of Birth') or contains(text(),'Address') or contains(text(),'Member ID')]",
|
||||
"//table", "//h1", "//h2",
|
||||
]:
|
||||
try:
|
||||
link_el = WebDriverWait(self.driver, 5).until(
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH, selector))
|
||||
)
|
||||
href = link_el.get_attribute("href")
|
||||
if href and "member-details" in href:
|
||||
detail_url = href
|
||||
print(f"[DDMA step2] Found detail URL: {href}")
|
||||
break
|
||||
print(f"[DDMA step2] Detail page content loaded: {selector}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if detail_url:
|
||||
# Always navigate via driver.get() — this blocks until the new page loads,
|
||||
# unlike click() which returns immediately and causes a race condition.
|
||||
print(f"[DDMA step2] Navigating to detail page: {detail_url}")
|
||||
self.driver.get(detail_url)
|
||||
time.sleep(3) # Let JavaScript finish rendering all sections
|
||||
|
||||
# Confirm we actually landed on the detail page (not redirected away)
|
||||
try:
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
lambda d: "member-details" in d.current_url
|
||||
)
|
||||
print(f"[DDMA step2] Confirmed on detail page: {self.driver.current_url}")
|
||||
except Exception:
|
||||
print(f"[DDMA step2] Warning: URL after navigation: {self.driver.current_url}")
|
||||
|
||||
# Wait for meaningful content on the detail page
|
||||
for selector in [
|
||||
"//*[contains(text(),'Date of Birth') or contains(text(),'Address') or contains(text(),'Member ID')]",
|
||||
"//table", "//h1", "//h2",
|
||||
]:
|
||||
try:
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH, selector))
|
||||
)
|
||||
print(f"[DDMA step2] Detail page content loaded: {selector}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
time.sleep(3) # Let JavaScript finish rendering all sections
|
||||
|
||||
# Get full page text as lines
|
||||
# Fallback: refine patient name from the detail page text if row extraction failed
|
||||
if not patientName:
|
||||
try:
|
||||
page_lines = self.driver.execute_script(
|
||||
"return document.body.innerText;"
|
||||
@@ -576,15 +542,29 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Preferred: the "Benefits for JOSHUA ZHANG" heading names the actual
|
||||
# patient. "Subscriber name:" is the policy holder (e.g. a parent), so
|
||||
# it is only used below if this heading is missing.
|
||||
for line in page_lines:
|
||||
heading = re.match(r'^benefits\s+for\s+(.+)$', line, re.IGNORECASE)
|
||||
if heading and looks_like_name(heading.group(1)):
|
||||
patientName = heading.group(1).strip()
|
||||
print(f"[DDMA step2] Extracted name from 'Benefits for' heading: '{patientName}'")
|
||||
break
|
||||
|
||||
# Scan lines for the "Name" label. When "Name" appears alone we scan
|
||||
# forward past accessibility text to the first plausible name value.
|
||||
for i, line in enumerate(page_lines):
|
||||
if patientName:
|
||||
break
|
||||
# Case 1: "Name : Value" or "Name: Value" on the same line
|
||||
# (DDMA's detail page labels this "Subscriber name:", so that
|
||||
# prefix must be accepted rather than excluded)
|
||||
same_line = re.match(
|
||||
r'^(?:member\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE
|
||||
r'^(?:member\s+|subscriber\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE
|
||||
)
|
||||
if same_line and not re.search(
|
||||
r'(provider|group|subscriber|plan)\s+name', line, re.IGNORECASE
|
||||
r'(provider|group|plan)\s+name', line, re.IGNORECASE
|
||||
):
|
||||
candidate = same_line.group(1).strip()
|
||||
if looks_like_name(candidate):
|
||||
@@ -593,7 +573,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
break
|
||||
|
||||
# Case 2: "Name" or "Name:" alone on a line — scan forward for value
|
||||
elif re.match(r'^(?:member\s+)?name\s*:?\s*$', line, re.IGNORECASE):
|
||||
elif re.match(r'^(?:member\s+|subscriber\s+)?name\s*:?\s*$', line, re.IGNORECASE):
|
||||
for j in range(i + 1, min(i + 6, len(page_lines))):
|
||||
candidate = page_lines[j].strip()
|
||||
if looks_like_name(candidate):
|
||||
@@ -602,8 +582,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
break
|
||||
if patientName:
|
||||
break
|
||||
else:
|
||||
print("[DDMA step2] Warning: Could not find patient detail link")
|
||||
|
||||
# Clean patient name — strip any remaining date artifacts
|
||||
if patientName:
|
||||
@@ -614,6 +592,27 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
if not patientName:
|
||||
print("[DDMA step2] Could not extract patient name from detail page")
|
||||
|
||||
# Fallback: refine eligibility status from the detail page's "Coverage is:"
|
||||
# label if the family-table row extraction didn't set it (e.g. DDMA
|
||||
# navigated straight to the detail page, skipping the members table).
|
||||
if eligibilityText == "unknown":
|
||||
try:
|
||||
page_text = self.driver.execute_script("return document.body.innerText;")
|
||||
coverage_match = re.search(
|
||||
r'coverage\s+is\s*[:\-]?\s*([A-Za-z]+)', page_text, re.IGNORECASE
|
||||
)
|
||||
if coverage_match:
|
||||
status_text = coverage_match.group(1).strip().lower()
|
||||
if "inactive" in status_text or "termed" in status_text:
|
||||
eligibilityText = "inactive"
|
||||
elif "active" in status_text:
|
||||
eligibilityText = "active"
|
||||
else:
|
||||
eligibilityText = status_text
|
||||
print(f"[DDMA step2] Extracted eligibility from 'Coverage is:' label: '{eligibilityText}'")
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Could not extract eligibility from 'Coverage is:' label: {e}")
|
||||
|
||||
# Wait for the page to be fully ready before capturing PDF
|
||||
try:
|
||||
WebDriverWait(self.driver, 30).until(
|
||||
@@ -650,14 +649,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
|
||||
print(f"[DDMA step2] PDF saved at: {pdf_path}")
|
||||
|
||||
# Close the browser window after PDF (session preserved in profile)
|
||||
try:
|
||||
from ddma_browser_manager import get_browser_manager
|
||||
get_browser_manager().quit_driver()
|
||||
print("[step2] Browser closed - session preserved in profile")
|
||||
except Exception as e:
|
||||
print(f"[step2] Error closing browser: {e}")
|
||||
|
||||
print(f"[DDMA step2] Final — PatientName: '{patientName}', MemberID: '{foundMemberId}'")
|
||||
|
||||
return {
|
||||
@@ -11,7 +11,7 @@ import json
|
||||
import base64
|
||||
from datetime import date
|
||||
|
||||
from ddma_browser_manager import get_browser_manager
|
||||
from claimmd_browser_manager import get_browser_manager
|
||||
|
||||
MEMBERS_URL = "https://providers.deltadentalma.com/members"
|
||||
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
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, Select
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
import time
|
||||
import os
|
||||
import base64
|
||||
|
||||
|
||||
class AutomationMassHealthEligibilityHistoryCheck:
|
||||
def __init__(self, data):
|
||||
self.headless = False
|
||||
self.driver = None
|
||||
self.extracted_data = {}
|
||||
self.eligibility_tab = None # handle to the eligibility results tab
|
||||
|
||||
self.data = data.get("data")
|
||||
|
||||
self.massdhp_username = self.data.get("massdhpUsername", "")
|
||||
self.massdhp_password = self.data.get("massdhpPassword", "")
|
||||
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||
self.memberId = self.data.get("memberId", "")
|
||||
self.firstName = self.data.get("firstName", "")
|
||||
self.lastName = self.data.get("lastName", "")
|
||||
|
||||
# Convert dateOfBirth from YYYY-MM-DD to MMDDYYYY
|
||||
if self.dateOfBirth and "-" in self.dateOfBirth:
|
||||
parts = self.dateOfBirth.split("-")
|
||||
if len(parts) == 3:
|
||||
year, month, day = parts
|
||||
self.dateOfBirth = f"{month.zfill(2)}{day.zfill(2)}{year}"
|
||||
|
||||
self.download_dir = os.path.abspath("downloads")
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
|
||||
# ── driver ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def config_driver(self):
|
||||
options = webdriver.ChromeOptions()
|
||||
if self.headless:
|
||||
options.add_argument("--headless")
|
||||
|
||||
prefs = {
|
||||
"download.default_directory": self.download_dir,
|
||||
"plugins.always_open_pdf_externally": False,
|
||||
"download.prompt_for_download": False,
|
||||
"download.directory_upgrade": True,
|
||||
}
|
||||
options.add_experimental_option("prefs", prefs)
|
||||
|
||||
s = Service(ChromeDriverManager().install())
|
||||
self.driver = webdriver.Chrome(service=s, options=options)
|
||||
|
||||
# ── login ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def login(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
try:
|
||||
signin_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a.btn.btn-block.btn-primary[href='https://connectsso.masshealth-dental.org/mhprovider/index.html']")
|
||||
)
|
||||
)
|
||||
signin_button.click()
|
||||
time.sleep(3)
|
||||
|
||||
email_field = wait.until(EC.presence_of_element_located((By.ID, "User")))
|
||||
email_field.clear()
|
||||
email_field.send_keys(self.massdhp_username)
|
||||
|
||||
password_field = wait.until(EC.presence_of_element_located((By.ID, "Password")))
|
||||
password_field.clear()
|
||||
password_field.send_keys(self.massdhp_password)
|
||||
|
||||
login_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "input[type='submit'][name='submit'][value='Login']")
|
||||
)
|
||||
)
|
||||
login_button.click()
|
||||
|
||||
# Wait for the SSO redirect. On some machines Chrome opens the dashboard
|
||||
# in a new tab and closes the SSO tab — poll ALL window handles.
|
||||
print("[login] Waiting for SSO redirect to provider.masshealth-dental.org ...")
|
||||
deadline = time.time() + 30
|
||||
found = False
|
||||
while time.time() < deadline:
|
||||
time.sleep(0.5)
|
||||
for handle in self.driver.window_handles:
|
||||
try:
|
||||
self.driver.switch_to.window(handle)
|
||||
if self.driver.current_url.startswith("https://provider.masshealth-dental.org"):
|
||||
print(f"[login] Redirect complete. URL: {self.driver.current_url}")
|
||||
found = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if found:
|
||||
break
|
||||
|
||||
if not found:
|
||||
print("[login] Redirect timeout — portal window not found in any tab.")
|
||||
return "ERROR: Login redirect timed out — check credentials or portal availability."
|
||||
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[login] Error: {e}")
|
||||
return "ERROR:LOGIN FAILED"
|
||||
|
||||
# ── step 1 — search member, extract eligibility data ────────────────────────
|
||||
|
||||
def step1(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
substep = "init"
|
||||
try:
|
||||
print(f"[step1] URL: {self.driver.current_url}")
|
||||
|
||||
substep = "patient_management"
|
||||
patient_mgmt = wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//strong[@translate='Patient Management']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView(true);", patient_mgmt)
|
||||
self.driver.execute_script("arguments[0].click();", patient_mgmt)
|
||||
time.sleep(2)
|
||||
|
||||
substep = "member_eligibility_link"
|
||||
eligibility_link = wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//a[@translate='Member Eligibility']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].click();", eligibility_link)
|
||||
time.sleep(2)
|
||||
|
||||
substep = "provider_dropdown"
|
||||
provider_dropdown = wait.until(
|
||||
EC.presence_of_element_located((By.NAME, "provider"))
|
||||
)
|
||||
select_provider = Select(provider_dropdown)
|
||||
options = [o.text for o in select_provider.options]
|
||||
print(f"[step1] provider options: {options}")
|
||||
|
||||
substep = "select_provider"
|
||||
first_option = next(
|
||||
(o for o in select_provider.options if o.get_attribute("value").strip()),
|
||||
select_provider.options[0]
|
||||
)
|
||||
print(f"[step1] selecting provider: '{first_option.text}'")
|
||||
select_provider.select_by_value(first_option.get_attribute("value"))
|
||||
time.sleep(2)
|
||||
|
||||
substep = "member_dob"
|
||||
member_dob = wait.until(
|
||||
EC.presence_of_all_elements_located((By.NAME, "dateInput"))
|
||||
)[1]
|
||||
member_dob.clear()
|
||||
member_dob.send_keys(self.dateOfBirth)
|
||||
|
||||
substep = "member_number"
|
||||
member_number = wait.until(
|
||||
EC.presence_of_element_located((By.NAME, "memberNumber"))
|
||||
)
|
||||
member_number.clear()
|
||||
member_number.send_keys(self.memberId)
|
||||
|
||||
substep = "search_button"
|
||||
search_button = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary')]"))
|
||||
)
|
||||
search_button.click()
|
||||
|
||||
substep = "wait_results"
|
||||
wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']")
|
||||
)
|
||||
)
|
||||
|
||||
self.extracted_data = self._extract_data_from_page()
|
||||
print(f"[step1] data extracted: {self.extracted_data}")
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[step1] FAILED at substep='{substep}': {e}")
|
||||
print(f"[step1] URL at failure: {self.driver.current_url}")
|
||||
return f"ERROR:STEP1:{substep}"
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _cell_text(self, cell):
|
||||
text = cell.text.strip()
|
||||
if not text:
|
||||
try:
|
||||
text = (self.driver.execute_script("return arguments[0].innerText;", cell) or "").strip()
|
||||
except Exception:
|
||||
pass
|
||||
return text
|
||||
|
||||
def _normalize_id(self, s):
|
||||
return "".join(c for c in str(s) if c.isalnum()).lower()
|
||||
|
||||
def _extract_data_from_page(self):
|
||||
wait = WebDriverWait(self.driver, 15)
|
||||
extracted = {}
|
||||
try:
|
||||
wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr")
|
||||
)
|
||||
)
|
||||
|
||||
# AngularJS populates cell text after elements appear in the DOM.
|
||||
# Retry a few times until a row with non-empty cell[2] is found.
|
||||
for attempt in range(4):
|
||||
for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]:
|
||||
rows = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
f"//h4[text()='{status_label}']/following::table[1]/tbody/tr"
|
||||
)
|
||||
for row in rows:
|
||||
cells = row.find_elements(By.TAG_NAME, "td")
|
||||
if len(cells) < 3:
|
||||
continue
|
||||
member_number = self._cell_text(cells[2])
|
||||
norm_cell = self._normalize_id(member_number)
|
||||
norm_self = self._normalize_id(self.memberId)
|
||||
if norm_self and norm_cell and (norm_self in norm_cell or norm_cell in norm_self):
|
||||
full_name = self._cell_text(cells[4]) if len(cells) > 4 else ""
|
||||
plan_name = (
|
||||
self._cell_text(cells[6]) if len(cells) > 6
|
||||
else (self._cell_text(cells[-1]) if len(cells) > 4 else "")
|
||||
)
|
||||
name_parts = full_name.split()
|
||||
extracted = {
|
||||
"eligibility": elig_flag,
|
||||
"firstName": name_parts[0] if name_parts else "",
|
||||
"lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "",
|
||||
"insurance": plan_name,
|
||||
}
|
||||
print(f"[extraction] MATCHED {status_label} → name='{full_name}' plan='{plan_name}'")
|
||||
return extracted
|
||||
if attempt < 3:
|
||||
print(f"[extraction] cells not yet populated (attempt {attempt + 1}), retrying...")
|
||||
time.sleep(1.5)
|
||||
|
||||
print(f"[extraction] No matching row for memberId='{self.memberId}'")
|
||||
return {"eligibility": None}
|
||||
except Exception as e:
|
||||
print("Extraction error:", e)
|
||||
return {"eligibility": None}
|
||||
|
||||
# ── step 2 — print eligibility PDF, stay on results tab ─────────────────────
|
||||
|
||||
def step2_eligibility_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
self.eligibility_tab = self.driver.current_window_handle
|
||||
|
||||
try:
|
||||
download_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.XPATH, "//button[contains(.,'Printer Friendly Format')]")
|
||||
)
|
||||
)
|
||||
download_button.click()
|
||||
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||
new_tabs = [t for t in self.driver.window_handles if t != self.eligibility_tab]
|
||||
self.driver.switch_to.window(new_tabs[0])
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||
time.sleep(2)
|
||||
print("Printer-friendly tab opened:", self.driver.current_url)
|
||||
except TimeoutException:
|
||||
print("No new tab for eligibility; printing current page directly")
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"eligibility_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("Eligibility PDF saved at:", pdf_path)
|
||||
|
||||
# Close printer-friendly tab and return to eligibility results
|
||||
if self.driver.current_window_handle != self.eligibility_tab:
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.eligibility_tab)
|
||||
time.sleep(1)
|
||||
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step2_eligibility_pdf] failed: {e}")
|
||||
# Ensure we are back on the eligibility results tab even on error
|
||||
if self.eligibility_tab and self.driver.current_window_handle != self.eligibility_tab:
|
||||
try:
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.eligibility_tab)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
# ── step 3 — click member ID link → member details page ─────────────────────
|
||||
|
||||
def step3_click_member_id(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
substep = "init"
|
||||
try:
|
||||
substep = "ng_bind_link"
|
||||
member_link = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a[ng-bind='member.memberNumber']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].click();", member_link)
|
||||
time.sleep(2)
|
||||
print(f"[step3] clicked member ID link, URL: {self.driver.current_url}")
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[step3] FAILED at substep='{substep}': {e}")
|
||||
return f"ERROR:STEP3:{substep}"
|
||||
|
||||
# ── step 3b — print member details page as PDF via CDP ───────────────────────
|
||||
|
||||
def step3b_member_details_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
try:
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
time.sleep(2)
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"eligibility_member_details_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("Member details PDF saved at:", pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step3b_member_details_pdf] failed: {e}")
|
||||
return None
|
||||
|
||||
# ── step 4 — click "View Service History" on member details page ─────────────
|
||||
|
||||
def step4_view_service_history(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
substep = "init"
|
||||
try:
|
||||
# Primary: href contains /service-history and has btn-primary class
|
||||
substep = "service_history_link"
|
||||
history_link = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "a.btn.btn-primary[href*='service-history']")
|
||||
)
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView(true);", history_link)
|
||||
self.driver.execute_script("arguments[0].click();", history_link)
|
||||
time.sleep(2)
|
||||
print(f"[step4] navigated to service history, URL: {self.driver.current_url}")
|
||||
return "Success"
|
||||
except Exception as e:
|
||||
print(f"[step4] FAILED at substep='{substep}': {e}")
|
||||
return f"ERROR:STEP4:{substep}"
|
||||
|
||||
# ── step 5 — print service history PDF ──────────────────────────────────────
|
||||
|
||||
def step5_history_pdf(self):
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
current_tab = self.driver.current_window_handle
|
||||
try:
|
||||
print_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "button.btn.btn-primary[ng-click='vm.printPage()']")
|
||||
)
|
||||
)
|
||||
print_button.click()
|
||||
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||
new_tabs = [t for t in self.driver.window_handles if t != current_tab]
|
||||
self.driver.switch_to.window(new_tabs[0])
|
||||
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||
time.sleep(2)
|
||||
print("History printer-friendly tab opened:", self.driver.current_url)
|
||||
except TimeoutException:
|
||||
print("No new tab for history; printing current page directly")
|
||||
|
||||
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||
pdf_filename = f"eligibility_history_{safe_member}.pdf"
|
||||
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(pdf_bytes)
|
||||
print("History PDF saved at:", pdf_path)
|
||||
return pdf_path
|
||||
except Exception as e:
|
||||
print(f"[step5_history_pdf] failed: {e}")
|
||||
return None
|
||||
|
||||
# ── main workflow ────────────────────────────────────────────────────────────
|
||||
|
||||
def main_workflow(self, url):
|
||||
try:
|
||||
self.config_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.get(url)
|
||||
time.sleep(3)
|
||||
|
||||
login_result = self.login()
|
||||
if login_result.startswith("ERROR"):
|
||||
return {"status": "error", "message": login_result}
|
||||
|
||||
step1_result = self.step1()
|
||||
if step1_result.startswith("ERROR"):
|
||||
return {"status": "error", "message": step1_result}
|
||||
|
||||
# Print eligibility PDF; stays on results tab afterwards
|
||||
eligibility_pdf_path = self.step2_eligibility_pdf()
|
||||
|
||||
# Click the member ID link → member details
|
||||
step3_result = self.step3_click_member_id()
|
||||
if step3_result.startswith("ERROR"):
|
||||
return {
|
||||
"status": "partial",
|
||||
"message": step3_result,
|
||||
"pdf_path": eligibility_pdf_path,
|
||||
"file_type": "pdf",
|
||||
}
|
||||
|
||||
# Print member details page
|
||||
member_details_pdf_path = self.step3b_member_details_pdf()
|
||||
|
||||
# Click "View Service History"
|
||||
step4_result = self.step4_view_service_history()
|
||||
if step4_result.startswith("ERROR"):
|
||||
return {
|
||||
"status": "partial",
|
||||
"message": step4_result,
|
||||
"pdf_path": eligibility_pdf_path,
|
||||
"member_details_pdf_path": member_details_pdf_path,
|
||||
"file_type": "pdf",
|
||||
}
|
||||
|
||||
# Print history PDF
|
||||
history_pdf_path = self.step5_history_pdf()
|
||||
|
||||
result = {
|
||||
"status": "success",
|
||||
"pdf_path": eligibility_pdf_path,
|
||||
"member_details_pdf_path": member_details_pdf_path,
|
||||
"history_pdf_path": history_pdf_path,
|
||||
"file_type": "pdf",
|
||||
"message": "Eligibility, member details, and service history PDFs captured",
|
||||
}
|
||||
if self.extracted_data:
|
||||
result.update(self.extracted_data)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
finally:
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
@@ -52,6 +52,7 @@ class AutomationUnitedDHClaimSubmit:
|
||||
|
||||
self.uniteddh_username = claim.get("uniteddhUsername", "")
|
||||
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
||||
self.payment_group_id = claim.get("paymentGroupId", "")
|
||||
|
||||
self.download_dir = get_browser_manager().download_dir
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
@@ -595,11 +596,11 @@ class AutomationUnitedDHClaimSubmit:
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
f"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'{self.payment_group_id}')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedDH Claim] step1: Selected Treatment Location: Summit Dental Care")
|
||||
print(f"[UnitedDH Claim] step1: Selected Treatment Location: {self.payment_group_id}")
|
||||
location_selected = True
|
||||
except TimeoutException:
|
||||
try:
|
||||
@@ -632,11 +633,11 @@ class AutomationUnitedDHClaimSubmit:
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
f"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'{self.payment_group_id}')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedDH Claim] step1: Selected Billing Entity: Summit Dental Care")
|
||||
print(f"[UnitedDH Claim] step1: Selected Billing Entity: {self.payment_group_id}")
|
||||
billing_selected = True
|
||||
except TimeoutException:
|
||||
try:
|
||||
|
||||
@@ -52,6 +52,7 @@ class AutomationUnitedDHPreAuth:
|
||||
|
||||
self.uniteddh_username = claim.get("uniteddhUsername", "")
|
||||
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
||||
self.payment_group_id = claim.get("paymentGroupId", "")
|
||||
|
||||
self.download_dir = get_browser_manager().download_dir
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
@@ -554,11 +555,11 @@ class AutomationUnitedDHPreAuth:
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
f"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'{self.payment_group_id}')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedDH PreAuth] step1: Selected Treatment Location: Summit Dental Care")
|
||||
print(f"[UnitedDH PreAuth] step1: Selected Treatment Location: {self.payment_group_id}")
|
||||
location_selected = True
|
||||
except TimeoutException:
|
||||
try:
|
||||
@@ -590,11 +591,11 @@ class AutomationUnitedDHPreAuth:
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
f"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'{self.payment_group_id}')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedDH PreAuth] step1: Selected Billing Entity: Summit Dental Care")
|
||||
print(f"[UnitedDH PreAuth] step1: Selected Billing Entity: {self.payment_group_id}")
|
||||
billing_selected = True
|
||||
except TimeoutException:
|
||||
try:
|
||||
|
||||
@@ -27,6 +27,7 @@ class AutomationUnitedSCOEligibilityCheck:
|
||||
self.lastName = self.data.get("lastName", "")
|
||||
self.unitedsco_username = self.data.get("unitedscoUsername", "")
|
||||
self.unitedsco_password = self.data.get("unitedscoPassword", "")
|
||||
self.payment_group_id = self.data.get("paymentGroupId", "")
|
||||
|
||||
# Use browser manager's download dir
|
||||
self.download_dir = get_browser_manager().download_dir
|
||||
@@ -653,11 +654,11 @@ class AutomationUnitedSCOEligibilityCheck:
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
f"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'{self.payment_group_id}')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedSCO step1] Selected Treatment Location: Summit Dental Care")
|
||||
print(f"[UnitedSCO step1] Selected Treatment Location: {self.payment_group_id}")
|
||||
location_selected = True
|
||||
except TimeoutException:
|
||||
try:
|
||||
@@ -691,11 +692,11 @@ class AutomationUnitedSCOEligibilityCheck:
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
f"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'{self.payment_group_id}')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedSCO step1] Selected Billing Entity: Summit Dental Care")
|
||||
print(f"[UnitedSCO step1] Selected Billing Entity: {self.payment_group_id}")
|
||||
billing_selected = True
|
||||
except TimeoutException:
|
||||
try:
|
||||
|
||||
@@ -71,7 +71,7 @@ echo ""
|
||||
echo "=============================="
|
||||
echo " STEP 13 — Desktop shortcuts "
|
||||
echo "=============================="
|
||||
bash ~/Desktop/DentalManagementMH06/setup-desktop-shortcut.sh
|
||||
bash ~/Desktop/Wellspringmanagement01/setup-desktop-shortcut.sh
|
||||
echo "✓ Desktop shortcuts created"
|
||||
|
||||
echo ""
|
||||
@@ -79,6 +79,6 @@ echo "============================================"
|
||||
echo " All steps complete!"
|
||||
echo ""
|
||||
echo " STEP 12 — To run the app, open 2 terminals:"
|
||||
echo " Terminal 1: cd ~/Desktop/DentalManagementMH06 && npm run dev"
|
||||
echo " Terminal 2: cd ~/Desktop/DentalManagementMH06/apps/SeleniumService && .venv/bin/python3 agent.py"
|
||||
echo " Terminal 1: cd ~/Desktop/Wellspringmanagement01 && npm run dev"
|
||||
echo " Terminal 2: cd ~/Desktop/Wellspringmanagement01/apps/SeleniumService && .venv/bin/python3 agent.py"
|
||||
echo "============================================"
|
||||
|
||||
Generated
+29
-34
@@ -142,6 +142,7 @@
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"postcss": "^8.4.47",
|
||||
"react": "^19.1.0",
|
||||
"react-barcode": "^1.6.1",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
@@ -274,7 +275,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -559,7 +559,8 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@chevrotain/cst-dts-gen": {
|
||||
"version": "10.5.0",
|
||||
@@ -628,8 +629,7 @@
|
||||
"version": "0.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz",
|
||||
"integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@electric-sql/pglite-socket": {
|
||||
"version": "0.0.20",
|
||||
@@ -5013,7 +5013,6 @@
|
||||
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
@@ -5066,7 +5065,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@@ -5131,7 +5129,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -5142,7 +5139,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5281,7 +5277,6 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -5590,7 +5585,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6136,7 +6130,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -7265,8 +7258,7 @@
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/embla-carousel-react": {
|
||||
"version": "8.6.0",
|
||||
@@ -7541,7 +7533,6 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8765,7 +8756,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz",
|
||||
"integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -9139,6 +9129,12 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbarcode": {
|
||||
"version": "3.12.3",
|
||||
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.3.tgz",
|
||||
"integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -9290,6 +9286,7 @@
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.0.tgz",
|
||||
"integrity": "sha512-GGaj5IMRfLv2HXXFzGk9diISMYLTpSTh6fzCZGKxWYW/NqEztIFtnXLq6G/RVhzFRmCykLap1fuC67LVKoQLcg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"p-queue": "6.6.2"
|
||||
},
|
||||
@@ -10141,6 +10138,7 @@
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
@@ -10485,7 +10483,6 @@
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz",
|
||||
"integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
@@ -10563,6 +10560,7 @@
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
@@ -10594,6 +10592,7 @@
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
@@ -10768,7 +10767,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
||||
"integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -10801,7 +10799,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.11.0",
|
||||
"pg-pool": "^3.11.0",
|
||||
@@ -10966,7 +10963,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -11196,7 +11192,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -11889,11 +11884,23 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-barcode": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-barcode/-/react-barcode-1.6.1.tgz",
|
||||
"integrity": "sha512-pc4ftnO5syHa/UjCruEeRsomlhoxKSugIgTA8T4dH0fvc89UMHL+/1Sp25IAphqG44pJkE5hMXhv89iS09jQyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jsbarcode": "^3.8.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16 - 19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-contexify": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-6.0.0.tgz",
|
||||
@@ -11981,7 +11988,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -11994,7 +12000,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
||||
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -12026,7 +12031,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -12307,8 +12311,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -13240,7 +13243,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -13534,7 +13536,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13812,7 +13813,6 @@
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -13994,7 +13994,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14285,7 +14284,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -14834,7 +14832,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15073,7 +15070,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -15196,7 +15192,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -500,6 +500,23 @@ exports.Prisma.PatientConversationScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.LabRxTemplateScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
name: 'name',
|
||||
labName: 'labName',
|
||||
labPhone: 'labPhone',
|
||||
labFax: 'labFax',
|
||||
labAddress: 'labAddress',
|
||||
labAccount: 'labAccount',
|
||||
caseType: 'caseType',
|
||||
material: 'material',
|
||||
instructions: 'instructions',
|
||||
sortOrder: 'sortOrder',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.CommissionBatchScalarFieldEnum = {
|
||||
id: 'id',
|
||||
npiProviderId: 'npiProviderId',
|
||||
@@ -516,6 +533,12 @@ exports.Prisma.CommissionBatchItemScalarFieldEnum = {
|
||||
collectionAmount: 'collectionAmount'
|
||||
};
|
||||
|
||||
exports.Prisma.SeleniumSettingsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
paymentGroupId: 'paymentGroupId'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
@@ -667,8 +690,10 @@ exports.Prisma.ModelName = {
|
||||
InsuranceContact: 'InsuranceContact',
|
||||
ProcedureTimeslot: 'ProcedureTimeslot',
|
||||
PatientConversation: 'PatientConversation',
|
||||
LabRxTemplate: 'LabRxTemplate',
|
||||
CommissionBatch: 'CommissionBatch',
|
||||
CommissionBatchItem: 'CommissionBatchItem'
|
||||
CommissionBatchItem: 'CommissionBatchItem',
|
||||
SeleniumSettings: 'SeleniumSettings'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+3734
-2
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-d351a8e2afa5a626b25c24fc325b9f8c2c9b0ee5bb0f800b83ea538e43d12368",
|
||||
"name": "prisma-client-1397903f012032ff276557e7a077db07df8a7cae6f68fef0b7224d110a42b0e3",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "default.js",
|
||||
|
||||
@@ -50,6 +50,8 @@ model User {
|
||||
procedureTimeslot ProcedureTimeslot?
|
||||
insuranceContacts InsuranceContact[]
|
||||
patientConversations PatientConversation[]
|
||||
labRxTemplates LabRxTemplate[]
|
||||
seleniumSettings SeleniumSettings?
|
||||
}
|
||||
|
||||
model Patient {
|
||||
@@ -694,6 +696,28 @@ model PatientConversation {
|
||||
@@map("patient_conversation")
|
||||
}
|
||||
|
||||
model LabRxTemplate {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
name String
|
||||
labName String?
|
||||
labPhone String?
|
||||
labFax String?
|
||||
labAddress String?
|
||||
labAccount String?
|
||||
caseType String?
|
||||
material String?
|
||||
instructions String?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("lab_rx_template")
|
||||
}
|
||||
|
||||
// Commission tracking
|
||||
model CommissionBatch {
|
||||
id Int @id @default(autoincrement())
|
||||
@@ -721,3 +745,13 @@ model CommissionBatchItem {
|
||||
@@unique([commissionBatchId, paymentId])
|
||||
@@index([paymentId])
|
||||
}
|
||||
|
||||
model SeleniumSettings {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @unique
|
||||
paymentGroupId String @default("")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("selenium_settings")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "lab_rx_template" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"labName" TEXT,
|
||||
"labPhone" TEXT,
|
||||
"labFax" TEXT,
|
||||
"labAddress" TEXT,
|
||||
"labAccount" TEXT,
|
||||
"caseType" TEXT,
|
||||
"material" TEXT,
|
||||
"instructions" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "lab_rx_template_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lab_rx_template_userId_idx" ON "lab_rx_template"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lab_rx_template" ADD CONSTRAINT "lab_rx_template_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "lab_rx_template" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "lab_rx_template" ADD COLUMN "doctorName" TEXT;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "selenium_settings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"paymentGroupId" TEXT NOT NULL DEFAULT '',
|
||||
|
||||
CONSTRAINT "selenium_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "selenium_settings_userId_key" ON "selenium_settings"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "selenium_settings" ADD CONSTRAINT "selenium_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `doctorName` on the `lab_rx_template` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Patient" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "autoMhCheckDayOfWeek" INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN "autoMhCheckEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "autoMhCheckHour" INTEGER NOT NULL DEFAULT 13;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "lab_rx_template" DROP COLUMN "doctorName";
|
||||
@@ -50,6 +50,8 @@ model User {
|
||||
procedureTimeslot ProcedureTimeslot?
|
||||
insuranceContacts InsuranceContact[]
|
||||
patientConversations PatientConversation[]
|
||||
labRxTemplates LabRxTemplate[]
|
||||
seleniumSettings SeleniumSettings?
|
||||
}
|
||||
|
||||
model Patient {
|
||||
@@ -695,6 +697,28 @@ model PatientConversation {
|
||||
@@map("patient_conversation")
|
||||
}
|
||||
|
||||
model LabRxTemplate {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
name String
|
||||
labName String?
|
||||
labPhone String?
|
||||
labFax String?
|
||||
labAddress String?
|
||||
labAccount String?
|
||||
caseType String?
|
||||
material String?
|
||||
instructions String?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("lab_rx_template")
|
||||
}
|
||||
|
||||
// Commission tracking
|
||||
model CommissionBatch {
|
||||
id Int @id @default(autoincrement())
|
||||
@@ -722,3 +746,13 @@ model CommissionBatchItem {
|
||||
@@unique([commissionBatchId, paymentId])
|
||||
@@index([paymentId])
|
||||
}
|
||||
|
||||
model SeleniumSettings {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @unique
|
||||
paymentGroupId String @default("")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("selenium_settings")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"generatorVersion": "1.0.0",
|
||||
"generatedAt": "2026-06-29T02:05:45.074Z",
|
||||
"outputPath": "/home/gg/Desktop/DentalManagementMH06/packages/db/shared",
|
||||
"generatedAt": "2026-09-22T04:11:49.276Z",
|
||||
"outputPath": "/home/kk/Desktop/DentalManagementMH06/packages/db/shared",
|
||||
"files": [
|
||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
||||
@@ -37,8 +37,10 @@
|
||||
"schemas/enums/InsuranceContactScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/ProcedureTimeslotScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/PatientConversationScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/LabRxTemplateScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/CommissionBatchScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/CommissionBatchItemScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/SeleniumSettingsScalarFieldEnum.schema.ts",
|
||||
"schemas/enums/SortOrder.schema.ts",
|
||||
"schemas/enums/NullableJsonNullValueInput.schema.ts",
|
||||
"schemas/enums/JsonNullValueInput.schema.ts",
|
||||
@@ -217,6 +219,11 @@
|
||||
"schemas/objects/PatientConversationWhereUniqueInput.schema.ts",
|
||||
"schemas/objects/PatientConversationOrderByWithAggregationInput.schema.ts",
|
||||
"schemas/objects/PatientConversationScalarWhereWithAggregatesInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateWhereInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateOrderByWithRelationInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateWhereUniqueInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateOrderByWithAggregationInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateScalarWhereWithAggregatesInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchWhereInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchOrderByWithRelationInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchWhereUniqueInput.schema.ts",
|
||||
@@ -227,6 +234,11 @@
|
||||
"schemas/objects/CommissionBatchItemWhereUniqueInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemOrderByWithAggregationInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemScalarWhereWithAggregatesInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsWhereInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsOrderByWithRelationInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsWhereUniqueInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsOrderByWithAggregationInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsScalarWhereWithAggregatesInput.schema.ts",
|
||||
"schemas/objects/UserCreateInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedCreateInput.schema.ts",
|
||||
"schemas/objects/UserUpdateInput.schema.ts",
|
||||
@@ -451,6 +463,13 @@
|
||||
"schemas/objects/PatientConversationCreateManyInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUpdateManyMutationInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUncheckedUpdateManyInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedCreateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpdateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedUpdateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateManyInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpdateManyMutationInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedUpdateManyInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchCreateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchUncheckedCreateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchUpdateInput.schema.ts",
|
||||
@@ -465,6 +484,13 @@
|
||||
"schemas/objects/CommissionBatchItemCreateManyInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemUpdateManyMutationInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemUncheckedUpdateManyInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCreateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedCreateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUpdateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedUpdateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCreateManyInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUpdateManyMutationInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedUpdateManyInput.schema.ts",
|
||||
"schemas/objects/IntFilter.schema.ts",
|
||||
"schemas/objects/StringFilter.schema.ts",
|
||||
"schemas/objects/BoolFilter.schema.ts",
|
||||
@@ -489,6 +515,8 @@
|
||||
"schemas/objects/ProcedureTimeslotNullableScalarRelationFilter.schema.ts",
|
||||
"schemas/objects/InsuranceContactListRelationFilter.schema.ts",
|
||||
"schemas/objects/PatientConversationListRelationFilter.schema.ts",
|
||||
"schemas/objects/LabRxTemplateListRelationFilter.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsNullableScalarRelationFilter.schema.ts",
|
||||
"schemas/objects/PatientOrderByRelationAggregateInput.schema.ts",
|
||||
"schemas/objects/AppointmentOrderByRelationAggregateInput.schema.ts",
|
||||
"schemas/objects/StaffOrderByRelationAggregateInput.schema.ts",
|
||||
@@ -505,6 +533,7 @@
|
||||
"schemas/objects/CommunicationOrderByRelationAggregateInput.schema.ts",
|
||||
"schemas/objects/InsuranceContactOrderByRelationAggregateInput.schema.ts",
|
||||
"schemas/objects/PatientConversationOrderByRelationAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateOrderByRelationAggregateInput.schema.ts",
|
||||
"schemas/objects/UserCountOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/UserAvgOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/UserMaxOrderByAggregateInput.schema.ts",
|
||||
@@ -756,6 +785,11 @@
|
||||
"schemas/objects/PatientConversationMaxOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/PatientConversationMinOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/PatientConversationSumOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCountOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateAvgOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateMaxOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateMinOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateSumOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/NpiProviderScalarRelationFilter.schema.ts",
|
||||
"schemas/objects/CommissionBatchCountOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchAvgOrderByAggregateInput.schema.ts",
|
||||
@@ -769,6 +803,11 @@
|
||||
"schemas/objects/CommissionBatchItemMaxOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemMinOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemSumOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCountOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsAvgOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsMaxOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsMinOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsSumOrderByAggregateInput.schema.ts",
|
||||
"schemas/objects/PatientCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/AppointmentCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/StaffCreateNestedManyWithoutUserInput.schema.ts",
|
||||
@@ -790,6 +829,8 @@
|
||||
"schemas/objects/ProcedureTimeslotCreateNestedOneWithoutUserInput.schema.ts",
|
||||
"schemas/objects/InsuranceContactCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/AppointmentUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/StaffUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||
@@ -811,6 +852,8 @@
|
||||
"schemas/objects/ProcedureTimeslotUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||
"schemas/objects/InsuranceContactUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||
"schemas/objects/StringFieldUpdateOperationsInput.schema.ts",
|
||||
"schemas/objects/BoolFieldUpdateOperationsInput.schema.ts",
|
||||
"schemas/objects/IntFieldUpdateOperationsInput.schema.ts",
|
||||
@@ -835,6 +878,8 @@
|
||||
"schemas/objects/ProcedureTimeslotUpdateOneWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/InsuranceContactUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/PatientUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/AppointmentUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/StaffUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
@@ -856,6 +901,8 @@
|
||||
"schemas/objects/ProcedureTimeslotUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/InsuranceContactUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||
"schemas/objects/UserCreateNestedOneWithoutPatientsInput.schema.ts",
|
||||
"schemas/objects/AppointmentCreateNestedManyWithoutPatientInput.schema.ts",
|
||||
"schemas/objects/AppointmentProcedureCreateNestedManyWithoutPatientInput.schema.ts",
|
||||
@@ -1088,6 +1135,8 @@
|
||||
"schemas/objects/UserCreateNestedOneWithoutPatientConversationsInput.schema.ts",
|
||||
"schemas/objects/PatientUpdateOneRequiredWithoutConversationNestedInput.schema.ts",
|
||||
"schemas/objects/UserUpdateOneRequiredWithoutPatientConversationsNestedInput.schema.ts",
|
||||
"schemas/objects/UserCreateNestedOneWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserUpdateOneRequiredWithoutLabRxTemplatesNestedInput.schema.ts",
|
||||
"schemas/objects/NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemUncheckedCreateNestedManyWithoutCommissionBatchInput.schema.ts",
|
||||
@@ -1098,6 +1147,8 @@
|
||||
"schemas/objects/PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchUpdateOneRequiredWithoutItemsNestedInput.schema.ts",
|
||||
"schemas/objects/PaymentUpdateOneRequiredWithoutCommissionBatchItemsNestedInput.schema.ts",
|
||||
"schemas/objects/UserCreateNestedOneWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserUpdateOneRequiredWithoutSeleniumSettingsNestedInput.schema.ts",
|
||||
"schemas/objects/NestedIntFilter.schema.ts",
|
||||
"schemas/objects/NestedStringFilter.schema.ts",
|
||||
"schemas/objects/NestedBoolFilter.schema.ts",
|
||||
@@ -1227,6 +1278,13 @@
|
||||
"schemas/objects/PatientConversationUncheckedCreateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationCreateOrConnectWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationCreateManyUserInputEnvelope.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedCreateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateOrConnectWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateManyUserInputEnvelope.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCreateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedCreateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCreateOrConnectWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientUpsertWithWhereUniqueWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientUpdateManyWithWhereWithoutUserInput.schema.ts",
|
||||
@@ -1311,6 +1369,14 @@
|
||||
"schemas/objects/PatientConversationUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUpdateManyWithWhereWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationScalarWhereInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpsertWithWhereUniqueWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpdateManyWithWhereWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateScalarWhereInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUpsertWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUpdateToOneWithWhereWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsUncheckedUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/UserCreateWithoutPatientsInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedCreateWithoutPatientsInput.schema.ts",
|
||||
"schemas/objects/UserCreateOrConnectWithoutPatientsInput.schema.ts",
|
||||
@@ -1861,6 +1927,13 @@
|
||||
"schemas/objects/UserUpdateToOneWithWhereWithoutPatientConversationsInput.schema.ts",
|
||||
"schemas/objects/UserUpdateWithoutPatientConversationsInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedUpdateWithoutPatientConversationsInput.schema.ts",
|
||||
"schemas/objects/UserCreateWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedCreateWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserCreateOrConnectWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserUpsertWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserUpdateToOneWithWhereWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserUpdateWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedUpdateWithoutLabRxTemplatesInput.schema.ts",
|
||||
"schemas/objects/NpiProviderCreateWithoutCommissionBatchesInput.schema.ts",
|
||||
"schemas/objects/NpiProviderUncheckedCreateWithoutCommissionBatchesInput.schema.ts",
|
||||
"schemas/objects/NpiProviderCreateOrConnectWithoutCommissionBatchesInput.schema.ts",
|
||||
@@ -1889,6 +1962,13 @@
|
||||
"schemas/objects/PaymentUpdateToOneWithWhereWithoutCommissionBatchItemsInput.schema.ts",
|
||||
"schemas/objects/PaymentUpdateWithoutCommissionBatchItemsInput.schema.ts",
|
||||
"schemas/objects/PaymentUncheckedUpdateWithoutCommissionBatchItemsInput.schema.ts",
|
||||
"schemas/objects/UserCreateWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedCreateWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserCreateOrConnectWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserUpsertWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserUpdateToOneWithWhereWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserUpdateWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/UserUncheckedUpdateWithoutSeleniumSettingsInput.schema.ts",
|
||||
"schemas/objects/PatientCreateManyUserInput.schema.ts",
|
||||
"schemas/objects/AppointmentCreateManyUserInput.schema.ts",
|
||||
"schemas/objects/StaffCreateManyUserInput.schema.ts",
|
||||
@@ -1905,6 +1985,7 @@
|
||||
"schemas/objects/CommunicationCreateManyUserInput.schema.ts",
|
||||
"schemas/objects/InsuranceContactCreateManyUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationCreateManyUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCreateManyUserInput.schema.ts",
|
||||
"schemas/objects/PatientUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientUncheckedUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientUncheckedUpdateManyWithoutUserInput.schema.ts",
|
||||
@@ -1953,6 +2034,9 @@
|
||||
"schemas/objects/PatientConversationUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUncheckedUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/PatientConversationUncheckedUpdateManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedUpdateWithoutUserInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateUncheckedUpdateManyWithoutUserInput.schema.ts",
|
||||
"schemas/objects/AppointmentCreateManyPatientInput.schema.ts",
|
||||
"schemas/objects/AppointmentProcedureCreateManyPatientInput.schema.ts",
|
||||
"schemas/objects/ClaimCreateManyPatientInput.schema.ts",
|
||||
@@ -2225,6 +2309,11 @@
|
||||
"schemas/objects/PatientConversationSumAggregateInput.schema.ts",
|
||||
"schemas/objects/PatientConversationMinAggregateInput.schema.ts",
|
||||
"schemas/objects/PatientConversationMaxAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateCountAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateAvgAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateSumAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateMinAggregateInput.schema.ts",
|
||||
"schemas/objects/LabRxTemplateMaxAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchCountAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchAvgAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchSumAggregateInput.schema.ts",
|
||||
@@ -2235,6 +2324,11 @@
|
||||
"schemas/objects/CommissionBatchItemSumAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemMinAggregateInput.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemMaxAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsCountAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsAvgAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsSumAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsMinAggregateInput.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsMaxAggregateInput.schema.ts",
|
||||
"schemas/objects/UserCountOutputTypeSelect.schema.ts",
|
||||
"schemas/objects/PatientCountOutputTypeSelect.schema.ts",
|
||||
"schemas/objects/AppointmentCountOutputTypeSelect.schema.ts",
|
||||
@@ -2264,6 +2358,7 @@
|
||||
"schemas/objects/UserCountOutputTypeCountCommunicationsArgs.schema.ts",
|
||||
"schemas/objects/UserCountOutputTypeCountInsuranceContactsArgs.schema.ts",
|
||||
"schemas/objects/UserCountOutputTypeCountPatientConversationsArgs.schema.ts",
|
||||
"schemas/objects/UserCountOutputTypeCountLabRxTemplatesArgs.schema.ts",
|
||||
"schemas/objects/PatientCountOutputTypeArgs.schema.ts",
|
||||
"schemas/objects/PatientCountOutputTypeCountAppointmentsArgs.schema.ts",
|
||||
"schemas/objects/PatientCountOutputTypeCountProceduresArgs.schema.ts",
|
||||
@@ -2335,8 +2430,10 @@
|
||||
"schemas/objects/InsuranceContactSelect.schema.ts",
|
||||
"schemas/objects/ProcedureTimeslotSelect.schema.ts",
|
||||
"schemas/objects/PatientConversationSelect.schema.ts",
|
||||
"schemas/objects/LabRxTemplateSelect.schema.ts",
|
||||
"schemas/objects/CommissionBatchSelect.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemSelect.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsSelect.schema.ts",
|
||||
"schemas/objects/UserArgs.schema.ts",
|
||||
"schemas/objects/PatientArgs.schema.ts",
|
||||
"schemas/objects/AppointmentArgs.schema.ts",
|
||||
@@ -2369,8 +2466,10 @@
|
||||
"schemas/objects/InsuranceContactArgs.schema.ts",
|
||||
"schemas/objects/ProcedureTimeslotArgs.schema.ts",
|
||||
"schemas/objects/PatientConversationArgs.schema.ts",
|
||||
"schemas/objects/LabRxTemplateArgs.schema.ts",
|
||||
"schemas/objects/CommissionBatchArgs.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemArgs.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsArgs.schema.ts",
|
||||
"schemas/objects/UserInclude.schema.ts",
|
||||
"schemas/objects/PatientInclude.schema.ts",
|
||||
"schemas/objects/AppointmentInclude.schema.ts",
|
||||
@@ -2402,8 +2501,10 @@
|
||||
"schemas/objects/InsuranceContactInclude.schema.ts",
|
||||
"schemas/objects/ProcedureTimeslotInclude.schema.ts",
|
||||
"schemas/objects/PatientConversationInclude.schema.ts",
|
||||
"schemas/objects/LabRxTemplateInclude.schema.ts",
|
||||
"schemas/objects/CommissionBatchInclude.schema.ts",
|
||||
"schemas/objects/CommissionBatchItemInclude.schema.ts",
|
||||
"schemas/objects/SeleniumSettingsInclude.schema.ts",
|
||||
"schemas/findUniqueUser.schema.ts",
|
||||
"schemas/findUniqueOrThrowUser.schema.ts",
|
||||
"schemas/findFirstUser.schema.ts",
|
||||
@@ -2948,6 +3049,23 @@
|
||||
"schemas/upsertOnePatientConversation.schema.ts",
|
||||
"schemas/aggregatePatientConversation.schema.ts",
|
||||
"schemas/groupByPatientConversation.schema.ts",
|
||||
"schemas/findUniqueLabRxTemplate.schema.ts",
|
||||
"schemas/findUniqueOrThrowLabRxTemplate.schema.ts",
|
||||
"schemas/findFirstLabRxTemplate.schema.ts",
|
||||
"schemas/findFirstOrThrowLabRxTemplate.schema.ts",
|
||||
"schemas/findManyLabRxTemplate.schema.ts",
|
||||
"schemas/countLabRxTemplate.schema.ts",
|
||||
"schemas/createOneLabRxTemplate.schema.ts",
|
||||
"schemas/createManyLabRxTemplate.schema.ts",
|
||||
"schemas/createManyAndReturnLabRxTemplate.schema.ts",
|
||||
"schemas/deleteOneLabRxTemplate.schema.ts",
|
||||
"schemas/deleteManyLabRxTemplate.schema.ts",
|
||||
"schemas/updateOneLabRxTemplate.schema.ts",
|
||||
"schemas/updateManyLabRxTemplate.schema.ts",
|
||||
"schemas/updateManyAndReturnLabRxTemplate.schema.ts",
|
||||
"schemas/upsertOneLabRxTemplate.schema.ts",
|
||||
"schemas/aggregateLabRxTemplate.schema.ts",
|
||||
"schemas/groupByLabRxTemplate.schema.ts",
|
||||
"schemas/findUniqueCommissionBatch.schema.ts",
|
||||
"schemas/findUniqueOrThrowCommissionBatch.schema.ts",
|
||||
"schemas/findFirstCommissionBatch.schema.ts",
|
||||
@@ -2982,6 +3100,23 @@
|
||||
"schemas/upsertOneCommissionBatchItem.schema.ts",
|
||||
"schemas/aggregateCommissionBatchItem.schema.ts",
|
||||
"schemas/groupByCommissionBatchItem.schema.ts",
|
||||
"schemas/findUniqueSeleniumSettings.schema.ts",
|
||||
"schemas/findUniqueOrThrowSeleniumSettings.schema.ts",
|
||||
"schemas/findFirstSeleniumSettings.schema.ts",
|
||||
"schemas/findFirstOrThrowSeleniumSettings.schema.ts",
|
||||
"schemas/findManySeleniumSettings.schema.ts",
|
||||
"schemas/countSeleniumSettings.schema.ts",
|
||||
"schemas/createOneSeleniumSettings.schema.ts",
|
||||
"schemas/createManySeleniumSettings.schema.ts",
|
||||
"schemas/createManyAndReturnSeleniumSettings.schema.ts",
|
||||
"schemas/deleteOneSeleniumSettings.schema.ts",
|
||||
"schemas/deleteManySeleniumSettings.schema.ts",
|
||||
"schemas/updateOneSeleniumSettings.schema.ts",
|
||||
"schemas/updateManySeleniumSettings.schema.ts",
|
||||
"schemas/updateManyAndReturnSeleniumSettings.schema.ts",
|
||||
"schemas/upsertOneSeleniumSettings.schema.ts",
|
||||
"schemas/aggregateSeleniumSettings.schema.ts",
|
||||
"schemas/groupBySeleniumSettings.schema.ts",
|
||||
"schemas/results/UserFindUniqueResult.schema.ts",
|
||||
"schemas/results/UserFindFirstResult.schema.ts",
|
||||
"schemas/results/UserFindManyResult.schema.ts",
|
||||
@@ -3398,6 +3533,19 @@
|
||||
"schemas/results/PatientConversationAggregateResult.schema.ts",
|
||||
"schemas/results/PatientConversationGroupByResult.schema.ts",
|
||||
"schemas/results/PatientConversationCountResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateFindUniqueResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateFindFirstResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateFindManyResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateCreateResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateCreateManyResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateUpdateResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateUpdateManyResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateUpsertResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateDeleteResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateDeleteManyResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateAggregateResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateGroupByResult.schema.ts",
|
||||
"schemas/results/LabRxTemplateCountResult.schema.ts",
|
||||
"schemas/results/CommissionBatchFindUniqueResult.schema.ts",
|
||||
"schemas/results/CommissionBatchFindFirstResult.schema.ts",
|
||||
"schemas/results/CommissionBatchFindManyResult.schema.ts",
|
||||
@@ -3424,6 +3572,19 @@
|
||||
"schemas/results/CommissionBatchItemAggregateResult.schema.ts",
|
||||
"schemas/results/CommissionBatchItemGroupByResult.schema.ts",
|
||||
"schemas/results/CommissionBatchItemCountResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsFindUniqueResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsFindFirstResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsFindManyResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsCreateResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsCreateManyResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsUpdateResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsUpdateManyResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsUpsertResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsDeleteResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsDeleteManyResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsAggregateResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsGroupByResult.schema.ts",
|
||||
"schemas/results/SeleniumSettingsCountResult.schema.ts",
|
||||
"schemas/results/index.ts",
|
||||
"schemas/index.ts",
|
||||
"schemas/variants/pure/User.pure.ts",
|
||||
@@ -3458,8 +3619,10 @@
|
||||
"schemas/variants/pure/InsuranceContact.pure.ts",
|
||||
"schemas/variants/pure/ProcedureTimeslot.pure.ts",
|
||||
"schemas/variants/pure/PatientConversation.pure.ts",
|
||||
"schemas/variants/pure/LabRxTemplate.pure.ts",
|
||||
"schemas/variants/pure/CommissionBatch.pure.ts",
|
||||
"schemas/variants/pure/CommissionBatchItem.pure.ts",
|
||||
"schemas/variants/pure/SeleniumSettings.pure.ts",
|
||||
"schemas/variants/pure/index.ts",
|
||||
"schemas/variants/input/User.input.ts",
|
||||
"schemas/variants/input/Patient.input.ts",
|
||||
@@ -3493,8 +3656,10 @@
|
||||
"schemas/variants/input/InsuranceContact.input.ts",
|
||||
"schemas/variants/input/ProcedureTimeslot.input.ts",
|
||||
"schemas/variants/input/PatientConversation.input.ts",
|
||||
"schemas/variants/input/LabRxTemplate.input.ts",
|
||||
"schemas/variants/input/CommissionBatch.input.ts",
|
||||
"schemas/variants/input/CommissionBatchItem.input.ts",
|
||||
"schemas/variants/input/SeleniumSettings.input.ts",
|
||||
"schemas/variants/input/index.ts",
|
||||
"schemas/variants/result/User.result.ts",
|
||||
"schemas/variants/result/Patient.result.ts",
|
||||
@@ -3528,8 +3693,10 @@
|
||||
"schemas/variants/result/InsuranceContact.result.ts",
|
||||
"schemas/variants/result/ProcedureTimeslot.result.ts",
|
||||
"schemas/variants/result/PatientConversation.result.ts",
|
||||
"schemas/variants/result/LabRxTemplate.result.ts",
|
||||
"schemas/variants/result/CommissionBatch.result.ts",
|
||||
"schemas/variants/result/CommissionBatchItem.result.ts",
|
||||
"schemas/variants/result/SeleniumSettings.result.ts",
|
||||
"schemas/variants/result/index.ts",
|
||||
"schemas/variants/index.ts"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Prisma } from '../../generated/prisma';
|
||||
import Decimal from 'decimal.js';
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
|
||||
import Decimal from "decimal.js";
|
||||
|
||||
// DECIMAL HELPERS
|
||||
//------------------------------------------------------
|
||||
@@ -23,7 +24,7 @@ export const isValidDecimalInput = (
|
||||
if (v === undefined || v === null) return false;
|
||||
return (
|
||||
// Explicit instance checks first
|
||||
v instanceof Prisma.Decimal ||
|
||||
v instanceof Decimal ||
|
||||
// If Decimal.js is present and imported by the generator, this symbol exists at runtime
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Decimal may be undefined when not installed; codegen controls the import
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateOrderByWithRelationInputObjectSchema as LabRxTemplateOrderByWithRelationInputObjectSchema } from './objects/LabRxTemplateOrderByWithRelationInput.schema';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
import { LabRxTemplateCountAggregateInputObjectSchema as LabRxTemplateCountAggregateInputObjectSchema } from './objects/LabRxTemplateCountAggregateInput.schema';
|
||||
import { LabRxTemplateMinAggregateInputObjectSchema as LabRxTemplateMinAggregateInputObjectSchema } from './objects/LabRxTemplateMinAggregateInput.schema';
|
||||
import { LabRxTemplateMaxAggregateInputObjectSchema as LabRxTemplateMaxAggregateInputObjectSchema } from './objects/LabRxTemplateMaxAggregateInput.schema';
|
||||
import { LabRxTemplateAvgAggregateInputObjectSchema as LabRxTemplateAvgAggregateInputObjectSchema } from './objects/LabRxTemplateAvgAggregateInput.schema';
|
||||
import { LabRxTemplateSumAggregateInputObjectSchema as LabRxTemplateSumAggregateInputObjectSchema } from './objects/LabRxTemplateSumAggregateInput.schema';
|
||||
|
||||
export const LabRxTemplateAggregateSchema: z.ZodType<Prisma.LabRxTemplateAggregateArgs> = z.object({ orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), _count: z.union([ z.literal(true), LabRxTemplateCountAggregateInputObjectSchema ]).optional(), _min: LabRxTemplateMinAggregateInputObjectSchema.optional(), _max: LabRxTemplateMaxAggregateInputObjectSchema.optional(), _avg: LabRxTemplateAvgAggregateInputObjectSchema.optional(), _sum: LabRxTemplateSumAggregateInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateAggregateArgs>;
|
||||
|
||||
export const LabRxTemplateAggregateZodSchema = z.object({ orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), _count: z.union([ z.literal(true), LabRxTemplateCountAggregateInputObjectSchema ]).optional(), _min: LabRxTemplateMinAggregateInputObjectSchema.optional(), _max: LabRxTemplateMaxAggregateInputObjectSchema.optional(), _avg: LabRxTemplateAvgAggregateInputObjectSchema.optional(), _sum: LabRxTemplateSumAggregateInputObjectSchema.optional() }).strict();
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsOrderByWithRelationInputObjectSchema as SeleniumSettingsOrderByWithRelationInputObjectSchema } from './objects/SeleniumSettingsOrderByWithRelationInput.schema';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
import { SeleniumSettingsCountAggregateInputObjectSchema as SeleniumSettingsCountAggregateInputObjectSchema } from './objects/SeleniumSettingsCountAggregateInput.schema';
|
||||
import { SeleniumSettingsMinAggregateInputObjectSchema as SeleniumSettingsMinAggregateInputObjectSchema } from './objects/SeleniumSettingsMinAggregateInput.schema';
|
||||
import { SeleniumSettingsMaxAggregateInputObjectSchema as SeleniumSettingsMaxAggregateInputObjectSchema } from './objects/SeleniumSettingsMaxAggregateInput.schema';
|
||||
import { SeleniumSettingsAvgAggregateInputObjectSchema as SeleniumSettingsAvgAggregateInputObjectSchema } from './objects/SeleniumSettingsAvgAggregateInput.schema';
|
||||
import { SeleniumSettingsSumAggregateInputObjectSchema as SeleniumSettingsSumAggregateInputObjectSchema } from './objects/SeleniumSettingsSumAggregateInput.schema';
|
||||
|
||||
export const SeleniumSettingsAggregateSchema: z.ZodType<Prisma.SeleniumSettingsAggregateArgs> = z.object({ orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), _count: z.union([ z.literal(true), SeleniumSettingsCountAggregateInputObjectSchema ]).optional(), _min: SeleniumSettingsMinAggregateInputObjectSchema.optional(), _max: SeleniumSettingsMaxAggregateInputObjectSchema.optional(), _avg: SeleniumSettingsAvgAggregateInputObjectSchema.optional(), _sum: SeleniumSettingsSumAggregateInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsAggregateArgs>;
|
||||
|
||||
export const SeleniumSettingsAggregateZodSchema = z.object({ orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), _count: z.union([ z.literal(true), SeleniumSettingsCountAggregateInputObjectSchema ]).optional(), _min: SeleniumSettingsMinAggregateInputObjectSchema.optional(), _max: SeleniumSettingsMaxAggregateInputObjectSchema.optional(), _avg: SeleniumSettingsAvgAggregateInputObjectSchema.optional(), _sum: SeleniumSettingsSumAggregateInputObjectSchema.optional() }).strict();
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateOrderByWithRelationInputObjectSchema as LabRxTemplateOrderByWithRelationInputObjectSchema } from './objects/LabRxTemplateOrderByWithRelationInput.schema';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
import { LabRxTemplateCountAggregateInputObjectSchema as LabRxTemplateCountAggregateInputObjectSchema } from './objects/LabRxTemplateCountAggregateInput.schema';
|
||||
|
||||
export const LabRxTemplateCountSchema: z.ZodType<Prisma.LabRxTemplateCountArgs> = z.object({ orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), select: z.union([ z.literal(true), LabRxTemplateCountAggregateInputObjectSchema ]).optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateCountArgs>;
|
||||
|
||||
export const LabRxTemplateCountZodSchema = z.object({ orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), select: z.union([ z.literal(true), LabRxTemplateCountAggregateInputObjectSchema ]).optional() }).strict();
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsOrderByWithRelationInputObjectSchema as SeleniumSettingsOrderByWithRelationInputObjectSchema } from './objects/SeleniumSettingsOrderByWithRelationInput.schema';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
import { SeleniumSettingsCountAggregateInputObjectSchema as SeleniumSettingsCountAggregateInputObjectSchema } from './objects/SeleniumSettingsCountAggregateInput.schema';
|
||||
|
||||
export const SeleniumSettingsCountSchema: z.ZodType<Prisma.SeleniumSettingsCountArgs> = z.object({ orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), select: z.union([ z.literal(true), SeleniumSettingsCountAggregateInputObjectSchema ]).optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsCountArgs>;
|
||||
|
||||
export const SeleniumSettingsCountZodSchema = z.object({ orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), select: z.union([ z.literal(true), SeleniumSettingsCountAggregateInputObjectSchema ]).optional() }).strict();
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateSelectObjectSchema as LabRxTemplateSelectObjectSchema } from './objects/LabRxTemplateSelect.schema';
|
||||
import { LabRxTemplateCreateManyInputObjectSchema as LabRxTemplateCreateManyInputObjectSchema } from './objects/LabRxTemplateCreateManyInput.schema';
|
||||
|
||||
export const LabRxTemplateCreateManyAndReturnSchema: z.ZodType<Prisma.LabRxTemplateCreateManyAndReturnArgs> = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), data: z.union([ LabRxTemplateCreateManyInputObjectSchema, z.array(LabRxTemplateCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateCreateManyAndReturnArgs>;
|
||||
|
||||
export const LabRxTemplateCreateManyAndReturnZodSchema = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), data: z.union([ LabRxTemplateCreateManyInputObjectSchema, z.array(LabRxTemplateCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict();
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsSelectObjectSchema as SeleniumSettingsSelectObjectSchema } from './objects/SeleniumSettingsSelect.schema';
|
||||
import { SeleniumSettingsCreateManyInputObjectSchema as SeleniumSettingsCreateManyInputObjectSchema } from './objects/SeleniumSettingsCreateManyInput.schema';
|
||||
|
||||
export const SeleniumSettingsCreateManyAndReturnSchema: z.ZodType<Prisma.SeleniumSettingsCreateManyAndReturnArgs> = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), data: z.union([ SeleniumSettingsCreateManyInputObjectSchema, z.array(SeleniumSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsCreateManyAndReturnArgs>;
|
||||
|
||||
export const SeleniumSettingsCreateManyAndReturnZodSchema = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), data: z.union([ SeleniumSettingsCreateManyInputObjectSchema, z.array(SeleniumSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict();
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateCreateManyInputObjectSchema as LabRxTemplateCreateManyInputObjectSchema } from './objects/LabRxTemplateCreateManyInput.schema';
|
||||
|
||||
export const LabRxTemplateCreateManySchema: z.ZodType<Prisma.LabRxTemplateCreateManyArgs> = z.object({ data: z.union([ LabRxTemplateCreateManyInputObjectSchema, z.array(LabRxTemplateCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateCreateManyArgs>;
|
||||
|
||||
export const LabRxTemplateCreateManyZodSchema = z.object({ data: z.union([ LabRxTemplateCreateManyInputObjectSchema, z.array(LabRxTemplateCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict();
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsCreateManyInputObjectSchema as SeleniumSettingsCreateManyInputObjectSchema } from './objects/SeleniumSettingsCreateManyInput.schema';
|
||||
|
||||
export const SeleniumSettingsCreateManySchema: z.ZodType<Prisma.SeleniumSettingsCreateManyArgs> = z.object({ data: z.union([ SeleniumSettingsCreateManyInputObjectSchema, z.array(SeleniumSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsCreateManyArgs>;
|
||||
|
||||
export const SeleniumSettingsCreateManyZodSchema = z.object({ data: z.union([ SeleniumSettingsCreateManyInputObjectSchema, z.array(SeleniumSettingsCreateManyInputObjectSchema) ]), skipDuplicates: z.boolean().optional() }).strict();
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateSelectObjectSchema as LabRxTemplateSelectObjectSchema } from './objects/LabRxTemplateSelect.schema';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateCreateInputObjectSchema as LabRxTemplateCreateInputObjectSchema } from './objects/LabRxTemplateCreateInput.schema';
|
||||
import { LabRxTemplateUncheckedCreateInputObjectSchema as LabRxTemplateUncheckedCreateInputObjectSchema } from './objects/LabRxTemplateUncheckedCreateInput.schema';
|
||||
|
||||
export const LabRxTemplateCreateOneSchema: z.ZodType<Prisma.LabRxTemplateCreateArgs> = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), data: z.union([LabRxTemplateCreateInputObjectSchema, LabRxTemplateUncheckedCreateInputObjectSchema]) }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateCreateArgs>;
|
||||
|
||||
export const LabRxTemplateCreateOneZodSchema = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), data: z.union([LabRxTemplateCreateInputObjectSchema, LabRxTemplateUncheckedCreateInputObjectSchema]) }).strict();
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsSelectObjectSchema as SeleniumSettingsSelectObjectSchema } from './objects/SeleniumSettingsSelect.schema';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsCreateInputObjectSchema as SeleniumSettingsCreateInputObjectSchema } from './objects/SeleniumSettingsCreateInput.schema';
|
||||
import { SeleniumSettingsUncheckedCreateInputObjectSchema as SeleniumSettingsUncheckedCreateInputObjectSchema } from './objects/SeleniumSettingsUncheckedCreateInput.schema';
|
||||
|
||||
export const SeleniumSettingsCreateOneSchema: z.ZodType<Prisma.SeleniumSettingsCreateArgs> = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), data: z.union([SeleniumSettingsCreateInputObjectSchema, SeleniumSettingsUncheckedCreateInputObjectSchema]) }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsCreateArgs>;
|
||||
|
||||
export const SeleniumSettingsCreateOneZodSchema = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), data: z.union([SeleniumSettingsCreateInputObjectSchema, SeleniumSettingsUncheckedCreateInputObjectSchema]) }).strict();
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
|
||||
export const LabRxTemplateDeleteManySchema: z.ZodType<Prisma.LabRxTemplateDeleteManyArgs> = z.object({ where: LabRxTemplateWhereInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateDeleteManyArgs>;
|
||||
|
||||
export const LabRxTemplateDeleteManyZodSchema = z.object({ where: LabRxTemplateWhereInputObjectSchema.optional() }).strict();
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
|
||||
export const SeleniumSettingsDeleteManySchema: z.ZodType<Prisma.SeleniumSettingsDeleteManyArgs> = z.object({ where: SeleniumSettingsWhereInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsDeleteManyArgs>;
|
||||
|
||||
export const SeleniumSettingsDeleteManyZodSchema = z.object({ where: SeleniumSettingsWhereInputObjectSchema.optional() }).strict();
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateSelectObjectSchema as LabRxTemplateSelectObjectSchema } from './objects/LabRxTemplateSelect.schema';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
|
||||
export const LabRxTemplateDeleteOneSchema: z.ZodType<Prisma.LabRxTemplateDeleteArgs> = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), where: LabRxTemplateWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateDeleteArgs>;
|
||||
|
||||
export const LabRxTemplateDeleteOneZodSchema = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), where: LabRxTemplateWhereUniqueInputObjectSchema }).strict();
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsSelectObjectSchema as SeleniumSettingsSelectObjectSchema } from './objects/SeleniumSettingsSelect.schema';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
|
||||
export const SeleniumSettingsDeleteOneSchema: z.ZodType<Prisma.SeleniumSettingsDeleteArgs> = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), where: SeleniumSettingsWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsDeleteArgs>;
|
||||
|
||||
export const SeleniumSettingsDeleteOneZodSchema = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), where: SeleniumSettingsWhereUniqueInputObjectSchema }).strict();
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const LabRxTemplateScalarFieldEnumSchema = z.enum(['id', 'userId', 'name', 'labName', 'labPhone', 'labFax', 'labAddress', 'labAccount', 'caseType', 'material', 'instructions', 'sortOrder', 'createdAt', 'updatedAt'])
|
||||
|
||||
export type LabRxTemplateScalarFieldEnum = z.infer<typeof LabRxTemplateScalarFieldEnumSchema>;
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const SeleniumSettingsScalarFieldEnumSchema = z.enum(['id', 'userId', 'paymentGroupId'])
|
||||
|
||||
export type SeleniumSettingsScalarFieldEnum = z.infer<typeof SeleniumSettingsScalarFieldEnumSchema>;
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateOrderByWithRelationInputObjectSchema as LabRxTemplateOrderByWithRelationInputObjectSchema } from './objects/LabRxTemplateOrderByWithRelationInput.schema';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
import { LabRxTemplateScalarFieldEnumSchema } from './enums/LabRxTemplateScalarFieldEnum.schema';
|
||||
|
||||
// Select schema needs to be in file to prevent circular imports
|
||||
//------------------------------------------------------
|
||||
|
||||
export const LabRxTemplateFindFirstSelectSchema: z.ZodType<Prisma.LabRxTemplateSelect> = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
name: z.boolean().optional(),
|
||||
labName: z.boolean().optional(),
|
||||
labPhone: z.boolean().optional(),
|
||||
labFax: z.boolean().optional(),
|
||||
labAddress: z.boolean().optional(),
|
||||
labAccount: z.boolean().optional(),
|
||||
caseType: z.boolean().optional(),
|
||||
material: z.boolean().optional(),
|
||||
instructions: z.boolean().optional(),
|
||||
sortOrder: z.boolean().optional(),
|
||||
createdAt: z.boolean().optional(),
|
||||
updatedAt: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.LabRxTemplateSelect>;
|
||||
|
||||
export const LabRxTemplateFindFirstSelectZodSchema = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
name: z.boolean().optional(),
|
||||
labName: z.boolean().optional(),
|
||||
labPhone: z.boolean().optional(),
|
||||
labFax: z.boolean().optional(),
|
||||
labAddress: z.boolean().optional(),
|
||||
labAccount: z.boolean().optional(),
|
||||
caseType: z.boolean().optional(),
|
||||
material: z.boolean().optional(),
|
||||
instructions: z.boolean().optional(),
|
||||
sortOrder: z.boolean().optional(),
|
||||
createdAt: z.boolean().optional(),
|
||||
updatedAt: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
export const LabRxTemplateFindFirstSchema: z.ZodType<Prisma.LabRxTemplateFindFirstArgs> = z.object({ select: LabRxTemplateFindFirstSelectSchema.optional(), include: z.lazy(() => LabRxTemplateIncludeObjectSchema.optional()), orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([LabRxTemplateScalarFieldEnumSchema, LabRxTemplateScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateFindFirstArgs>;
|
||||
|
||||
export const LabRxTemplateFindFirstZodSchema = z.object({ select: LabRxTemplateFindFirstSelectSchema.optional(), include: z.lazy(() => LabRxTemplateIncludeObjectSchema.optional()), orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([LabRxTemplateScalarFieldEnumSchema, LabRxTemplateScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateOrderByWithRelationInputObjectSchema as LabRxTemplateOrderByWithRelationInputObjectSchema } from './objects/LabRxTemplateOrderByWithRelationInput.schema';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
import { LabRxTemplateScalarFieldEnumSchema } from './enums/LabRxTemplateScalarFieldEnum.schema';
|
||||
|
||||
// Select schema needs to be in file to prevent circular imports
|
||||
//------------------------------------------------------
|
||||
|
||||
export const LabRxTemplateFindFirstOrThrowSelectSchema: z.ZodType<Prisma.LabRxTemplateSelect> = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
name: z.boolean().optional(),
|
||||
labName: z.boolean().optional(),
|
||||
labPhone: z.boolean().optional(),
|
||||
labFax: z.boolean().optional(),
|
||||
labAddress: z.boolean().optional(),
|
||||
labAccount: z.boolean().optional(),
|
||||
caseType: z.boolean().optional(),
|
||||
material: z.boolean().optional(),
|
||||
instructions: z.boolean().optional(),
|
||||
sortOrder: z.boolean().optional(),
|
||||
createdAt: z.boolean().optional(),
|
||||
updatedAt: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.LabRxTemplateSelect>;
|
||||
|
||||
export const LabRxTemplateFindFirstOrThrowSelectZodSchema = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
name: z.boolean().optional(),
|
||||
labName: z.boolean().optional(),
|
||||
labPhone: z.boolean().optional(),
|
||||
labFax: z.boolean().optional(),
|
||||
labAddress: z.boolean().optional(),
|
||||
labAccount: z.boolean().optional(),
|
||||
caseType: z.boolean().optional(),
|
||||
material: z.boolean().optional(),
|
||||
instructions: z.boolean().optional(),
|
||||
sortOrder: z.boolean().optional(),
|
||||
createdAt: z.boolean().optional(),
|
||||
updatedAt: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
export const LabRxTemplateFindFirstOrThrowSchema: z.ZodType<Prisma.LabRxTemplateFindFirstOrThrowArgs> = z.object({ select: LabRxTemplateFindFirstOrThrowSelectSchema.optional(), include: z.lazy(() => LabRxTemplateIncludeObjectSchema.optional()), orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([LabRxTemplateScalarFieldEnumSchema, LabRxTemplateScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateFindFirstOrThrowArgs>;
|
||||
|
||||
export const LabRxTemplateFindFirstOrThrowZodSchema = z.object({ select: LabRxTemplateFindFirstOrThrowSelectSchema.optional(), include: z.lazy(() => LabRxTemplateIncludeObjectSchema.optional()), orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([LabRxTemplateScalarFieldEnumSchema, LabRxTemplateScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsOrderByWithRelationInputObjectSchema as SeleniumSettingsOrderByWithRelationInputObjectSchema } from './objects/SeleniumSettingsOrderByWithRelationInput.schema';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
import { SeleniumSettingsScalarFieldEnumSchema } from './enums/SeleniumSettingsScalarFieldEnum.schema';
|
||||
|
||||
// Select schema needs to be in file to prevent circular imports
|
||||
//------------------------------------------------------
|
||||
|
||||
export const SeleniumSettingsFindFirstOrThrowSelectSchema: z.ZodType<Prisma.SeleniumSettingsSelect> = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
paymentGroupId: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsSelect>;
|
||||
|
||||
export const SeleniumSettingsFindFirstOrThrowSelectZodSchema = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
paymentGroupId: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
export const SeleniumSettingsFindFirstOrThrowSchema: z.ZodType<Prisma.SeleniumSettingsFindFirstOrThrowArgs> = z.object({ select: SeleniumSettingsFindFirstOrThrowSelectSchema.optional(), include: z.lazy(() => SeleniumSettingsIncludeObjectSchema.optional()), orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([SeleniumSettingsScalarFieldEnumSchema, SeleniumSettingsScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsFindFirstOrThrowArgs>;
|
||||
|
||||
export const SeleniumSettingsFindFirstOrThrowZodSchema = z.object({ select: SeleniumSettingsFindFirstOrThrowSelectSchema.optional(), include: z.lazy(() => SeleniumSettingsIncludeObjectSchema.optional()), orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([SeleniumSettingsScalarFieldEnumSchema, SeleniumSettingsScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||
@@ -41,6 +41,8 @@ export const UserFindFirstOrThrowSelectSchema: z.ZodType<Prisma.UserSelect> = z.
|
||||
procedureTimeslot: z.boolean().optional(),
|
||||
insuranceContacts: z.boolean().optional(),
|
||||
patientConversations: z.boolean().optional(),
|
||||
labRxTemplates: z.boolean().optional(),
|
||||
seleniumSettings: z.boolean().optional(),
|
||||
_count: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
||||
|
||||
@@ -76,6 +78,8 @@ export const UserFindFirstOrThrowSelectZodSchema = z.object({
|
||||
procedureTimeslot: z.boolean().optional(),
|
||||
insuranceContacts: z.boolean().optional(),
|
||||
patientConversations: z.boolean().optional(),
|
||||
labRxTemplates: z.boolean().optional(),
|
||||
seleniumSettings: z.boolean().optional(),
|
||||
_count: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsOrderByWithRelationInputObjectSchema as SeleniumSettingsOrderByWithRelationInputObjectSchema } from './objects/SeleniumSettingsOrderByWithRelationInput.schema';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
import { SeleniumSettingsScalarFieldEnumSchema } from './enums/SeleniumSettingsScalarFieldEnum.schema';
|
||||
|
||||
// Select schema needs to be in file to prevent circular imports
|
||||
//------------------------------------------------------
|
||||
|
||||
export const SeleniumSettingsFindFirstSelectSchema: z.ZodType<Prisma.SeleniumSettingsSelect> = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
paymentGroupId: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsSelect>;
|
||||
|
||||
export const SeleniumSettingsFindFirstSelectZodSchema = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
paymentGroupId: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
export const SeleniumSettingsFindFirstSchema: z.ZodType<Prisma.SeleniumSettingsFindFirstArgs> = z.object({ select: SeleniumSettingsFindFirstSelectSchema.optional(), include: z.lazy(() => SeleniumSettingsIncludeObjectSchema.optional()), orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([SeleniumSettingsScalarFieldEnumSchema, SeleniumSettingsScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsFindFirstArgs>;
|
||||
|
||||
export const SeleniumSettingsFindFirstZodSchema = z.object({ select: SeleniumSettingsFindFirstSelectSchema.optional(), include: z.lazy(() => SeleniumSettingsIncludeObjectSchema.optional()), orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([SeleniumSettingsScalarFieldEnumSchema, SeleniumSettingsScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||
@@ -41,6 +41,8 @@ export const UserFindFirstSelectSchema: z.ZodType<Prisma.UserSelect> = z.object(
|
||||
procedureTimeslot: z.boolean().optional(),
|
||||
insuranceContacts: z.boolean().optional(),
|
||||
patientConversations: z.boolean().optional(),
|
||||
labRxTemplates: z.boolean().optional(),
|
||||
seleniumSettings: z.boolean().optional(),
|
||||
_count: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
||||
|
||||
@@ -76,6 +78,8 @@ export const UserFindFirstSelectZodSchema = z.object({
|
||||
procedureTimeslot: z.boolean().optional(),
|
||||
insuranceContacts: z.boolean().optional(),
|
||||
patientConversations: z.boolean().optional(),
|
||||
labRxTemplates: z.boolean().optional(),
|
||||
seleniumSettings: z.boolean().optional(),
|
||||
_count: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateOrderByWithRelationInputObjectSchema as LabRxTemplateOrderByWithRelationInputObjectSchema } from './objects/LabRxTemplateOrderByWithRelationInput.schema';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
import { LabRxTemplateScalarFieldEnumSchema } from './enums/LabRxTemplateScalarFieldEnum.schema';
|
||||
|
||||
// Select schema needs to be in file to prevent circular imports
|
||||
//------------------------------------------------------
|
||||
|
||||
export const LabRxTemplateFindManySelectSchema: z.ZodType<Prisma.LabRxTemplateSelect> = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
name: z.boolean().optional(),
|
||||
labName: z.boolean().optional(),
|
||||
labPhone: z.boolean().optional(),
|
||||
labFax: z.boolean().optional(),
|
||||
labAddress: z.boolean().optional(),
|
||||
labAccount: z.boolean().optional(),
|
||||
caseType: z.boolean().optional(),
|
||||
material: z.boolean().optional(),
|
||||
instructions: z.boolean().optional(),
|
||||
sortOrder: z.boolean().optional(),
|
||||
createdAt: z.boolean().optional(),
|
||||
updatedAt: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.LabRxTemplateSelect>;
|
||||
|
||||
export const LabRxTemplateFindManySelectZodSchema = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
name: z.boolean().optional(),
|
||||
labName: z.boolean().optional(),
|
||||
labPhone: z.boolean().optional(),
|
||||
labFax: z.boolean().optional(),
|
||||
labAddress: z.boolean().optional(),
|
||||
labAccount: z.boolean().optional(),
|
||||
caseType: z.boolean().optional(),
|
||||
material: z.boolean().optional(),
|
||||
instructions: z.boolean().optional(),
|
||||
sortOrder: z.boolean().optional(),
|
||||
createdAt: z.boolean().optional(),
|
||||
updatedAt: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
export const LabRxTemplateFindManySchema: z.ZodType<Prisma.LabRxTemplateFindManyArgs> = z.object({ select: LabRxTemplateFindManySelectSchema.optional(), include: z.lazy(() => LabRxTemplateIncludeObjectSchema.optional()), orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([LabRxTemplateScalarFieldEnumSchema, LabRxTemplateScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateFindManyArgs>;
|
||||
|
||||
export const LabRxTemplateFindManyZodSchema = z.object({ select: LabRxTemplateFindManySelectSchema.optional(), include: z.lazy(() => LabRxTemplateIncludeObjectSchema.optional()), orderBy: z.union([LabRxTemplateOrderByWithRelationInputObjectSchema, LabRxTemplateOrderByWithRelationInputObjectSchema.array()]).optional(), where: LabRxTemplateWhereInputObjectSchema.optional(), cursor: LabRxTemplateWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([LabRxTemplateScalarFieldEnumSchema, LabRxTemplateScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsOrderByWithRelationInputObjectSchema as SeleniumSettingsOrderByWithRelationInputObjectSchema } from './objects/SeleniumSettingsOrderByWithRelationInput.schema';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
import { SeleniumSettingsScalarFieldEnumSchema } from './enums/SeleniumSettingsScalarFieldEnum.schema';
|
||||
|
||||
// Select schema needs to be in file to prevent circular imports
|
||||
//------------------------------------------------------
|
||||
|
||||
export const SeleniumSettingsFindManySelectSchema: z.ZodType<Prisma.SeleniumSettingsSelect> = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
paymentGroupId: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsSelect>;
|
||||
|
||||
export const SeleniumSettingsFindManySelectZodSchema = z.object({
|
||||
id: z.boolean().optional(),
|
||||
userId: z.boolean().optional(),
|
||||
paymentGroupId: z.boolean().optional(),
|
||||
user: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
export const SeleniumSettingsFindManySchema: z.ZodType<Prisma.SeleniumSettingsFindManyArgs> = z.object({ select: SeleniumSettingsFindManySelectSchema.optional(), include: z.lazy(() => SeleniumSettingsIncludeObjectSchema.optional()), orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([SeleniumSettingsScalarFieldEnumSchema, SeleniumSettingsScalarFieldEnumSchema.array()]).optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsFindManyArgs>;
|
||||
|
||||
export const SeleniumSettingsFindManyZodSchema = z.object({ select: SeleniumSettingsFindManySelectSchema.optional(), include: z.lazy(() => SeleniumSettingsIncludeObjectSchema.optional()), orderBy: z.union([SeleniumSettingsOrderByWithRelationInputObjectSchema, SeleniumSettingsOrderByWithRelationInputObjectSchema.array()]).optional(), where: SeleniumSettingsWhereInputObjectSchema.optional(), cursor: SeleniumSettingsWhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.union([SeleniumSettingsScalarFieldEnumSchema, SeleniumSettingsScalarFieldEnumSchema.array()]).optional() }).strict();
|
||||
@@ -41,6 +41,8 @@ export const UserFindManySelectSchema: z.ZodType<Prisma.UserSelect> = z.object({
|
||||
procedureTimeslot: z.boolean().optional(),
|
||||
insuranceContacts: z.boolean().optional(),
|
||||
patientConversations: z.boolean().optional(),
|
||||
labRxTemplates: z.boolean().optional(),
|
||||
seleniumSettings: z.boolean().optional(),
|
||||
_count: z.boolean().optional()
|
||||
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
||||
|
||||
@@ -76,6 +78,8 @@ export const UserFindManySelectZodSchema = z.object({
|
||||
procedureTimeslot: z.boolean().optional(),
|
||||
insuranceContacts: z.boolean().optional(),
|
||||
patientConversations: z.boolean().optional(),
|
||||
labRxTemplates: z.boolean().optional(),
|
||||
seleniumSettings: z.boolean().optional(),
|
||||
_count: z.boolean().optional()
|
||||
}).strict();
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateSelectObjectSchema as LabRxTemplateSelectObjectSchema } from './objects/LabRxTemplateSelect.schema';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
|
||||
export const LabRxTemplateFindUniqueSchema: z.ZodType<Prisma.LabRxTemplateFindUniqueArgs> = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), where: LabRxTemplateWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateFindUniqueArgs>;
|
||||
|
||||
export const LabRxTemplateFindUniqueZodSchema = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), where: LabRxTemplateWhereUniqueInputObjectSchema }).strict();
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateSelectObjectSchema as LabRxTemplateSelectObjectSchema } from './objects/LabRxTemplateSelect.schema';
|
||||
import { LabRxTemplateIncludeObjectSchema as LabRxTemplateIncludeObjectSchema } from './objects/LabRxTemplateInclude.schema';
|
||||
import { LabRxTemplateWhereUniqueInputObjectSchema as LabRxTemplateWhereUniqueInputObjectSchema } from './objects/LabRxTemplateWhereUniqueInput.schema';
|
||||
|
||||
export const LabRxTemplateFindUniqueOrThrowSchema: z.ZodType<Prisma.LabRxTemplateFindUniqueOrThrowArgs> = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), where: LabRxTemplateWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateFindUniqueOrThrowArgs>;
|
||||
|
||||
export const LabRxTemplateFindUniqueOrThrowZodSchema = z.object({ select: LabRxTemplateSelectObjectSchema.optional(), include: LabRxTemplateIncludeObjectSchema.optional(), where: LabRxTemplateWhereUniqueInputObjectSchema }).strict();
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsSelectObjectSchema as SeleniumSettingsSelectObjectSchema } from './objects/SeleniumSettingsSelect.schema';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
|
||||
export const SeleniumSettingsFindUniqueOrThrowSchema: z.ZodType<Prisma.SeleniumSettingsFindUniqueOrThrowArgs> = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), where: SeleniumSettingsWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsFindUniqueOrThrowArgs>;
|
||||
|
||||
export const SeleniumSettingsFindUniqueOrThrowZodSchema = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), where: SeleniumSettingsWhereUniqueInputObjectSchema }).strict();
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsSelectObjectSchema as SeleniumSettingsSelectObjectSchema } from './objects/SeleniumSettingsSelect.schema';
|
||||
import { SeleniumSettingsIncludeObjectSchema as SeleniumSettingsIncludeObjectSchema } from './objects/SeleniumSettingsInclude.schema';
|
||||
import { SeleniumSettingsWhereUniqueInputObjectSchema as SeleniumSettingsWhereUniqueInputObjectSchema } from './objects/SeleniumSettingsWhereUniqueInput.schema';
|
||||
|
||||
export const SeleniumSettingsFindUniqueSchema: z.ZodType<Prisma.SeleniumSettingsFindUniqueArgs> = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), where: SeleniumSettingsWhereUniqueInputObjectSchema }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsFindUniqueArgs>;
|
||||
|
||||
export const SeleniumSettingsFindUniqueZodSchema = z.object({ select: SeleniumSettingsSelectObjectSchema.optional(), include: SeleniumSettingsIncludeObjectSchema.optional(), where: SeleniumSettingsWhereUniqueInputObjectSchema }).strict();
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { LabRxTemplateWhereInputObjectSchema as LabRxTemplateWhereInputObjectSchema } from './objects/LabRxTemplateWhereInput.schema';
|
||||
import { LabRxTemplateOrderByWithAggregationInputObjectSchema as LabRxTemplateOrderByWithAggregationInputObjectSchema } from './objects/LabRxTemplateOrderByWithAggregationInput.schema';
|
||||
import { LabRxTemplateScalarWhereWithAggregatesInputObjectSchema as LabRxTemplateScalarWhereWithAggregatesInputObjectSchema } from './objects/LabRxTemplateScalarWhereWithAggregatesInput.schema';
|
||||
import { LabRxTemplateScalarFieldEnumSchema } from './enums/LabRxTemplateScalarFieldEnum.schema';
|
||||
import { LabRxTemplateCountAggregateInputObjectSchema as LabRxTemplateCountAggregateInputObjectSchema } from './objects/LabRxTemplateCountAggregateInput.schema';
|
||||
import { LabRxTemplateMinAggregateInputObjectSchema as LabRxTemplateMinAggregateInputObjectSchema } from './objects/LabRxTemplateMinAggregateInput.schema';
|
||||
import { LabRxTemplateMaxAggregateInputObjectSchema as LabRxTemplateMaxAggregateInputObjectSchema } from './objects/LabRxTemplateMaxAggregateInput.schema';
|
||||
import { LabRxTemplateAvgAggregateInputObjectSchema as LabRxTemplateAvgAggregateInputObjectSchema } from './objects/LabRxTemplateAvgAggregateInput.schema';
|
||||
import { LabRxTemplateSumAggregateInputObjectSchema as LabRxTemplateSumAggregateInputObjectSchema } from './objects/LabRxTemplateSumAggregateInput.schema';
|
||||
|
||||
export const LabRxTemplateGroupBySchema: z.ZodType<Prisma.LabRxTemplateGroupByArgs> = z.object({ where: LabRxTemplateWhereInputObjectSchema.optional(), orderBy: z.union([LabRxTemplateOrderByWithAggregationInputObjectSchema, LabRxTemplateOrderByWithAggregationInputObjectSchema.array()]).optional(), having: LabRxTemplateScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(LabRxTemplateScalarFieldEnumSchema), _count: z.union([ z.literal(true), LabRxTemplateCountAggregateInputObjectSchema ]).optional(), _min: LabRxTemplateMinAggregateInputObjectSchema.optional(), _max: LabRxTemplateMaxAggregateInputObjectSchema.optional(), _avg: LabRxTemplateAvgAggregateInputObjectSchema.optional(), _sum: LabRxTemplateSumAggregateInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.LabRxTemplateGroupByArgs>;
|
||||
|
||||
export const LabRxTemplateGroupByZodSchema = z.object({ where: LabRxTemplateWhereInputObjectSchema.optional(), orderBy: z.union([LabRxTemplateOrderByWithAggregationInputObjectSchema, LabRxTemplateOrderByWithAggregationInputObjectSchema.array()]).optional(), having: LabRxTemplateScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(LabRxTemplateScalarFieldEnumSchema), _count: z.union([ z.literal(true), LabRxTemplateCountAggregateInputObjectSchema ]).optional(), _min: LabRxTemplateMinAggregateInputObjectSchema.optional(), _max: LabRxTemplateMaxAggregateInputObjectSchema.optional(), _avg: LabRxTemplateAvgAggregateInputObjectSchema.optional(), _sum: LabRxTemplateSumAggregateInputObjectSchema.optional() }).strict();
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import * as z from 'zod';
|
||||
import { SeleniumSettingsWhereInputObjectSchema as SeleniumSettingsWhereInputObjectSchema } from './objects/SeleniumSettingsWhereInput.schema';
|
||||
import { SeleniumSettingsOrderByWithAggregationInputObjectSchema as SeleniumSettingsOrderByWithAggregationInputObjectSchema } from './objects/SeleniumSettingsOrderByWithAggregationInput.schema';
|
||||
import { SeleniumSettingsScalarWhereWithAggregatesInputObjectSchema as SeleniumSettingsScalarWhereWithAggregatesInputObjectSchema } from './objects/SeleniumSettingsScalarWhereWithAggregatesInput.schema';
|
||||
import { SeleniumSettingsScalarFieldEnumSchema } from './enums/SeleniumSettingsScalarFieldEnum.schema';
|
||||
import { SeleniumSettingsCountAggregateInputObjectSchema as SeleniumSettingsCountAggregateInputObjectSchema } from './objects/SeleniumSettingsCountAggregateInput.schema';
|
||||
import { SeleniumSettingsMinAggregateInputObjectSchema as SeleniumSettingsMinAggregateInputObjectSchema } from './objects/SeleniumSettingsMinAggregateInput.schema';
|
||||
import { SeleniumSettingsMaxAggregateInputObjectSchema as SeleniumSettingsMaxAggregateInputObjectSchema } from './objects/SeleniumSettingsMaxAggregateInput.schema';
|
||||
import { SeleniumSettingsAvgAggregateInputObjectSchema as SeleniumSettingsAvgAggregateInputObjectSchema } from './objects/SeleniumSettingsAvgAggregateInput.schema';
|
||||
import { SeleniumSettingsSumAggregateInputObjectSchema as SeleniumSettingsSumAggregateInputObjectSchema } from './objects/SeleniumSettingsSumAggregateInput.schema';
|
||||
|
||||
export const SeleniumSettingsGroupBySchema: z.ZodType<Prisma.SeleniumSettingsGroupByArgs> = z.object({ where: SeleniumSettingsWhereInputObjectSchema.optional(), orderBy: z.union([SeleniumSettingsOrderByWithAggregationInputObjectSchema, SeleniumSettingsOrderByWithAggregationInputObjectSchema.array()]).optional(), having: SeleniumSettingsScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(SeleniumSettingsScalarFieldEnumSchema), _count: z.union([ z.literal(true), SeleniumSettingsCountAggregateInputObjectSchema ]).optional(), _min: SeleniumSettingsMinAggregateInputObjectSchema.optional(), _max: SeleniumSettingsMaxAggregateInputObjectSchema.optional(), _avg: SeleniumSettingsAvgAggregateInputObjectSchema.optional(), _sum: SeleniumSettingsSumAggregateInputObjectSchema.optional() }).strict() as unknown as z.ZodType<Prisma.SeleniumSettingsGroupByArgs>;
|
||||
|
||||
export const SeleniumSettingsGroupByZodSchema = z.object({ where: SeleniumSettingsWhereInputObjectSchema.optional(), orderBy: z.union([SeleniumSettingsOrderByWithAggregationInputObjectSchema, SeleniumSettingsOrderByWithAggregationInputObjectSchema.array()]).optional(), having: SeleniumSettingsScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(SeleniumSettingsScalarFieldEnumSchema), _count: z.union([ z.literal(true), SeleniumSettingsCountAggregateInputObjectSchema ]).optional(), _min: SeleniumSettingsMinAggregateInputObjectSchema.optional(), _max: SeleniumSettingsMaxAggregateInputObjectSchema.optional(), _avg: SeleniumSettingsAvgAggregateInputObjectSchema.optional(), _sum: SeleniumSettingsSumAggregateInputObjectSchema.optional() }).strict();
|
||||
@@ -31,8 +31,10 @@ export * from './enums/OfficeContactScalarFieldEnum.schema'
|
||||
export * from './enums/InsuranceContactScalarFieldEnum.schema'
|
||||
export * from './enums/ProcedureTimeslotScalarFieldEnum.schema'
|
||||
export * from './enums/PatientConversationScalarFieldEnum.schema'
|
||||
export * from './enums/LabRxTemplateScalarFieldEnum.schema'
|
||||
export * from './enums/CommissionBatchScalarFieldEnum.schema'
|
||||
export * from './enums/CommissionBatchItemScalarFieldEnum.schema'
|
||||
export * from './enums/SeleniumSettingsScalarFieldEnum.schema'
|
||||
export * from './enums/SortOrder.schema'
|
||||
export * from './enums/NullableJsonNullValueInput.schema'
|
||||
export * from './enums/JsonNullValueInput.schema'
|
||||
@@ -595,6 +597,23 @@ export * from './updateManyAndReturnPatientConversation.schema'
|
||||
export * from './upsertOnePatientConversation.schema'
|
||||
export * from './aggregatePatientConversation.schema'
|
||||
export * from './groupByPatientConversation.schema'
|
||||
export * from './findUniqueLabRxTemplate.schema'
|
||||
export * from './findUniqueOrThrowLabRxTemplate.schema'
|
||||
export * from './findFirstLabRxTemplate.schema'
|
||||
export * from './findFirstOrThrowLabRxTemplate.schema'
|
||||
export * from './findManyLabRxTemplate.schema'
|
||||
export * from './countLabRxTemplate.schema'
|
||||
export * from './createOneLabRxTemplate.schema'
|
||||
export * from './createManyLabRxTemplate.schema'
|
||||
export * from './createManyAndReturnLabRxTemplate.schema'
|
||||
export * from './deleteOneLabRxTemplate.schema'
|
||||
export * from './deleteManyLabRxTemplate.schema'
|
||||
export * from './updateOneLabRxTemplate.schema'
|
||||
export * from './updateManyLabRxTemplate.schema'
|
||||
export * from './updateManyAndReturnLabRxTemplate.schema'
|
||||
export * from './upsertOneLabRxTemplate.schema'
|
||||
export * from './aggregateLabRxTemplate.schema'
|
||||
export * from './groupByLabRxTemplate.schema'
|
||||
export * from './findUniqueCommissionBatch.schema'
|
||||
export * from './findUniqueOrThrowCommissionBatch.schema'
|
||||
export * from './findFirstCommissionBatch.schema'
|
||||
@@ -629,6 +648,23 @@ export * from './updateManyAndReturnCommissionBatchItem.schema'
|
||||
export * from './upsertOneCommissionBatchItem.schema'
|
||||
export * from './aggregateCommissionBatchItem.schema'
|
||||
export * from './groupByCommissionBatchItem.schema'
|
||||
export * from './findUniqueSeleniumSettings.schema'
|
||||
export * from './findUniqueOrThrowSeleniumSettings.schema'
|
||||
export * from './findFirstSeleniumSettings.schema'
|
||||
export * from './findFirstOrThrowSeleniumSettings.schema'
|
||||
export * from './findManySeleniumSettings.schema'
|
||||
export * from './countSeleniumSettings.schema'
|
||||
export * from './createOneSeleniumSettings.schema'
|
||||
export * from './createManySeleniumSettings.schema'
|
||||
export * from './createManyAndReturnSeleniumSettings.schema'
|
||||
export * from './deleteOneSeleniumSettings.schema'
|
||||
export * from './deleteManySeleniumSettings.schema'
|
||||
export * from './updateOneSeleniumSettings.schema'
|
||||
export * from './updateManySeleniumSettings.schema'
|
||||
export * from './updateManyAndReturnSeleniumSettings.schema'
|
||||
export * from './upsertOneSeleniumSettings.schema'
|
||||
export * from './aggregateSeleniumSettings.schema'
|
||||
export * from './groupBySeleniumSettings.schema'
|
||||
export * from './results/UserFindUniqueResult.schema'
|
||||
export * from './results/UserFindFirstResult.schema'
|
||||
export * from './results/UserFindManyResult.schema'
|
||||
@@ -1045,6 +1081,19 @@ export * from './results/PatientConversationDeleteManyResult.schema'
|
||||
export * from './results/PatientConversationAggregateResult.schema'
|
||||
export * from './results/PatientConversationGroupByResult.schema'
|
||||
export * from './results/PatientConversationCountResult.schema'
|
||||
export * from './results/LabRxTemplateFindUniqueResult.schema'
|
||||
export * from './results/LabRxTemplateFindFirstResult.schema'
|
||||
export * from './results/LabRxTemplateFindManyResult.schema'
|
||||
export * from './results/LabRxTemplateCreateResult.schema'
|
||||
export * from './results/LabRxTemplateCreateManyResult.schema'
|
||||
export * from './results/LabRxTemplateUpdateResult.schema'
|
||||
export * from './results/LabRxTemplateUpdateManyResult.schema'
|
||||
export * from './results/LabRxTemplateUpsertResult.schema'
|
||||
export * from './results/LabRxTemplateDeleteResult.schema'
|
||||
export * from './results/LabRxTemplateDeleteManyResult.schema'
|
||||
export * from './results/LabRxTemplateAggregateResult.schema'
|
||||
export * from './results/LabRxTemplateGroupByResult.schema'
|
||||
export * from './results/LabRxTemplateCountResult.schema'
|
||||
export * from './results/CommissionBatchFindUniqueResult.schema'
|
||||
export * from './results/CommissionBatchFindFirstResult.schema'
|
||||
export * from './results/CommissionBatchFindManyResult.schema'
|
||||
@@ -1071,6 +1120,19 @@ export * from './results/CommissionBatchItemDeleteManyResult.schema'
|
||||
export * from './results/CommissionBatchItemAggregateResult.schema'
|
||||
export * from './results/CommissionBatchItemGroupByResult.schema'
|
||||
export * from './results/CommissionBatchItemCountResult.schema'
|
||||
export * from './results/SeleniumSettingsFindUniqueResult.schema'
|
||||
export * from './results/SeleniumSettingsFindFirstResult.schema'
|
||||
export * from './results/SeleniumSettingsFindManyResult.schema'
|
||||
export * from './results/SeleniumSettingsCreateResult.schema'
|
||||
export * from './results/SeleniumSettingsCreateManyResult.schema'
|
||||
export * from './results/SeleniumSettingsUpdateResult.schema'
|
||||
export * from './results/SeleniumSettingsUpdateManyResult.schema'
|
||||
export * from './results/SeleniumSettingsUpsertResult.schema'
|
||||
export * from './results/SeleniumSettingsDeleteResult.schema'
|
||||
export * from './results/SeleniumSettingsDeleteManyResult.schema'
|
||||
export * from './results/SeleniumSettingsAggregateResult.schema'
|
||||
export * from './results/SeleniumSettingsGroupByResult.schema'
|
||||
export * from './results/SeleniumSettingsCountResult.schema'
|
||||
export * from './results/index'
|
||||
export * from './objects/index'
|
||||
export * from './variants/pure/User.pure'
|
||||
@@ -1105,8 +1167,10 @@ export * from './variants/pure/OfficeContact.pure'
|
||||
export * from './variants/pure/InsuranceContact.pure'
|
||||
export * from './variants/pure/ProcedureTimeslot.pure'
|
||||
export * from './variants/pure/PatientConversation.pure'
|
||||
export * from './variants/pure/LabRxTemplate.pure'
|
||||
export * from './variants/pure/CommissionBatch.pure'
|
||||
export * from './variants/pure/CommissionBatchItem.pure'
|
||||
export * from './variants/pure/SeleniumSettings.pure'
|
||||
export * from './variants/pure/index'
|
||||
export * from './variants/input/User.input'
|
||||
export * from './variants/input/Patient.input'
|
||||
@@ -1140,8 +1204,10 @@ export * from './variants/input/OfficeContact.input'
|
||||
export * from './variants/input/InsuranceContact.input'
|
||||
export * from './variants/input/ProcedureTimeslot.input'
|
||||
export * from './variants/input/PatientConversation.input'
|
||||
export * from './variants/input/LabRxTemplate.input'
|
||||
export * from './variants/input/CommissionBatch.input'
|
||||
export * from './variants/input/CommissionBatchItem.input'
|
||||
export * from './variants/input/SeleniumSettings.input'
|
||||
export * from './variants/input/index'
|
||||
export * from './variants/result/User.result'
|
||||
export * from './variants/result/Patient.result'
|
||||
@@ -1175,7 +1241,9 @@ export * from './variants/result/OfficeContact.result'
|
||||
export * from './variants/result/InsuranceContact.result'
|
||||
export * from './variants/result/ProcedureTimeslot.result'
|
||||
export * from './variants/result/PatientConversation.result'
|
||||
export * from './variants/result/LabRxTemplate.result'
|
||||
export * from './variants/result/CommissionBatch.result'
|
||||
export * from './variants/result/CommissionBatchItem.result'
|
||||
export * from './variants/result/SeleniumSettings.result'
|
||||
export * from './variants/result/index'
|
||||
export * from './variants/index'
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as z from 'zod';
|
||||
import { Prisma } from '../../../generated/prisma';
|
||||
import Decimal from 'decimal.js';
|
||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
||||
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
||||
import type { Prisma } from '../../../generated/prisma';
|
||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
||||
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
||||
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
||||
|
||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||
|
||||
import Decimal from "decimal.js";
|
||||
const makeSchema = () => z.object({
|
||||
procedureCode: z.string(),
|
||||
procedureLabel: z.string().optional().nullable(),
|
||||
@@ -14,7 +15,7 @@ const makeSchema = () => z.object({
|
||||
z.number(),
|
||||
z.string(),
|
||||
z.instanceof(Decimal),
|
||||
z.instanceof(Prisma.Decimal),
|
||||
z.instanceof(Decimal),
|
||||
DecimalJSLikeSchema,
|
||||
]).refine((v) => isValidDecimalInput(v), {
|
||||
message: "Field 'fee' must be a Decimal",
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
import * as z from 'zod';
|
||||
import { Prisma } from '../../../generated/prisma';
|
||||
import Decimal from 'decimal.js';
|
||||
import type { Prisma } from '../../../generated/prisma';
|
||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||
|
||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||
|
||||
import Decimal from "decimal.js";
|
||||
const makeSchema = () => z.object({
|
||||
id: z.number().int().optional(),
|
||||
patientId: z.number().int(),
|
||||
@@ -14,7 +15,7 @@ const makeSchema = () => z.object({
|
||||
z.number(),
|
||||
z.string(),
|
||||
z.instanceof(Decimal),
|
||||
z.instanceof(Prisma.Decimal),
|
||||
z.instanceof(Decimal),
|
||||
DecimalJSLikeSchema,
|
||||
]).refine((v) => isValidDecimalInput(v), {
|
||||
message: "Field 'fee' must be a Decimal",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user