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,
|
"is_task_list_visible": true,
|
||||||
"active_task": "backend#dev"
|
"active_task": "frontend#dev"
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,8 @@ const ALIAS_MAP: Record<string, string> = {
|
|||||||
// Exams
|
// Exams
|
||||||
"perio exam": "periodic oral evaluation",
|
"perio exam": "periodic oral evaluation",
|
||||||
"periodic exam": "periodic oral evaluation",
|
"periodic exam": "periodic oral evaluation",
|
||||||
|
"period exam": "periodic oral evaluation",
|
||||||
|
"poe": "periodic oral evaluation",
|
||||||
"recall exam": "periodic oral evaluation",
|
"recall exam": "periodic oral evaluation",
|
||||||
"comp exam": "comprehensive oral evaluation",
|
"comp exam": "comprehensive oral evaluation",
|
||||||
"comprehensive 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)
|
// or short abbreviations that need precise mapping (checked before keyword search)
|
||||||
const DIRECT_CODE_MAP: Record<string, string> = {
|
const DIRECT_CODE_MAP: Record<string, string> = {
|
||||||
// Exams
|
// Exams
|
||||||
|
"poe": "D0120",
|
||||||
|
"period exam": "D0120",
|
||||||
"limited exam": "D0140",
|
"limited exam": "D0140",
|
||||||
"emergency exam": "D0140",
|
"emergency exam": "D0140",
|
||||||
// X-rays
|
// X-rays
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
import { InProcessQueue } from "./inProcessQueue";
|
import { InProcessQueue } from "./inProcessQueue";
|
||||||
import { io } from "../socket";
|
import { io } from "../socket";
|
||||||
import { runEligibilityProcessor } from "./processors/eligibilityProcessor";
|
|
||||||
import { runClaimStatusProcessor } from "./processors/claimStatusProcessor";
|
import { runClaimStatusProcessor } from "./processors/claimStatusProcessor";
|
||||||
import { runClaimSubmitProcessor } from "./processors/claimSubmitProcessor";
|
import { runClaimSubmitProcessor } from "./processors/claimSubmitProcessor";
|
||||||
import { runOcrProcessor } from "./processors/ocrProcessor";
|
import { runOcrProcessor } from "./processors/ocrProcessor";
|
||||||
@@ -23,8 +22,6 @@ import { runUnitedDHClaimProcessor } from "./processors/unitedDHClaimProcessor";
|
|||||||
import { runUnitedDHPreAuthProcessor } from "./processors/unitedDHPreAuthProcessor";
|
import { runUnitedDHPreAuthProcessor } from "./processors/unitedDHPreAuthProcessor";
|
||||||
import { runTuftsSCOPreAuthProcessor } from "./processors/tuftsSCOPreAuthProcessor";
|
import { runTuftsSCOPreAuthProcessor } from "./processors/tuftsSCOPreAuthProcessor";
|
||||||
import { runTuftsSCOClaimProcessor } from "./processors/tuftsSCOClaimProcessor";
|
import { runTuftsSCOClaimProcessor } from "./processors/tuftsSCOClaimProcessor";
|
||||||
import { runEligibilityHistoryProcessor } from "./processors/eligibilityHistoryProcessor";
|
|
||||||
import { runCmspEligibilityHistoryRemainingProcessor } from "./processors/cmspEligibilityHistoryRemainingProcessor";
|
|
||||||
import { runBcbsMaEligibilityProcessor } from "./processors/bcbsMaEligibilityProcessor";
|
import { runBcbsMaEligibilityProcessor } from "./processors/bcbsMaEligibilityProcessor";
|
||||||
import type { SeleniumJobData, OcrJobData } from "./queues";
|
import type { SeleniumJobData, OcrJobData } from "./queues";
|
||||||
|
|
||||||
@@ -59,16 +56,6 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
|||||||
message: "Selenium browser starting…",
|
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") {
|
if (jobType === "claim-status-check") {
|
||||||
return runClaimStatusProcessor({
|
return runClaimStatusProcessor({
|
||||||
enrichedPayload: data.enrichedPayload,
|
enrichedPayload: data.enrichedPayload,
|
||||||
@@ -231,26 +218,6 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
|||||||
job.id
|
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") {
|
if (jobType === "bcbs-ma-eligibility-check") {
|
||||||
return runBcbsMaEligibilityProcessor(
|
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. */
|
/** Job types dispatched to the selenium Python worker. */
|
||||||
export type SeleniumJobType =
|
export type SeleniumJobType =
|
||||||
| "eligibility-check"
|
|
||||||
| "claim-status-check"
|
| "claim-status-check"
|
||||||
| "claim-submit"
|
| "claim-submit"
|
||||||
| "claim-pre-auth"
|
| "claim-pre-auth"
|
||||||
@@ -19,8 +18,6 @@ export type SeleniumJobType =
|
|||||||
| "uniteddh-preauth-submit"
|
| "uniteddh-preauth-submit"
|
||||||
| "tuftssco-preauth-submit"
|
| "tuftssco-preauth-submit"
|
||||||
| "tuftssco-eligibility-check"
|
| "tuftssco-eligibility-check"
|
||||||
| "mh-eligibility-history-check"
|
|
||||||
| "cmsp-eligibility-history-remaining-check"
|
|
||||||
| "bcbs-ma-eligibility-check";
|
| "bcbs-ma-eligibility-check";
|
||||||
|
|
||||||
export interface SeleniumJobData {
|
export interface SeleniumJobData {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Worker, Job } from "bullmq";
|
|||||||
import { redisConnection } from "../connection";
|
import { redisConnection } from "../connection";
|
||||||
import { SeleniumJobData } from "../queues";
|
import { SeleniumJobData } from "../queues";
|
||||||
import { io } from "../../socket";
|
import { io } from "../../socket";
|
||||||
import { runEligibilityProcessor } from "../processors/eligibilityProcessor";
|
|
||||||
import { runClaimStatusProcessor } from "../processors/claimStatusProcessor";
|
import { runClaimStatusProcessor } from "../processors/claimStatusProcessor";
|
||||||
import { runClaimSubmitProcessor } from "../processors/claimSubmitProcessor";
|
import { runClaimSubmitProcessor } from "../processors/claimSubmitProcessor";
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ function emitJobUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
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;
|
const jobId = job.id ?? job.name;
|
||||||
|
|
||||||
emitJobUpdate(socketId, jobId, jobType, "active", {
|
emitJobUpdate(socketId, jobId, jobType, "active", {
|
||||||
@@ -37,16 +36,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
|||||||
try {
|
try {
|
||||||
let result: any;
|
let result: any;
|
||||||
|
|
||||||
if (jobType === "eligibility-check") {
|
if (jobType === "claim-status-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") {
|
|
||||||
result = await runClaimStatusProcessor({
|
result = await runClaimStatusProcessor({
|
||||||
enrichedPayload,
|
enrichedPayload,
|
||||||
insuranceId: job.data.insuranceId!,
|
insuranceId: job.data.insuranceId!,
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import shoppingVendorsRoutes from "./shopping-vendors";
|
|||||||
import feeScheduleRoutes from "./feeSchedule";
|
import feeScheduleRoutes from "./feeSchedule";
|
||||||
import licenseRoutes from "./license";
|
import licenseRoutes from "./license";
|
||||||
import insuranceStatusBcbsMaRoutes from "./insuranceStatusBcbsMa";
|
import insuranceStatusBcbsMaRoutes from "./insuranceStatusBcbsMa";
|
||||||
|
import labRxRoutes from "./lab-rx";
|
||||||
|
import seleniumSettingsRoutes from "./selenium-settings";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -90,5 +92,7 @@ router.use("/commissions", commissionsRoutes);
|
|||||||
router.use("/shopping-vendors", shoppingVendorsRoutes);
|
router.use("/shopping-vendors", shoppingVendorsRoutes);
|
||||||
router.use("/fee-schedule", feeScheduleRoutes);
|
router.use("/fee-schedule", feeScheduleRoutes);
|
||||||
router.use("/license", licenseRoutes);
|
router.use("/license", licenseRoutes);
|
||||||
|
router.use("/lab-rx", labRxRoutes);
|
||||||
|
router.use("/selenium-settings", seleniumSettingsRoutes);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "../../../../packages/db/types/patient-types";
|
} from "../../../../packages/db/types/patient-types";
|
||||||
import { formatDobForAgent } from "../utils/dateUtils";
|
import { formatDobForAgent } from "../utils/dateUtils";
|
||||||
import { seleniumQueue } from "../queue/queues";
|
import { seleniumQueue } from "../queue/queues";
|
||||||
import { enqueueSeleniumJob, getSeleniumJobStatus } from "../queue/jobRunner";
|
import { getSeleniumJobStatus } from "../queue/jobRunner";
|
||||||
|
|
||||||
const router = Router();
|
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(
|
router.post(
|
||||||
"/claim-status-check",
|
"/claim-status-check",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { storage } from "../storage";
|
|||||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||||
import { forwardOtpToSeleniumUnitedDHClaimAgent } from "../services/seleniumUnitedDHClaimClient";
|
import { forwardOtpToSeleniumUnitedDHClaimAgent } from "../services/seleniumUnitedDHClaimClient";
|
||||||
import { io } from "../socket";
|
import { io } from "../socket";
|
||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
|
||||||
const router = Router();
|
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 = {
|
const enrichedPayload = {
|
||||||
claim: {
|
claim: {
|
||||||
...claimData,
|
...claimData,
|
||||||
uniteddhUsername: credentials.username,
|
uniteddhUsername: credentials.username,
|
||||||
uniteddhPassword: credentials.password,
|
uniteddhPassword: credentials.password,
|
||||||
|
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { storage } from "../storage";
|
|||||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||||
import { forwardOtpToSeleniumUnitedDHPreAuthAgent } from "../services/seleniumUnitedDHPreAuthClient";
|
import { forwardOtpToSeleniumUnitedDHPreAuthAgent } from "../services/seleniumUnitedDHPreAuthClient";
|
||||||
import { io } from "../socket";
|
import { io } from "../socket";
|
||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
|
||||||
const router = Router();
|
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 = {
|
const enrichedPayload = {
|
||||||
claim: {
|
claim: {
|
||||||
...claimData,
|
...claimData,
|
||||||
uniteddhUsername: credentials.username,
|
uniteddhUsername: credentials.username,
|
||||||
uniteddhPassword: credentials.password,
|
uniteddhPassword: credentials.password,
|
||||||
|
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { storage } from "../storage";
|
|||||||
import { forwardOtpToSeleniumUnitedSCOAgent } from "../services/seleniumUnitedSCOEligibilityClient";
|
import { forwardOtpToSeleniumUnitedSCOAgent } from "../services/seleniumUnitedSCOEligibilityClient";
|
||||||
import { io } from "../socket";
|
import { io } from "../socket";
|
||||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -62,10 +63,13 @@ router.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seleniumSettings = await db.seleniumSettings.findUnique({ where: { userId: req.user.id } });
|
||||||
|
|
||||||
const enrichedData = {
|
const enrichedData = {
|
||||||
...rawData,
|
...rawData,
|
||||||
unitedscoUsername: credentials.username,
|
unitedscoUsername: credentials.username,
|
||||||
unitedscoPassword: credentials.password,
|
unitedscoPassword: credentials.password,
|
||||||
|
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const socketId: string | undefined = req.body.socketId;
|
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 { insuranceContactStorage } from "./insurance-contact-storage";
|
||||||
import { commissionsStorage } from "./commissions-storage";
|
import { commissionsStorage } from "./commissions-storage";
|
||||||
import { shoppingVendorStorage } from "./shopping-vendor-storage";
|
import { shoppingVendorStorage } from "./shopping-vendor-storage";
|
||||||
|
import { labRxStorage } from "./lab-rx-storage";
|
||||||
|
|
||||||
|
|
||||||
export const storage = {
|
export const storage = {
|
||||||
@@ -53,6 +54,7 @@ export const storage = {
|
|||||||
...insuranceContactStorage,
|
...insuranceContactStorage,
|
||||||
...commissionsStorage,
|
...commissionsStorage,
|
||||||
...shoppingVendorStorage,
|
...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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-barcode": "^1.6.1",
|
||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dnd": "^16.0.1",
|
"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 CloudStoragePage = lazy(() => import("./pages/cloud-storage-page"));
|
||||||
const JobMonitorPage = lazy(() => import("./pages/job-monitor-page"));
|
const JobMonitorPage = lazy(() => import("./pages/job-monitor-page"));
|
||||||
const ChartPage = lazy(() => import("./pages/chart-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 AiInputAgentPage = lazy(() => import("./pages/ai-input-agent-page"));
|
||||||
const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page"));
|
const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page"));
|
||||||
const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page"));
|
const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page"));
|
||||||
@@ -65,6 +66,7 @@ function Router() {
|
|||||||
/>
|
/>
|
||||||
<ProtectedRoute path="/reports" component={() => <ReportsPage />} />
|
<ProtectedRoute path="/reports" component={() => <ReportsPage />} />
|
||||||
<ProtectedRoute path="/cloud-storage" component={() => <CloudStoragePage />} />
|
<ProtectedRoute path="/cloud-storage" component={() => <CloudStoragePage />} />
|
||||||
|
<ProtectedRoute path="/dental-education" component={() => <DentalEducationPage />} />
|
||||||
<ProtectedRoute path="/ai-input-agent" component={() => <AiInputAgentPage />} />
|
<ProtectedRoute path="/ai-input-agent" component={() => <AiInputAgentPage />} />
|
||||||
<ProtectedRoute path="/dental-shopping/search-tag" component={() => <DentalShoppingSearchTagPage />} />
|
<ProtectedRoute path="/dental-shopping/search-tag" component={() => <DentalShoppingSearchTagPage />} />
|
||||||
<ProtectedRoute path="/dental-shopping/login-info" component={() => <DentalShoppingLoginInfoPage />} />
|
<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 { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { BarcodeModal } from "@/components/chart/barcode-modal";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { LabRxModal } from "@/components/chart/lab-rx-modal";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
|
import { Barcode, FileText } from "lucide-react";
|
||||||
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: "",
|
|
||||||
});
|
|
||||||
export function LabManagementTab() {
|
export function LabManagementTab() {
|
||||||
const [orders, setOrders] = useState([]);
|
const [selectedPatient, setSelectedPatient] = useState(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [barcodeOpen, setBarcodeOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState(newOrder());
|
const [labRxOpen, setLabRxOpen] = useState(false);
|
||||||
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>
|
|
||||||
|
|
||||||
<div className="border rounded-lg overflow-hidden">
|
return (
|
||||||
<Table>
|
<div className="space-y-6">
|
||||||
<TableHeader>
|
<Card>
|
||||||
<TableRow className="bg-gray-50">
|
<CardHeader>
|
||||||
<TableHead className="w-24">Order Date</TableHead>
|
<CardTitle>Lab Actions</CardTitle>
|
||||||
<TableHead className="w-16">Tooth</TableHead>
|
<CardDescription>
|
||||||
<TableHead>Case Type</TableHead>
|
{selectedPatient
|
||||||
<TableHead>Lab</TableHead>
|
? `Selected patient: ${selectedPatient.firstName} ${selectedPatient.lastName}`
|
||||||
<TableHead className="w-16 text-center">Shade</TableHead>
|
: "Select a patient below to get started."}
|
||||||
<TableHead className="w-24">Due</TableHead>
|
</CardDescription>
|
||||||
<TableHead className="w-28">Status</TableHead>
|
</CardHeader>
|
||||||
<TableHead className="w-20 text-right">Actions</TableHead>
|
<CardContent>
|
||||||
</TableRow>
|
<div className="flex gap-4">
|
||||||
</TableHeader>
|
<Button
|
||||||
<TableBody>
|
variant="outline"
|
||||||
{orders.length === 0 ? (<TableRow>
|
className="gap-2"
|
||||||
<TableCell colSpan={8} className="text-center text-gray-400 py-10">
|
disabled={!selectedPatient}
|
||||||
<div className="flex flex-col items-center gap-2">
|
onClick={() => setBarcodeOpen(true)}
|
||||||
<Package className="h-8 w-8 text-gray-300"/>
|
>
|
||||||
<span>No lab orders yet. Click "New Lab Order" to add one.</span>
|
<Barcode className="h-4 w-4" />
|
||||||
</div>
|
Create a Barcode
|
||||||
</TableCell>
|
</Button>
|
||||||
</TableRow>) : (orders.map((order) => (<TableRow key={order.id} className={order.rush ? "bg-red-50/40" : ""}>
|
<Button
|
||||||
<TableCell className="text-sm">{order.orderDate}</TableCell>
|
variant="outline"
|
||||||
<TableCell className="font-mono text-sm">{order.tooth || "—"}</TableCell>
|
className="gap-2"
|
||||||
<TableCell>
|
disabled={!selectedPatient}
|
||||||
<div className="flex items-center gap-1.5">
|
onClick={() => setLabRxOpen(true)}
|
||||||
<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">
|
<FileText className="h-4 w-4" />
|
||||||
RUSH
|
Lab RX
|
||||||
</Badge>)}
|
</Button>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
</CardContent>
|
||||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
</Card>
|
||||||
<Button onClick={handleSave} disabled={!editing.caseType}>Save</Button>
|
|
||||||
</DialogFooter>
|
<Card>
|
||||||
</DialogContent>
|
<CardHeader>
|
||||||
</Dialog>
|
<CardTitle>Patient Records</CardTitle>
|
||||||
</div>);
|
<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 { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { BarcodeModal } from "@/components/chart/barcode-modal";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { LabRxModal } from "@/components/chart/lab-rx-modal";
|
||||||
import {
|
import { Barcode, FileText } from "lucide-react";
|
||||||
Select,
|
import { Patient } from "@repo/db/types";
|
||||||
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: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
export function LabManagementTab() {
|
export function LabManagementTab() {
|
||||||
const [orders, setOrders] = useState<LabOrder[]>([]);
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [barcodeOpen, setBarcodeOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<LabOrder>(newOrder());
|
const [labRxOpen, setLabRxOpen] = useState(false);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
{/* Action buttons */}
|
||||||
<div className="flex gap-4 text-sm text-gray-600">
|
<Card>
|
||||||
{pending > 0 && <span>Open orders: <strong>{pending}</strong></span>}
|
<CardHeader>
|
||||||
{rush > 0 && <span className="text-red-600">Rush: <strong>{rush}</strong></span>}
|
<CardTitle>Lab Actions</CardTitle>
|
||||||
{orders.length === 0 && <span>No lab orders yet</span>}
|
<CardDescription>
|
||||||
</div>
|
{selectedPatient
|
||||||
<Button size="sm" onClick={openAdd} className="gap-1.5">
|
? `Selected patient: ${selectedPatient.firstName} ${selectedPatient.lastName}`
|
||||||
<Plus className="h-4 w-4" />
|
: "Select a patient below to get started."}
|
||||||
New Lab Order
|
</CardDescription>
|
||||||
</Button>
|
</CardHeader>
|
||||||
</div>
|
<CardContent>
|
||||||
|
<div className="flex gap-4">
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<Button
|
||||||
<Table>
|
variant="outline"
|
||||||
<TableHeader>
|
className="gap-2"
|
||||||
<TableRow className="bg-gray-50">
|
disabled={!selectedPatient}
|
||||||
<TableHead className="w-24">Order Date</TableHead>
|
onClick={() => setBarcodeOpen(true)}
|
||||||
<TableHead className="w-16">Tooth</TableHead>
|
>
|
||||||
<TableHead>Case Type</TableHead>
|
<Barcode className="h-4 w-4" />
|
||||||
<TableHead>Lab</TableHead>
|
Create a Barcode
|
||||||
<TableHead className="w-16 text-center">Shade</TableHead>
|
</Button>
|
||||||
<TableHead className="w-24">Due</TableHead>
|
<Button
|
||||||
<TableHead className="w-28">Status</TableHead>
|
variant="outline"
|
||||||
<TableHead className="w-20 text-right">Actions</TableHead>
|
className="gap-2"
|
||||||
</TableRow>
|
disabled={!selectedPatient}
|
||||||
</TableHeader>
|
onClick={() => setLabRxOpen(true)}
|
||||||
<TableBody>
|
>
|
||||||
{orders.length === 0 ? (
|
<FileText className="h-4 w-4" />
|
||||||
<TableRow>
|
Lab RX
|
||||||
<TableCell colSpan={8} className="text-center text-gray-400 py-10">
|
</Button>
|
||||||
<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>
|
</div>
|
||||||
<DialogFooter>
|
</CardContent>
|
||||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
</Card>
|
||||||
<Button onClick={handleSave} disabled={!editing.caseType}>Save</Button>
|
|
||||||
</DialogFooter>
|
{/* Patient search / selection */}
|
||||||
</DialogContent>
|
<Card>
|
||||||
</Dialog>
|
<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>
|
</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" />
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
Delta MA
|
Claim MD
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
X,
|
X,
|
||||||
ChevronRight,
|
|
||||||
Stethoscope,
|
Stethoscope,
|
||||||
Calendar,
|
Calendar,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -13,7 +12,6 @@ import {
|
|||||||
Paperclip,
|
Paperclip,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
@@ -21,8 +19,6 @@ import { setChatbotPendingFiles } from "@/lib/chatbotFileStore";
|
|||||||
|
|
||||||
type Step =
|
type Step =
|
||||||
| "menu"
|
| "menu"
|
||||||
| "eligibility-input"
|
|
||||||
| "eligibility-confirm"
|
|
||||||
| "ai-loading"
|
| "ai-loading"
|
||||||
| "patient-found"
|
| "patient-found"
|
||||||
| "eligibility-id-ready"
|
| "eligibility-id-ready"
|
||||||
@@ -53,12 +49,6 @@ interface PatientResult {
|
|||||||
dateOfBirth: string | null;
|
dateOfBirth: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EligibilityData {
|
|
||||||
memberId: string;
|
|
||||||
dob: string;
|
|
||||||
dobISO: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CheckAndClaimData {
|
interface CheckAndClaimData {
|
||||||
patient: PatientResult | null;
|
patient: PatientResult | null;
|
||||||
memberId: string;
|
memberId: string;
|
||||||
@@ -75,40 +65,6 @@ function makeMsg(role: "bot" | "user", text: string, isLoading = false): Message
|
|||||||
return { id: ++msgCounter, role, text, isLoading };
|
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 CHAT_STORAGE_KEY = "chatbot_messages";
|
||||||
const CHATBOT_JOB_TS_KEY = "chatbot_job_started_at";
|
const CHATBOT_JOB_TS_KEY = "chatbot_job_started_at";
|
||||||
|
|
||||||
@@ -158,9 +114,6 @@ export function ChatbotButton() {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [step, setStep] = useState<Step>("menu");
|
const [step, setStep] = useState<Step>("menu");
|
||||||
const [messages, setMessages] = useState<Message[]>(loadSavedMessages);
|
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 [freeTextInput, setFreeTextInput] = useState("");
|
||||||
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
|
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);
|
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 [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const pasteRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const freeTextRef = useRef<HTMLTextAreaElement>(null);
|
const freeTextRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -244,9 +196,6 @@ export function ChatbotButton() {
|
|||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step === "eligibility-input") {
|
|
||||||
setTimeout(() => pasteRef.current?.focus(), 50);
|
|
||||||
}
|
|
||||||
if (step === "menu") {
|
if (step === "menu") {
|
||||||
setTimeout(() => freeTextRef.current?.focus(), 50);
|
setTimeout(() => freeTextRef.current?.focus(), 50);
|
||||||
}
|
}
|
||||||
@@ -266,9 +215,6 @@ export function ChatbotButton() {
|
|||||||
// Resets step/data only — keeps message history
|
// Resets step/data only — keeps message history
|
||||||
const resetStep = () => {
|
const resetStep = () => {
|
||||||
setStep("menu");
|
setStep("menu");
|
||||||
setPasteInput("");
|
|
||||||
setParseError("");
|
|
||||||
setEligibilityData(null);
|
|
||||||
setFreeTextInput("");
|
setFreeTextInput("");
|
||||||
setPatientResult(null);
|
setPatientResult(null);
|
||||||
setEligibilityIdData(null);
|
setEligibilityIdData(null);
|
||||||
@@ -300,7 +246,7 @@ export function ChatbotButton() {
|
|||||||
resetStep();
|
resetStep();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOptionSelect = (option: "eligibility" | "schedule" | "claims") => {
|
const handleOptionSelect = (option: "schedule" | "claims") => {
|
||||||
if (option === "schedule") {
|
if (option === "schedule") {
|
||||||
addMsg("user", "Schedule an appointment");
|
addMsg("user", "Schedule an appointment");
|
||||||
addMsg("bot", "Opening the appointments page...");
|
addMsg("bot", "Opening the appointments page...");
|
||||||
@@ -309,67 +255,9 @@ export function ChatbotButton() {
|
|||||||
addMsg("user", "View claims");
|
addMsg("user", "View claims");
|
||||||
addMsg("bot", "Opening the claims page...");
|
addMsg("bot", "Opening the claims page...");
|
||||||
setTimeout(() => { setLocation("/claims"); setOpen(false); resetStep(); }, 600);
|
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) => {
|
const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => {
|
||||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck }));
|
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck }));
|
||||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||||
@@ -728,14 +616,7 @@ export function ChatbotButton() {
|
|||||||
|
|
||||||
{/* Quick-action buttons (menu step) */}
|
{/* Quick-action buttons (menu step) */}
|
||||||
{step === "menu" && (
|
{step === "menu" && (
|
||||||
<div className="grid grid-cols-2 gap-2 pt-1">
|
<div className="grid grid-cols-1 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>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOptionSelect("schedule")}
|
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"
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOptionSelect("claims")}
|
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" />
|
<FileText className="h-4 w-4 text-orange-500 shrink-0" />
|
||||||
<span>Claims</span>
|
<span>Claims</span>
|
||||||
@@ -753,58 +634,6 @@ export function ChatbotButton() {
|
|||||||
</div>
|
</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 */}
|
{/* AI-found patient card */}
|
||||||
{step === "patient-found" && patientResult && (
|
{step === "patient-found" && patientResult && (
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 space-y-2">
|
<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>
|
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 pt-1">
|
<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}>
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import {
|
|||||||
Building2,
|
Building2,
|
||||||
Timer,
|
Timer,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
MonitorCog,
|
||||||
|
GraduationCap,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Search,
|
Search,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
@@ -169,6 +171,11 @@ export function Sidebar() {
|
|||||||
path: "/cloud-storage",
|
path: "/cloud-storage",
|
||||||
icon: <Cloud className="h-5 w-5 text-sky-500" />,
|
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",
|
name: "AI Input Agent",
|
||||||
path: "/ai-input-agent",
|
path: "/ai-input-agent",
|
||||||
@@ -285,6 +292,11 @@ export function Sidebar() {
|
|||||||
path: "/settings/aichat",
|
path: "/settings/aichat",
|
||||||
icon: <Bot className="h-4 w-4 text-gray-400" />,
|
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>
|
</svg>
|
||||||
|
|
||||||
<h1 className="text-lg font-medium text-primary">
|
<h1 className="text-lg font-medium text-primary">
|
||||||
My Dental Office Management
|
Wellspring Acupuncture Office Management
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</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]);
|
}, [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 handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const raw = e.target.value;
|
const raw = e.target.value;
|
||||||
|
|
||||||
@@ -148,6 +169,7 @@ export function DateInput({
|
|||||||
placeholder="MM/DD/YYYY"
|
placeholder="MM/DD/YYYY"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
/>
|
/>
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<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 patient = apt ? patientsFromDay.find((p) => p.id === apt.patientId) : null;
|
||||||
const insuranceProvider = (patient as any)?.insuranceProvider as string | null ?? null;
|
const insuranceProvider = (patient as any)?.insuranceProvider as string | null ?? null;
|
||||||
const p = insuranceProvider?.toLowerCase() ?? "";
|
const p = insuranceProvider?.toLowerCase() ?? "";
|
||||||
const isMassHealth = p.includes("masshealth");
|
|
||||||
|
|
||||||
let autoCheck = "other-providers";
|
let autoCheck = "other-providers";
|
||||||
|
|
||||||
if (isMassHealth) {
|
if (p.includes("delta dental ma")) {
|
||||||
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")) {
|
|
||||||
autoCheck = "ddma";
|
autoCheck = "ddma";
|
||||||
} else if (p.includes("delta dental ins")) {
|
} else if (p.includes("delta dental ins")) {
|
||||||
autoCheck = "delta-ins";
|
autoCheck = "delta-ins";
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function AuthPage() {
|
|||||||
<Card className="p-8 bg-white">
|
<Card className="p-8 bg-white">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
|
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
|
||||||
My Dental Office Management
|
Wellspring Acupuncture Office Management
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm font-semibold tracking-[0.15em] bg-clip-text text-transparent animate-gradient-flash"
|
<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%" }}>
|
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
|
// CCA result: pdfFileId is already saved by the processor — open preview directly
|
||||||
if (jobResult.pdfFileId) {
|
if (jobResult.pdfFileId) {
|
||||||
advanceAiClaimQueue();
|
advanceAiClaimQueue();
|
||||||
advanceChatbotClaimQueue();
|
const advancedToNext = advanceChatbotClaimQueue();
|
||||||
setPreviewPdfId(jobResult.pdfFileId);
|
if (!advancedToNext) {
|
||||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
setPreviewPdfId(jobResult.pdfFileId);
|
||||||
setPreviewOpen(true);
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
}
|
||||||
dispatch(setTaskStatus({
|
dispatch(setTaskStatus({
|
||||||
key: "claimSubmit",
|
key: "claimSubmit",
|
||||||
status: "success",
|
status: "success",
|
||||||
@@ -336,7 +338,9 @@ export default function ClaimsPage() {
|
|||||||
|
|
||||||
// Advance the chatbot_claim_queue / chatbot_check_claim_queue after selenium PDF is saved.
|
// 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.
|
// 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 {
|
try {
|
||||||
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
||||||
if (raw) {
|
if (raw) {
|
||||||
@@ -369,7 +373,7 @@ export default function ClaimsPage() {
|
|||||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||||
setWouterLocation("/insurance-status");
|
setWouterLocation("/insurance-status");
|
||||||
}, 500);
|
}, 500);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||||
}
|
}
|
||||||
@@ -403,11 +407,13 @@ export default function ClaimsPage() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
||||||
}, 500);
|
}, 500);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
sessionStorage.removeItem("chatbot_claim_queue");
|
sessionStorage.removeItem("chatbot_claim_queue");
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Advance the ai_claim_queue by removing the first item (current patient).
|
// 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();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
||||||
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
||||||
if (!isDraft) {
|
// NOTE: advanceChatbotClaimQueue() is NOT called here. This function only creates
|
||||||
advanceAiClaimQueue();
|
// the claim DB record; for selenium-based insurances (MH/CCA/DDMA/Tufts/United) the
|
||||||
advanceChatbotClaimQueue();
|
// 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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -964,13 +973,15 @@ export default function ClaimsPage() {
|
|||||||
duration: isPreAuth ? 10000 : 5000,
|
duration: isPreAuth ? 10000 : 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let advancedToNext = false;
|
||||||
if (!isPreAuth) {
|
if (!isPreAuth) {
|
||||||
advanceAiClaimQueue();
|
advanceAiClaimQueue();
|
||||||
advanceChatbotClaimQueue();
|
advancedToNext = advanceChatbotClaimQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
// 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);
|
setPreviewPdfId(result.pdfFileId);
|
||||||
setPreviewFallbackFilename(result.fileName ?? null);
|
setPreviewFallbackFilename(result.fileName ?? null);
|
||||||
setPreviewOpen(true);
|
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,
|
clearTaskStatus,
|
||||||
} from "@/redux/slices/seleniumTaskSlice";
|
} from "@/redux/slices/seleniumTaskSlice";
|
||||||
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
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 { socket } from "@/lib/socket";
|
||||||
import { InsertPatient, Patient } from "@repo/db/types";
|
import { InsertPatient, Patient } from "@repo/db/types";
|
||||||
import { DateInput } from "@/components/ui/dateInput";
|
import { DateInput } from "@/components/ui/dateInput";
|
||||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||||
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||||
import { DualPdfPreviewModal } from "@/components/insurance-status/dual-pdf-preview-modal";
|
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
||||||
import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltains-button-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 { BcbsMaEligibilityButton } from "@/components/insurance-status/bcbs-ma-button-modal";
|
||||||
import { useLicense } from "@/hooks/use-license";
|
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
|
* 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
|
* 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 [dateOfBirth, setDateOfBirth] = useState<Date | null>(null);
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [insuranceProvider, setInsuranceProvider] = useState("");
|
||||||
const isFormIncomplete = !memberId || !dateOfBirth;
|
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
|
// AI Call Insurance section
|
||||||
const [aiCallOpen, setAiCallOpen] = useState(false);
|
const [aiCallOpen, setAiCallOpen] = useState(false);
|
||||||
@@ -147,27 +158,7 @@ export default function InsuranceStatusPage() {
|
|||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
// Dual PDF modal state (used by MH Eligibility & History)
|
const pendingAutoCheck = useRef<"ddma" | "delta-ins" | "united-sco" | "tufts-sco" | "cca" | null>(null);
|
||||||
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 [triggerTarget, setTriggerTarget] = useState<string | null>(null);
|
const [triggerTarget, setTriggerTarget] = useState<string | null>(null);
|
||||||
const pendingScrollTo = useRef<string | null>(null);
|
const pendingScrollTo = useRef<string | null>(null);
|
||||||
const [prefillTick, setPrefillTick] = useState(0);
|
const [prefillTick, setPrefillTick] = useState(0);
|
||||||
@@ -206,6 +197,11 @@ export default function InsuranceStatusPage() {
|
|||||||
setMemberId(selectedPatient.insuranceId ?? "");
|
setMemberId(selectedPatient.insuranceId ?? "");
|
||||||
setFirstName(selectedPatient.firstName ?? "");
|
setFirstName(selectedPatient.firstName ?? "");
|
||||||
setLastName(selectedPatient.lastName ?? "");
|
setLastName(selectedPatient.lastName ?? "");
|
||||||
|
setInsuranceProvider(
|
||||||
|
INSURANCE_PROVIDERS.includes(selectedPatient.insuranceProvider ?? "")
|
||||||
|
? selectedPatient.insuranceProvider!
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
|
||||||
const dob =
|
const dob =
|
||||||
typeof selectedPatient.dateOfBirth === "string"
|
typeof selectedPatient.dateOfBirth === "string"
|
||||||
@@ -217,9 +213,34 @@ export default function InsuranceStatusPage() {
|
|||||||
setFirstName("");
|
setFirstName("");
|
||||||
setLastName("");
|
setLastName("");
|
||||||
setDateOfBirth(null);
|
setDateOfBirth(null);
|
||||||
|
setInsuranceProvider("");
|
||||||
}
|
}
|
||||||
}, [selectedPatient]);
|
}, [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
|
// Auto-lookup patient by member ID when typed manually
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPatient && memberId === (selectedPatient.insuranceId ?? "")) return;
|
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 handleAddPatient = async () => {
|
||||||
const newPatient: InsertPatient = {
|
const newPatient: InsertPatient = {
|
||||||
firstName,
|
firstName,
|
||||||
@@ -343,347 +314,13 @@ export default function InsuranceStatusPage() {
|
|||||||
await addPatientMutation.mutateAsync(newPatient);
|
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")
|
// Auto-trigger after prefill (from chatbot or schedule page "Check Eligibility")
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return;
|
if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return;
|
||||||
const check = pendingAutoCheck.current;
|
const check = pendingAutoCheck.current;
|
||||||
pendingAutoCheck.current = null;
|
pendingAutoCheck.current = null;
|
||||||
if (check === "mh" || check === "mh-history") {
|
// Button-component-handled providers: pass target to their autoTrigger prop
|
||||||
if (isUnder14(dateOfBirth)) {
|
setTriggerTarget(check);
|
||||||
handleCMSPButton();
|
|
||||||
} else {
|
|
||||||
handleMHEligibilityHistoryButton();
|
|
||||||
}
|
|
||||||
} else if (check === "cmsp") {
|
|
||||||
handleCMSPButton();
|
|
||||||
} else {
|
|
||||||
// Button-component-handled providers: pass target to their autoTrigger prop
|
|
||||||
setTriggerTarget(check);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [memberId, dateOfBirth, prefillTick]);
|
}, [memberId, dateOfBirth, prefillTick]);
|
||||||
|
|
||||||
@@ -820,7 +457,7 @@ export default function InsuranceStatusPage() {
|
|||||||
const autoCheck = params.get("autoCheck") as typeof pendingAutoCheck.current;
|
const autoCheck = params.get("autoCheck") as typeof pendingAutoCheck.current;
|
||||||
const scrollTo = params.get("scrollTo");
|
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)) {
|
if (autoCheck && (knownAutoChecks as readonly string[]).includes(autoCheck)) {
|
||||||
pendingAutoCheck.current = autoCheck;
|
pendingAutoCheck.current = autoCheck;
|
||||||
} else if (scrollTo) {
|
} else if (scrollTo) {
|
||||||
@@ -932,100 +569,25 @@ export default function InsuranceStatusPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col-2 gap-4">
|
<div className="grid grid-cols-4 md:grid-cols-4 gap-4 mb-4">
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
onClick={() => handleMHEligibilityButton()}
|
<Label htmlFor="insuranceProvider">Insurance Provider</Label>
|
||||||
className="w-full"
|
<Select
|
||||||
disabled={isCheckingEligibilityStatus}
|
value={insuranceProvider}
|
||||||
>
|
onValueChange={setInsuranceProvider}
|
||||||
{isCheckingEligibilityStatus ? (
|
>
|
||||||
<>
|
<SelectTrigger id="insuranceProvider">
|
||||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
<SelectValue placeholder="Select insurance provider" />
|
||||||
Processing...
|
</SelectTrigger>
|
||||||
</>
|
<SelectContent>
|
||||||
) : (
|
{INSURANCE_PROVIDERS.map((name) => (
|
||||||
<>
|
<SelectItem key={name} value={name}>
|
||||||
<CheckCircle className="h-4 w-4 mr-2" />
|
{name}
|
||||||
MH Eligibility
|
</SelectItem>
|
||||||
</>
|
))}
|
||||||
)}
|
</SelectContent>
|
||||||
</Button>
|
</Select>
|
||||||
|
</div>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* TEMP PROVIDER BUTTONS */}
|
{/* TEMP PROVIDER BUTTONS */}
|
||||||
@@ -1046,6 +608,7 @@ export default function InsuranceStatusPage() {
|
|||||||
autoTrigger={triggerTarget === "ddma"}
|
autoTrigger={triggerTarget === "ddma"}
|
||||||
onAutoTriggered={() => setTriggerTarget(null)}
|
onAutoTriggered={() => setTriggerTarget(null)}
|
||||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||||
|
void saveInsuranceProvider();
|
||||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
if (!claimed) {
|
if (!claimed) {
|
||||||
@@ -1068,6 +631,7 @@ export default function InsuranceStatusPage() {
|
|||||||
autoTrigger={triggerTarget === "delta-ins"}
|
autoTrigger={triggerTarget === "delta-ins"}
|
||||||
onAutoTriggered={() => setTriggerTarget(null)}
|
onAutoTriggered={() => setTriggerTarget(null)}
|
||||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||||
|
void saveInsuranceProvider();
|
||||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
if (!claimed) {
|
if (!claimed) {
|
||||||
@@ -1088,6 +652,7 @@ export default function InsuranceStatusPage() {
|
|||||||
lastName={lastName}
|
lastName={lastName}
|
||||||
isFormIncomplete={isFormIncomplete}
|
isFormIncomplete={isFormIncomplete}
|
||||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||||
|
void saveInsuranceProvider();
|
||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
setPreviewPdfId(pdfId);
|
setPreviewPdfId(pdfId);
|
||||||
setPreviewFallbackFilename(
|
setPreviewFallbackFilename(
|
||||||
@@ -1112,6 +677,7 @@ export default function InsuranceStatusPage() {
|
|||||||
autoTrigger={triggerTarget === "tufts-sco"}
|
autoTrigger={triggerTarget === "tufts-sco"}
|
||||||
onAutoTriggered={() => setTriggerTarget(null)}
|
onAutoTriggered={() => setTriggerTarget(null)}
|
||||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||||
|
void saveInsuranceProvider();
|
||||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
if (!claimed) {
|
if (!claimed) {
|
||||||
@@ -1134,6 +700,7 @@ export default function InsuranceStatusPage() {
|
|||||||
autoTrigger={triggerTarget === "united-sco"}
|
autoTrigger={triggerTarget === "united-sco"}
|
||||||
onAutoTriggered={() => setTriggerTarget(null)}
|
onAutoTriggered={() => setTriggerTarget(null)}
|
||||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||||
|
void saveInsuranceProvider();
|
||||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
if (!claimed) {
|
if (!claimed) {
|
||||||
@@ -1156,6 +723,7 @@ export default function InsuranceStatusPage() {
|
|||||||
autoTrigger={triggerTarget === "cca"}
|
autoTrigger={triggerTarget === "cca"}
|
||||||
onAutoTriggered={() => setTriggerTarget(null)}
|
onAutoTriggered={() => setTriggerTarget(null)}
|
||||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||||
|
void saveInsuranceProvider();
|
||||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
if (!claimed) {
|
if (!claimed) {
|
||||||
@@ -1169,81 +737,6 @@ export default function InsuranceStatusPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1394,78 +887,6 @@ export default function InsuranceStatusPage() {
|
|||||||
autoDownload
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
|||||||
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
||||||
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
||||||
import { AiChatSettingsCard } from "@/components/settings/ai-chat-settings-card";
|
import { AiChatSettingsCard } from "@/components/settings/ai-chat-settings-card";
|
||||||
|
import { SeleniumSettingsCard } from "@/components/settings/selenium-settings-card";
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser } = useAuth();
|
||||||
@@ -232,6 +233,8 @@ export default function SettingsPage() {
|
|||||||
return <ProcedureTimeslotCard />;
|
return <ProcedureTimeslotCard />;
|
||||||
case "insurancecontact":
|
case "insurancecontact":
|
||||||
return <InsuranceContactCard />;
|
return <InsuranceContactCard />;
|
||||||
|
case "selenium":
|
||||||
|
return <SeleniumSettingsCard />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { OfficeContactCard } from "@/components/settings/office-contact-card";
|
|||||||
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
import { ProcedureTimeslotCard } from "@/components/settings/procedure-timeslot-card";
|
||||||
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
import { InsuranceContactCard } from "@/components/settings/insurance-contact-card";
|
||||||
import { AiChatSettingsCard } from "@/components/settings/ai-chat-settings-card";
|
import { AiChatSettingsCard } from "@/components/settings/ai-chat-settings-card";
|
||||||
|
import { SeleniumSettingsCard } from "@/components/settings/selenium-settings-card";
|
||||||
|
|
||||||
type SectionId =
|
type SectionId =
|
||||||
| "staff"
|
| "staff"
|
||||||
@@ -34,7 +35,8 @@ type SectionId =
|
|||||||
| "officehours"
|
| "officehours"
|
||||||
| "officecontact"
|
| "officecontact"
|
||||||
| "proceduretimeslot"
|
| "proceduretimeslot"
|
||||||
| "insurancecontact";
|
| "insurancecontact"
|
||||||
|
| "selenium";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -276,6 +278,9 @@ export default function SettingsPage() {
|
|||||||
case "insurancecontact":
|
case "insurancecontact":
|
||||||
return <InsuranceContactCard />;
|
return <InsuranceContactCard />;
|
||||||
|
|
||||||
|
case "selenium":
|
||||||
|
return <SeleniumSettingsCard />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ import uvicorn
|
|||||||
import asyncio
|
import asyncio
|
||||||
from selenium_claimSubmitWorker import AutomationMassHealthClaimsLogin
|
from selenium_claimSubmitWorker import AutomationMassHealthClaimsLogin
|
||||||
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
||||||
from selenium_MH_eligibilityHistoryCheckWorker import AutomationMassHealthEligibilityHistoryCheck
|
|
||||||
from selenium_CMSP_eligibilityHistoryRemainingCheckWorker import AutomationCMSPEligibilityHistoryRemainingCheck
|
|
||||||
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
||||||
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
||||||
from selenium_MHSinglePaymentCheckWorker import AutomationMassHealthSinglePaymentCheck
|
from selenium_MHSinglePaymentCheckWorker import AutomationMassHealthSinglePaymentCheck
|
||||||
from selenium_MHBatchPaymentCheckWorker import AutomationMassHealthBatchPaymentCheck
|
from selenium_MHBatchPaymentCheckWorker import AutomationMassHealthBatchPaymentCheck
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import helpers_ddma_eligibility as hddma
|
import helpers_claimmd_eligibility as hddma
|
||||||
import helpers_deltains_eligibility as hdeltains
|
import helpers_deltains_eligibility as hdeltains
|
||||||
import helpers_unitedsco_eligibility as hunitedsco
|
import helpers_unitedsco_eligibility as hunitedsco
|
||||||
import helpers_dentaquest_eligibility as hdentaquest
|
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 helpers_bcbs_ma_eligibility as hbcbs_ma
|
||||||
|
|
||||||
# Import startup session-clear functions
|
# 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 deltains_browser_manager import clear_deltains_session_on_startup
|
||||||
from unitedsco_browser_manager import clear_unitedsco_session_on_startup
|
from unitedsco_browser_manager import clear_unitedsco_session_on_startup
|
||||||
from dentaquest_browser_manager import clear_dentaquest_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:
|
async with lock:
|
||||||
active_jobs -= 1
|
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)
|
# Endpoint: 2.1 — Start the automation for Claims login (open browser and log in)
|
||||||
@app.post("/claims-login")
|
@app.post("/claims-login")
|
||||||
async def start_claims_login(request: Request):
|
async def start_claims_login(request: Request):
|
||||||
@@ -410,7 +331,7 @@ async def ddma_eligibility(request: Request):
|
|||||||
waiting_jobs += 1
|
waiting_jobs += 1
|
||||||
|
|
||||||
# run in background (queued under semaphore)
|
# 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}
|
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.webdriver.support import expected_conditions as EC
|
||||||
from selenium.common.exceptions import WebDriverException, TimeoutException
|
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||||
|
|
||||||
from selenium_DDMA_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
from selenium_ClaimMD_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
||||||
|
|
||||||
# In-memory session store
|
# In-memory session store
|
||||||
sessions: Dict[str, Dict[str, Any]] = {}
|
sessions: Dict[str, Dict[str, Any]] = {}
|
||||||
@@ -146,13 +146,11 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
|||||||
if otp_value:
|
if otp_value:
|
||||||
print(f"[OTP poll {poll+1}] OTP received from app, typing it in...")
|
print(f"[OTP poll {poll+1}] OTP received from app, typing it in...")
|
||||||
try:
|
try:
|
||||||
otp_input = driver.find_element(By.XPATH,
|
otp_input = driver.find_element(By.XPATH, "//input[@data-test-id='MfaCode']")
|
||||||
"//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.clear()
|
otp_input.clear()
|
||||||
otp_input.send_keys(otp_value)
|
otp_input.send_keys(otp_value)
|
||||||
try:
|
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()
|
verify_btn.click()
|
||||||
except Exception:
|
except Exception:
|
||||||
otp_input.send_keys("\n")
|
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:
|
if "member" in current_url or "dashboard" in current_url or "eligibility" in current_url:
|
||||||
try:
|
try:
|
||||||
member_search = WebDriverWait(driver, 5).until(
|
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!")
|
print("[OTP] Member search found — login successful!")
|
||||||
login_success = True
|
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)
|
# Check if OTP input is still visible (user hasn't finished)
|
||||||
try:
|
try:
|
||||||
otp_input_elem = driver.find_element(By.XPATH,
|
otp_input_elem = driver.find_element(By.XPATH, "//input[@data-test-id='MfaCode']")
|
||||||
"//input[contains(@aria-label,'Verification') or contains(@placeholder,'verification') or @type='tel']"
|
|
||||||
)
|
|
||||||
print(f"[OTP poll {poll+1}] OTP input still visible - waiting...")
|
print(f"[OTP poll {poll+1}] OTP input still visible - waiting...")
|
||||||
except Exception:
|
except Exception:
|
||||||
# OTP input gone — may mean login is completing; try members page
|
# 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...")
|
print("[OTP] OTP input gone, trying to navigate to members page...")
|
||||||
try:
|
try:
|
||||||
driver.get("https://providers.deltadentalma.com/members")
|
driver.get("https://www.deltadentalma.com/private/provider/dashboard")
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -200,10 +196,10 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
|||||||
# Final check — navigate directly to members page
|
# Final check — navigate directly to members page
|
||||||
try:
|
try:
|
||||||
print("[OTP] Final attempt - navigating to members page...")
|
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)
|
await asyncio.sleep(3)
|
||||||
member_search = WebDriverWait(driver, 10).until(
|
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!")
|
print("[OTP] Member search found — login successful!")
|
||||||
login_success = True
|
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.common.exceptions import WebDriverException, TimeoutException
|
||||||
|
|
||||||
from selenium_DDMA_claimSubmitWorker import AutomationDDMAClaimSubmit
|
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]] = {}
|
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):
|
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.
|
login/OTP, then continues into claim-specific steps.
|
||||||
|
|
||||||
OTP handling uses two complementary strategies:
|
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)
|
# Close the browser window after claim (session preserved in profile)
|
||||||
try:
|
try:
|
||||||
from ddma_browser_manager import get_browser_manager
|
from claimmd_browser_manager import get_browser_manager
|
||||||
get_browser_manager().quit_driver()
|
get_browser_manager().quit_driver()
|
||||||
print("[DDMA Claim] Browser closed - session preserved in profile")
|
print("[DDMA Claim] Browser closed - session preserved in profile")
|
||||||
except Exception as close_err:
|
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 os
|
||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
|
import calendar
|
||||||
|
|
||||||
from ddma_browser_manager import get_browser_manager
|
from claimmd_browser_manager import get_browser_manager
|
||||||
|
|
||||||
class AutomationDeltaDentalMAEligibilityCheck:
|
class AutomationDeltaDentalMAEligibilityCheck:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
@@ -45,7 +46,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
|
|
||||||
# Try to click logout button if visible
|
# Try to click logout button if visible
|
||||||
try:
|
try:
|
||||||
self.driver.get("https://providers.deltadentalma.com/")
|
self.driver.get("https://www.deltadentalma.com/")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
logout_selectors = [
|
logout_selectors = [
|
||||||
@@ -99,27 +100,27 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
current_url = self.driver.current_url
|
current_url = self.driver.current_url
|
||||||
print(f"[login] Current URL: {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)
|
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")
|
print("[login] Already on logged-in page - skipping login entirely")
|
||||||
if "member" not in current_url.lower():
|
if "member" not in current_url.lower():
|
||||||
try:
|
try:
|
||||||
member_search = WebDriverWait(self.driver, 5).until(
|
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")
|
print("[login] Found member search input - returning ALREADY_LOGGED_IN")
|
||||||
return "ALREADY_LOGGED_IN"
|
return "ALREADY_LOGGED_IN"
|
||||||
except TimeoutException:
|
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}")
|
print(f"[login] Navigating to members page: {members_url}")
|
||||||
self.driver.get(members_url)
|
self.driver.get(members_url)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
member_search = WebDriverWait(self.driver, 5).until(
|
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")
|
print("[login] Member search found - ALREADY_LOGGED_IN")
|
||||||
return "ALREADY_LOGGED_IN"
|
return "ALREADY_LOGGED_IN"
|
||||||
@@ -137,9 +138,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
current_url = self.driver.current_url
|
current_url = self.driver.current_url
|
||||||
print(f"[login] URL after navigation: {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(
|
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:
|
if member_search:
|
||||||
print("[login] Session valid - skipping login")
|
print("[login] Session valid - skipping login")
|
||||||
@@ -172,7 +173,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
try:
|
try:
|
||||||
otp_candidate = WebDriverWait(self.driver, 10).until(
|
otp_candidate = WebDriverWait(self.driver, 10).until(
|
||||||
EC.presence_of_element_located(
|
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:
|
if otp_candidate:
|
||||||
@@ -189,7 +190,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
try:
|
try:
|
||||||
member_search = WebDriverWait(self.driver, 5).until(
|
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:
|
if member_search:
|
||||||
print("[login] Already authenticated after modal dismiss")
|
print("[login] Already authenticated after modal dismiss")
|
||||||
@@ -200,30 +201,21 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
# Fill login form
|
# Fill login form
|
||||||
try:
|
try:
|
||||||
email_field = WebDriverWait(self.driver, 10).until(
|
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:
|
except TimeoutException:
|
||||||
print("[login] Could not find login form - page may have changed")
|
print("[login] Could not find login form - page may have changed")
|
||||||
return "ERROR: Login form not found"
|
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.clear()
|
||||||
email_field.send_keys(self.massddma_username)
|
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.clear()
|
||||||
password_field.send_keys(self.massddma_password)
|
password_field.send_keys(self.massddma_password)
|
||||||
|
|
||||||
# Remember me
|
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@data-test-id='SigninSubmitButton']")))
|
||||||
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.click()
|
login_button.click()
|
||||||
|
|
||||||
# Save credentials hash after login attempt
|
# Save credentials hash after login attempt
|
||||||
@@ -234,7 +226,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
try:
|
try:
|
||||||
otp_candidate = WebDriverWait(self.driver, 30).until(
|
otp_candidate = WebDriverWait(self.driver, 30).until(
|
||||||
EC.presence_of_element_located(
|
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:
|
if otp_candidate:
|
||||||
@@ -247,7 +239,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
current_url = self.driver.current_url.lower()
|
current_url = self.driver.current_url.lower()
|
||||||
if "member" in current_url or "dashboard" in current_url:
|
if "member" in current_url or "dashboard" in current_url:
|
||||||
member_search = WebDriverWait(self.driver, 5).until(
|
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")
|
print("[login] Login successful - now on member search page")
|
||||||
return "SUCCESS"
|
return "SUCCESS"
|
||||||
@@ -264,7 +256,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
pass
|
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")
|
print("[login] Login failed - still on login page")
|
||||||
return "ERROR: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)
|
print("[login] Exception during login:", e)
|
||||||
return f"ERROR:LOGIN FAILED: {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):
|
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)
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fields = []
|
fields = []
|
||||||
if self.memberId:
|
if self.memberId:
|
||||||
fields.append(f"ID: {self.memberId}")
|
fields.append(f"SubscriberID: {self.memberId}")
|
||||||
if self.firstName:
|
|
||||||
fields.append(f"FirstName: {self.firstName}")
|
|
||||||
if self.lastName:
|
|
||||||
fields.append(f"LastName: {self.lastName}")
|
|
||||||
if self.dateOfBirth:
|
if self.dateOfBirth:
|
||||||
fields.append(f"DOB: {self.dateOfBirth}")
|
fields.append(f"DOB: {self.dateOfBirth}")
|
||||||
print(f"[DDMA step1] Starting search with: {', '.join(fields)}")
|
print(f"[DDMA step1] Starting search with: {', '.join(fields)}")
|
||||||
|
|
||||||
# Wait for the search page to be fully loaded before interacting
|
search_page_url = "https://www.deltadentalma.com/private/provider/check-patient-benefits-search"
|
||||||
wait.until(EC.presence_of_element_located(
|
|
||||||
(By.XPATH, '//input[@placeholder="Search by member ID"]')
|
|
||||||
))
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Select provider from dropdown based on NPI settings
|
# Try navigating straight to the search page first — avoids any click/visibility issues
|
||||||
self._select_provider_dropdown()
|
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):
|
if "check-patient-benefits-search" not in self.driver.current_url.lower():
|
||||||
el.click()
|
# Direct deep-link didn't stick (e.g. bounced back to sign-in/dashboard) —
|
||||||
el.send_keys(Keys.CONTROL, "a")
|
# fall back to clicking the Group/Health Connector link from the dashboard.
|
||||||
el.send_keys(Keys.BACKSPACE)
|
print(f"[DDMA step1] Direct navigation redirected to: {self.driver.current_url} — falling back to clicking the link")
|
||||||
el.send_keys(value)
|
|
||||||
|
|
||||||
# 1. Fill Member ID if provided
|
if "private/provider/dashboard" not in self.driver.current_url.lower():
|
||||||
if self.memberId:
|
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:
|
try:
|
||||||
member_id_input = wait.until(EC.presence_of_element_located(
|
connector_link = WebDriverWait(self.driver, 20).until(
|
||||||
(By.XPATH, '//input[@placeholder="Search by member ID"]')
|
EC.element_to_be_clickable((By.XPATH, link_xpath))
|
||||||
))
|
)
|
||||||
member_id_input.clear()
|
connector_link.click()
|
||||||
member_id_input.send_keys(self.memberId)
|
except TimeoutException:
|
||||||
print(f"[DDMA step1] Entered Member ID: {self.memberId}")
|
# Element may exist but be hidden (e.g. a responsive duplicate) — try any match via JS click
|
||||||
time.sleep(0.2)
|
candidates = self.driver.find_elements(By.XPATH, link_xpath)
|
||||||
except Exception as e:
|
print(f"[DDMA step1] element_to_be_clickable timed out; found {len(candidates)} matching link(s) in DOM")
|
||||||
print(f"[DDMA step1] Warning: Could not fill Member ID: {e}")
|
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:
|
if self.dateOfBirth:
|
||||||
try:
|
try:
|
||||||
dob_parts = self.dateOfBirth.split("-")
|
dob_parts = self.dateOfBirth.split("-")
|
||||||
@@ -379,54 +334,57 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
month = dob_parts[1].zfill(2)
|
month = dob_parts[1].zfill(2)
|
||||||
day = dob_parts[2].zfill(2)
|
day = dob_parts[2].zfill(2)
|
||||||
|
|
||||||
dob_container = wait.until(
|
birthdate_input = wait.until(EC.element_to_be_clickable(
|
||||||
EC.presence_of_element_located(
|
(By.XPATH, "//input[@data-test-id='InStateSearchBirthdate']")
|
||||||
(By.XPATH, "//div[@data-testid='member-search_date-of-birth']")
|
))
|
||||||
)
|
birthdate_input.click()
|
||||||
)
|
birthdate_input.send_keys(Keys.CONTROL, "a")
|
||||||
|
birthdate_input.send_keys(Keys.BACKSPACE)
|
||||||
month_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']")
|
birthdate_input.send_keys(f"{month}{day}{year}")
|
||||||
day_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']")
|
print(f"[DDMA step1] Filled Birthdate: {month}/{day}/{year}")
|
||||||
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}")
|
|
||||||
except Exception as e:
|
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)
|
time.sleep(0.3)
|
||||||
|
|
||||||
# Click Search button
|
# Click Search button
|
||||||
continue_btn = wait.until(EC.element_to_be_clickable(
|
search_btn = wait.until(EC.element_to_be_clickable(
|
||||||
(By.XPATH, '//button[@data-testid="member-search_search-button"]')
|
(By.XPATH, "//button[@data-test-id='InStateSearchButton']")
|
||||||
))
|
))
|
||||||
continue_btn.click()
|
search_btn.click()
|
||||||
print("[DDMA step1] Clicked Search button")
|
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:
|
try:
|
||||||
WebDriverWait(self.driver, 15).until(
|
WebDriverWait(self.driver, 15).until(
|
||||||
EC.any_of(
|
lambda d: d.find_elements(By.XPATH, "//table//tbody//tr")
|
||||||
EC.presence_of_element_located((By.XPATH, "//tbody//tr")),
|
or "check-patient-benefit-details" in d.current_url.lower()
|
||||||
EC.presence_of_element_located((By.XPATH, '//div[@data-testid="member-search-result-no-results"]')),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
time.sleep(3)
|
time.sleep(2)
|
||||||
except TimeoutException:
|
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
|
if "check-patient-benefit-details" in self.driver.current_url.lower():
|
||||||
try:
|
print(f"[DDMA step1] Search navigated directly to detail page: {self.driver.current_url}")
|
||||||
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
|
|
||||||
|
|
||||||
print("[DDMA step1] Search completed successfully")
|
print("[DDMA step1] Search completed successfully")
|
||||||
return "Success"
|
return "Success"
|
||||||
@@ -442,112 +400,120 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
try:
|
try:
|
||||||
import re
|
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"
|
eligibilityText = "unknown"
|
||||||
foundMemberId = self.memberId or ""
|
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
|
# DDMA now sometimes skips the family-members results table entirely and
|
||||||
try:
|
# navigates straight to the single-patient detail page (when Subscriber ID +
|
||||||
first_row = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]")
|
# DOB match exactly one person). In that case there's no members table to
|
||||||
row_text = first_row.text.strip()
|
# parse here — skip straight to the detail-page fallback extraction below.
|
||||||
print(f"[DDMA step2] First row text: {row_text[:150]}...")
|
already_on_detail_page = "check-patient-benefit-details" in self.driver.current_url.lower()
|
||||||
|
|
||||||
lines = row_text.split('\n') if row_text else []
|
rows = []
|
||||||
for line in lines:
|
target_row = None
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
if not already_on_detail_page:
|
||||||
continue
|
# Wait for results table, then pause for full render
|
||||||
if re.match(r"^[A-Za-z\s\-']{2,60}$", line):
|
try:
|
||||||
patientName = line
|
WebDriverWait(self.driver, 10).until(
|
||||||
print(f"[DDMA step2] Extracted patient name from row: '{patientName}'")
|
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
|
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:
|
try:
|
||||||
short_wait = WebDriverWait(self.driver, 3)
|
WebDriverWait(self.driver, 15).until(
|
||||||
status_link = short_wait.until(EC.presence_of_element_located((
|
lambda d: "check-patient-benefit-details" in d.current_url.lower()
|
||||||
By.XPATH,
|
)
|
||||||
"(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]"
|
print(f"[DDMA step2] Navigated to detail page: {self.driver.current_url}")
|
||||||
)))
|
|
||||||
eligibilityText = status_link.text.strip().lower()
|
|
||||||
print(f"[DDMA step2] Found eligibility status: {eligibilityText}")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
print(f"[DDMA step2] Warning: Still on search page: {self.driver.current_url}")
|
||||||
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
|
|
||||||
|
|
||||||
# Find the member-details URL from the first row
|
# Wait for meaningful content on the detail page
|
||||||
print("[DDMA step2] Looking for patient detail link...")
|
for selector in [
|
||||||
detail_url = None
|
"//*[contains(text(),'Date of Birth') or contains(text(),'Address') or contains(text(),'Member ID')]",
|
||||||
|
"//table", "//h1", "//h2",
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
link_el = WebDriverWait(self.driver, 5).until(
|
WebDriverWait(self.driver, 15).until(
|
||||||
EC.presence_of_element_located((By.XPATH, selector))
|
EC.presence_of_element_located((By.XPATH, selector))
|
||||||
)
|
)
|
||||||
href = link_el.get_attribute("href")
|
print(f"[DDMA step2] Detail page content loaded: {selector}")
|
||||||
if href and "member-details" in href:
|
break
|
||||||
detail_url = href
|
|
||||||
print(f"[DDMA step2] Found detail URL: {href}")
|
|
||||||
break
|
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if detail_url:
|
time.sleep(3) # Let JavaScript finish rendering all sections
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Confirm we actually landed on the detail page (not redirected away)
|
# Fallback: refine patient name from the detail page text if row extraction failed
|
||||||
try:
|
if not patientName:
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
page_lines = self.driver.execute_script(
|
page_lines = self.driver.execute_script(
|
||||||
"return document.body.innerText;"
|
"return document.body.innerText;"
|
||||||
@@ -576,15 +542,29 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
return False
|
return False
|
||||||
return True
|
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
|
# Scan lines for the "Name" label. When "Name" appears alone we scan
|
||||||
# forward past accessibility text to the first plausible name value.
|
# forward past accessibility text to the first plausible name value.
|
||||||
for i, line in enumerate(page_lines):
|
for i, line in enumerate(page_lines):
|
||||||
|
if patientName:
|
||||||
|
break
|
||||||
# Case 1: "Name : Value" or "Name: Value" on the same line
|
# 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(
|
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(
|
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()
|
candidate = same_line.group(1).strip()
|
||||||
if looks_like_name(candidate):
|
if looks_like_name(candidate):
|
||||||
@@ -593,7 +573,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Case 2: "Name" or "Name:" alone on a line — scan forward for value
|
# 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))):
|
for j in range(i + 1, min(i + 6, len(page_lines))):
|
||||||
candidate = page_lines[j].strip()
|
candidate = page_lines[j].strip()
|
||||||
if looks_like_name(candidate):
|
if looks_like_name(candidate):
|
||||||
@@ -602,8 +582,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
break
|
break
|
||||||
if patientName:
|
if patientName:
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
print("[DDMA step2] Warning: Could not find patient detail link")
|
|
||||||
|
|
||||||
# Clean patient name — strip any remaining date artifacts
|
# Clean patient name — strip any remaining date artifacts
|
||||||
if patientName:
|
if patientName:
|
||||||
@@ -614,6 +592,27 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
if not patientName:
|
if not patientName:
|
||||||
print("[DDMA step2] Could not extract patient name from detail page")
|
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
|
# Wait for the page to be fully ready before capturing PDF
|
||||||
try:
|
try:
|
||||||
WebDriverWait(self.driver, 30).until(
|
WebDriverWait(self.driver, 30).until(
|
||||||
@@ -650,14 +649,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
|
|
||||||
print(f"[DDMA step2] PDF saved at: {pdf_path}")
|
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}'")
|
print(f"[DDMA step2] Final — PatientName: '{patientName}', MemberID: '{foundMemberId}'")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -11,7 +11,7 @@ import json
|
|||||||
import base64
|
import base64
|
||||||
from datetime import date
|
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"
|
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_username = claim.get("uniteddhUsername", "")
|
||||||
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
||||||
|
self.payment_group_id = claim.get("paymentGroupId", "")
|
||||||
|
|
||||||
self.download_dir = get_browser_manager().download_dir
|
self.download_dir = get_browser_manager().download_dir
|
||||||
os.makedirs(self.download_dir, exist_ok=True)
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
@@ -595,11 +596,11 @@ class AutomationUnitedDHClaimSubmit:
|
|||||||
try:
|
try:
|
||||||
summit_option = WebDriverWait(self.driver, 5).until(
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
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()
|
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
|
location_selected = True
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
try:
|
try:
|
||||||
@@ -632,11 +633,11 @@ class AutomationUnitedDHClaimSubmit:
|
|||||||
try:
|
try:
|
||||||
summit_option = WebDriverWait(self.driver, 5).until(
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
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()
|
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
|
billing_selected = True
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class AutomationUnitedDHPreAuth:
|
|||||||
|
|
||||||
self.uniteddh_username = claim.get("uniteddhUsername", "")
|
self.uniteddh_username = claim.get("uniteddhUsername", "")
|
||||||
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
||||||
|
self.payment_group_id = claim.get("paymentGroupId", "")
|
||||||
|
|
||||||
self.download_dir = get_browser_manager().download_dir
|
self.download_dir = get_browser_manager().download_dir
|
||||||
os.makedirs(self.download_dir, exist_ok=True)
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
@@ -554,11 +555,11 @@ class AutomationUnitedDHPreAuth:
|
|||||||
try:
|
try:
|
||||||
summit_option = WebDriverWait(self.driver, 5).until(
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
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()
|
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
|
location_selected = True
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
try:
|
try:
|
||||||
@@ -590,11 +591,11 @@ class AutomationUnitedDHPreAuth:
|
|||||||
try:
|
try:
|
||||||
summit_option = WebDriverWait(self.driver, 5).until(
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
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()
|
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
|
billing_selected = True
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
self.lastName = self.data.get("lastName", "")
|
self.lastName = self.data.get("lastName", "")
|
||||||
self.unitedsco_username = self.data.get("unitedscoUsername", "")
|
self.unitedsco_username = self.data.get("unitedscoUsername", "")
|
||||||
self.unitedsco_password = self.data.get("unitedscoPassword", "")
|
self.unitedsco_password = self.data.get("unitedscoPassword", "")
|
||||||
|
self.payment_group_id = self.data.get("paymentGroupId", "")
|
||||||
|
|
||||||
# Use browser manager's download dir
|
# Use browser manager's download dir
|
||||||
self.download_dir = get_browser_manager().download_dir
|
self.download_dir = get_browser_manager().download_dir
|
||||||
@@ -653,11 +654,11 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
try:
|
try:
|
||||||
summit_option = WebDriverWait(self.driver, 5).until(
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
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()
|
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
|
location_selected = True
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
try:
|
try:
|
||||||
@@ -691,11 +692,11 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
try:
|
try:
|
||||||
summit_option = WebDriverWait(self.driver, 5).until(
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
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()
|
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
|
billing_selected = True
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ echo ""
|
|||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo " STEP 13 — Desktop shortcuts "
|
echo " STEP 13 — Desktop shortcuts "
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
bash ~/Desktop/DentalManagementMH06/setup-desktop-shortcut.sh
|
bash ~/Desktop/Wellspringmanagement01/setup-desktop-shortcut.sh
|
||||||
echo "✓ Desktop shortcuts created"
|
echo "✓ Desktop shortcuts created"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -79,6 +79,6 @@ echo "============================================"
|
|||||||
echo " All steps complete!"
|
echo " All steps complete!"
|
||||||
echo ""
|
echo ""
|
||||||
echo " STEP 12 — To run the app, open 2 terminals:"
|
echo " STEP 12 — To run the app, open 2 terminals:"
|
||||||
echo " Terminal 1: cd ~/Desktop/DentalManagementMH06 && npm run dev"
|
echo " Terminal 1: cd ~/Desktop/Wellspringmanagement01 && npm run dev"
|
||||||
echo " Terminal 2: cd ~/Desktop/DentalManagementMH06/apps/SeleniumService && .venv/bin/python3 agent.py"
|
echo " Terminal 2: cd ~/Desktop/Wellspringmanagement01/apps/SeleniumService && .venv/bin/python3 agent.py"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|||||||
Generated
+29
-34
@@ -142,6 +142,7 @@
|
|||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-barcode": "^1.6.1",
|
||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
@@ -274,7 +275,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -559,7 +559,8 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@chevrotain/cst-dts-gen": {
|
"node_modules/@chevrotain/cst-dts-gen": {
|
||||||
"version": "10.5.0",
|
"version": "10.5.0",
|
||||||
@@ -628,8 +629,7 @@
|
|||||||
"version": "0.3.15",
|
"version": "0.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz",
|
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz",
|
||||||
"integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==",
|
"integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@electric-sql/pglite-socket": {
|
"node_modules/@electric-sql/pglite-socket": {
|
||||||
"version": "0.0.20",
|
"version": "0.0.20",
|
||||||
@@ -5013,7 +5013,6 @@
|
|||||||
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hoist-non-react-statics": "^3.3.0"
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
},
|
},
|
||||||
@@ -5066,7 +5065,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
@@ -5131,7 +5129,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -5142,7 +5139,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -5281,7 +5277,6 @@
|
|||||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.1",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.56.1",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
@@ -5590,7 +5585,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -6136,7 +6130,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -7265,8 +7258,7 @@
|
|||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
@@ -7541,7 +7533,6 @@
|
|||||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -8765,7 +8756,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz",
|
||||||
"integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==",
|
"integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
}
|
}
|
||||||
@@ -9139,6 +9129,12 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"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": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.0.tgz",
|
||||||
"integrity": "sha512-GGaj5IMRfLv2HXXFzGk9diISMYLTpSTh6fzCZGKxWYW/NqEztIFtnXLq6G/RVhzFRmCykLap1fuC67LVKoQLcg==",
|
"integrity": "sha512-GGaj5IMRfLv2HXXFzGk9diISMYLTpSTh6fzCZGKxWYW/NqEztIFtnXLq6G/RVhzFRmCykLap1fuC67LVKoQLcg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-queue": "6.6.2"
|
"p-queue": "6.6.2"
|
||||||
},
|
},
|
||||||
@@ -10141,6 +10138,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"mustache": "bin/mustache"
|
"mustache": "bin/mustache"
|
||||||
}
|
}
|
||||||
@@ -10485,7 +10483,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz",
|
||||||
"integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==",
|
"integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.25 || ^4.0"
|
"zod": "^3.25 || ^4.0"
|
||||||
@@ -10563,6 +10560,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eventemitter3": "^4.0.4",
|
"eventemitter3": "^4.0.4",
|
||||||
"p-timeout": "^3.2.0"
|
"p-timeout": "^3.2.0"
|
||||||
@@ -10594,6 +10592,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-finally": "^1.0.0"
|
"p-finally": "^1.0.0"
|
||||||
},
|
},
|
||||||
@@ -10768,7 +10767,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
||||||
"integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
|
"integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -10801,7 +10799,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||||
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.11.0",
|
"pg-connection-string": "^2.11.0",
|
||||||
"pg-pool": "^3.11.0",
|
"pg-pool": "^3.11.0",
|
||||||
@@ -10966,7 +10963,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -11196,7 +11192,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -11889,11 +11884,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-contexify": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -11994,7 +12000,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
||||||
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -12026,7 +12031,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
@@ -12307,8 +12311,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -13240,7 +13243,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -13534,7 +13536,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -13812,7 +13813,6 @@
|
|||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.27.0",
|
"esbuild": "~0.27.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -13994,7 +13994,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -14285,7 +14284,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -14834,7 +14832,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -15073,7 +15070,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
@@ -15196,7 +15192,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"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'
|
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 = {
|
exports.Prisma.CommissionBatchScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
npiProviderId: 'npiProviderId',
|
npiProviderId: 'npiProviderId',
|
||||||
@@ -516,6 +533,12 @@ exports.Prisma.CommissionBatchItemScalarFieldEnum = {
|
|||||||
collectionAmount: 'collectionAmount'
|
collectionAmount: 'collectionAmount'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SeleniumSettingsScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
paymentGroupId: 'paymentGroupId'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -667,8 +690,10 @@ exports.Prisma.ModelName = {
|
|||||||
InsuranceContact: 'InsuranceContact',
|
InsuranceContact: 'InsuranceContact',
|
||||||
ProcedureTimeslot: 'ProcedureTimeslot',
|
ProcedureTimeslot: 'ProcedureTimeslot',
|
||||||
PatientConversation: 'PatientConversation',
|
PatientConversation: 'PatientConversation',
|
||||||
|
LabRxTemplate: 'LabRxTemplate',
|
||||||
CommissionBatch: 'CommissionBatch',
|
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",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ model User {
|
|||||||
procedureTimeslot ProcedureTimeslot?
|
procedureTimeslot ProcedureTimeslot?
|
||||||
insuranceContacts InsuranceContact[]
|
insuranceContacts InsuranceContact[]
|
||||||
patientConversations PatientConversation[]
|
patientConversations PatientConversation[]
|
||||||
|
labRxTemplates LabRxTemplate[]
|
||||||
|
seleniumSettings SeleniumSettings?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Patient {
|
model Patient {
|
||||||
@@ -694,6 +696,28 @@ model PatientConversation {
|
|||||||
@@map("patient_conversation")
|
@@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
|
// Commission tracking
|
||||||
model CommissionBatch {
|
model CommissionBatch {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
@@ -721,3 +745,13 @@ model CommissionBatchItem {
|
|||||||
@@unique([commissionBatchId, paymentId])
|
@@unique([commissionBatchId, paymentId])
|
||||||
@@index([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?
|
procedureTimeslot ProcedureTimeslot?
|
||||||
insuranceContacts InsuranceContact[]
|
insuranceContacts InsuranceContact[]
|
||||||
patientConversations PatientConversation[]
|
patientConversations PatientConversation[]
|
||||||
|
labRxTemplates LabRxTemplate[]
|
||||||
|
seleniumSettings SeleniumSettings?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Patient {
|
model Patient {
|
||||||
@@ -695,6 +697,28 @@ model PatientConversation {
|
|||||||
@@map("patient_conversation")
|
@@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
|
// Commission tracking
|
||||||
model CommissionBatch {
|
model CommissionBatch {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
@@ -722,3 +746,13 @@ model CommissionBatchItem {
|
|||||||
@@unique([commissionBatchId, paymentId])
|
@@unique([commissionBatchId, paymentId])
|
||||||
@@index([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",
|
"version": "1.0",
|
||||||
"generatorVersion": "1.0.0",
|
"generatorVersion": "1.0.0",
|
||||||
"generatedAt": "2026-06-29T02:05:45.074Z",
|
"generatedAt": "2026-09-22T04:11:49.276Z",
|
||||||
"outputPath": "/home/gg/Desktop/DentalManagementMH06/packages/db/shared",
|
"outputPath": "/home/kk/Desktop/DentalManagementMH06/packages/db/shared",
|
||||||
"files": [
|
"files": [
|
||||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||||
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
||||||
@@ -37,8 +37,10 @@
|
|||||||
"schemas/enums/InsuranceContactScalarFieldEnum.schema.ts",
|
"schemas/enums/InsuranceContactScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/ProcedureTimeslotScalarFieldEnum.schema.ts",
|
"schemas/enums/ProcedureTimeslotScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/PatientConversationScalarFieldEnum.schema.ts",
|
"schemas/enums/PatientConversationScalarFieldEnum.schema.ts",
|
||||||
|
"schemas/enums/LabRxTemplateScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/CommissionBatchScalarFieldEnum.schema.ts",
|
"schemas/enums/CommissionBatchScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/CommissionBatchItemScalarFieldEnum.schema.ts",
|
"schemas/enums/CommissionBatchItemScalarFieldEnum.schema.ts",
|
||||||
|
"schemas/enums/SeleniumSettingsScalarFieldEnum.schema.ts",
|
||||||
"schemas/enums/SortOrder.schema.ts",
|
"schemas/enums/SortOrder.schema.ts",
|
||||||
"schemas/enums/NullableJsonNullValueInput.schema.ts",
|
"schemas/enums/NullableJsonNullValueInput.schema.ts",
|
||||||
"schemas/enums/JsonNullValueInput.schema.ts",
|
"schemas/enums/JsonNullValueInput.schema.ts",
|
||||||
@@ -217,6 +219,11 @@
|
|||||||
"schemas/objects/PatientConversationWhereUniqueInput.schema.ts",
|
"schemas/objects/PatientConversationWhereUniqueInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationOrderByWithAggregationInput.schema.ts",
|
"schemas/objects/PatientConversationOrderByWithAggregationInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationScalarWhereWithAggregatesInput.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/CommissionBatchWhereInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchOrderByWithRelationInput.schema.ts",
|
"schemas/objects/CommissionBatchOrderByWithRelationInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchWhereUniqueInput.schema.ts",
|
"schemas/objects/CommissionBatchWhereUniqueInput.schema.ts",
|
||||||
@@ -227,6 +234,11 @@
|
|||||||
"schemas/objects/CommissionBatchItemWhereUniqueInput.schema.ts",
|
"schemas/objects/CommissionBatchItemWhereUniqueInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemOrderByWithAggregationInput.schema.ts",
|
"schemas/objects/CommissionBatchItemOrderByWithAggregationInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemScalarWhereWithAggregatesInput.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/UserCreateInput.schema.ts",
|
||||||
"schemas/objects/UserUncheckedCreateInput.schema.ts",
|
"schemas/objects/UserUncheckedCreateInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateInput.schema.ts",
|
"schemas/objects/UserUpdateInput.schema.ts",
|
||||||
@@ -451,6 +463,13 @@
|
|||||||
"schemas/objects/PatientConversationCreateManyInput.schema.ts",
|
"schemas/objects/PatientConversationCreateManyInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUpdateManyMutationInput.schema.ts",
|
"schemas/objects/PatientConversationUpdateManyMutationInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUncheckedUpdateManyInput.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/CommissionBatchCreateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchUncheckedCreateInput.schema.ts",
|
"schemas/objects/CommissionBatchUncheckedCreateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchUpdateInput.schema.ts",
|
"schemas/objects/CommissionBatchUpdateInput.schema.ts",
|
||||||
@@ -465,6 +484,13 @@
|
|||||||
"schemas/objects/CommissionBatchItemCreateManyInput.schema.ts",
|
"schemas/objects/CommissionBatchItemCreateManyInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemUpdateManyMutationInput.schema.ts",
|
"schemas/objects/CommissionBatchItemUpdateManyMutationInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemUncheckedUpdateManyInput.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/IntFilter.schema.ts",
|
||||||
"schemas/objects/StringFilter.schema.ts",
|
"schemas/objects/StringFilter.schema.ts",
|
||||||
"schemas/objects/BoolFilter.schema.ts",
|
"schemas/objects/BoolFilter.schema.ts",
|
||||||
@@ -489,6 +515,8 @@
|
|||||||
"schemas/objects/ProcedureTimeslotNullableScalarRelationFilter.schema.ts",
|
"schemas/objects/ProcedureTimeslotNullableScalarRelationFilter.schema.ts",
|
||||||
"schemas/objects/InsuranceContactListRelationFilter.schema.ts",
|
"schemas/objects/InsuranceContactListRelationFilter.schema.ts",
|
||||||
"schemas/objects/PatientConversationListRelationFilter.schema.ts",
|
"schemas/objects/PatientConversationListRelationFilter.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateListRelationFilter.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsNullableScalarRelationFilter.schema.ts",
|
||||||
"schemas/objects/PatientOrderByRelationAggregateInput.schema.ts",
|
"schemas/objects/PatientOrderByRelationAggregateInput.schema.ts",
|
||||||
"schemas/objects/AppointmentOrderByRelationAggregateInput.schema.ts",
|
"schemas/objects/AppointmentOrderByRelationAggregateInput.schema.ts",
|
||||||
"schemas/objects/StaffOrderByRelationAggregateInput.schema.ts",
|
"schemas/objects/StaffOrderByRelationAggregateInput.schema.ts",
|
||||||
@@ -505,6 +533,7 @@
|
|||||||
"schemas/objects/CommunicationOrderByRelationAggregateInput.schema.ts",
|
"schemas/objects/CommunicationOrderByRelationAggregateInput.schema.ts",
|
||||||
"schemas/objects/InsuranceContactOrderByRelationAggregateInput.schema.ts",
|
"schemas/objects/InsuranceContactOrderByRelationAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationOrderByRelationAggregateInput.schema.ts",
|
"schemas/objects/PatientConversationOrderByRelationAggregateInput.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateOrderByRelationAggregateInput.schema.ts",
|
||||||
"schemas/objects/UserCountOrderByAggregateInput.schema.ts",
|
"schemas/objects/UserCountOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/UserAvgOrderByAggregateInput.schema.ts",
|
"schemas/objects/UserAvgOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/UserMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/UserMaxOrderByAggregateInput.schema.ts",
|
||||||
@@ -756,6 +785,11 @@
|
|||||||
"schemas/objects/PatientConversationMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/PatientConversationMaxOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationMinOrderByAggregateInput.schema.ts",
|
"schemas/objects/PatientConversationMinOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationSumOrderByAggregateInput.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/NpiProviderScalarRelationFilter.schema.ts",
|
||||||
"schemas/objects/CommissionBatchCountOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchCountOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchAvgOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchAvgOrderByAggregateInput.schema.ts",
|
||||||
@@ -769,6 +803,11 @@
|
|||||||
"schemas/objects/CommissionBatchItemMaxOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchItemMaxOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemMinOrderByAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchItemMinOrderByAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemSumOrderByAggregateInput.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/PatientCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AppointmentCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/AppointmentCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/StaffCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/StaffCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
@@ -790,6 +829,8 @@
|
|||||||
"schemas/objects/ProcedureTimeslotCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/ProcedureTimeslotCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/InsuranceContactCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/InsuranceContactCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/AppointmentUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/AppointmentUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/StaffUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/StaffUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
@@ -811,6 +852,8 @@
|
|||||||
"schemas/objects/ProcedureTimeslotUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
"schemas/objects/ProcedureTimeslotUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/InsuranceContactUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/InsuranceContactUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateUncheckedCreateNestedManyWithoutUserInput.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsUncheckedCreateNestedOneWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/StringFieldUpdateOperationsInput.schema.ts",
|
"schemas/objects/StringFieldUpdateOperationsInput.schema.ts",
|
||||||
"schemas/objects/BoolFieldUpdateOperationsInput.schema.ts",
|
"schemas/objects/BoolFieldUpdateOperationsInput.schema.ts",
|
||||||
"schemas/objects/IntFieldUpdateOperationsInput.schema.ts",
|
"schemas/objects/IntFieldUpdateOperationsInput.schema.ts",
|
||||||
@@ -835,6 +878,8 @@
|
|||||||
"schemas/objects/ProcedureTimeslotUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/ProcedureTimeslotUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/InsuranceContactUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/InsuranceContactUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/PatientConversationUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/PatientUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/PatientUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/AppointmentUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/AppointmentUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/StaffUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/StaffUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
@@ -856,6 +901,8 @@
|
|||||||
"schemas/objects/ProcedureTimeslotUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
"schemas/objects/ProcedureTimeslotUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/InsuranceContactUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/InsuranceContactUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
"schemas/objects/PatientConversationUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateUncheckedUpdateManyWithoutUserNestedInput.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsUncheckedUpdateOneWithoutUserNestedInput.schema.ts",
|
||||||
"schemas/objects/UserCreateNestedOneWithoutPatientsInput.schema.ts",
|
"schemas/objects/UserCreateNestedOneWithoutPatientsInput.schema.ts",
|
||||||
"schemas/objects/AppointmentCreateNestedManyWithoutPatientInput.schema.ts",
|
"schemas/objects/AppointmentCreateNestedManyWithoutPatientInput.schema.ts",
|
||||||
"schemas/objects/AppointmentProcedureCreateNestedManyWithoutPatientInput.schema.ts",
|
"schemas/objects/AppointmentProcedureCreateNestedManyWithoutPatientInput.schema.ts",
|
||||||
@@ -1088,6 +1135,8 @@
|
|||||||
"schemas/objects/UserCreateNestedOneWithoutPatientConversationsInput.schema.ts",
|
"schemas/objects/UserCreateNestedOneWithoutPatientConversationsInput.schema.ts",
|
||||||
"schemas/objects/PatientUpdateOneRequiredWithoutConversationNestedInput.schema.ts",
|
"schemas/objects/PatientUpdateOneRequiredWithoutConversationNestedInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateOneRequiredWithoutPatientConversationsNestedInput.schema.ts",
|
"schemas/objects/UserUpdateOneRequiredWithoutPatientConversationsNestedInput.schema.ts",
|
||||||
|
"schemas/objects/UserCreateNestedOneWithoutLabRxTemplatesInput.schema.ts",
|
||||||
|
"schemas/objects/UserUpdateOneRequiredWithoutLabRxTemplatesNestedInput.schema.ts",
|
||||||
"schemas/objects/NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema.ts",
|
"schemas/objects/NpiProviderCreateNestedOneWithoutCommissionBatchesInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema.ts",
|
"schemas/objects/CommissionBatchItemCreateNestedManyWithoutCommissionBatchInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemUncheckedCreateNestedManyWithoutCommissionBatchInput.schema.ts",
|
"schemas/objects/CommissionBatchItemUncheckedCreateNestedManyWithoutCommissionBatchInput.schema.ts",
|
||||||
@@ -1098,6 +1147,8 @@
|
|||||||
"schemas/objects/PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema.ts",
|
"schemas/objects/PaymentCreateNestedOneWithoutCommissionBatchItemsInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchUpdateOneRequiredWithoutItemsNestedInput.schema.ts",
|
"schemas/objects/CommissionBatchUpdateOneRequiredWithoutItemsNestedInput.schema.ts",
|
||||||
"schemas/objects/PaymentUpdateOneRequiredWithoutCommissionBatchItemsNestedInput.schema.ts",
|
"schemas/objects/PaymentUpdateOneRequiredWithoutCommissionBatchItemsNestedInput.schema.ts",
|
||||||
|
"schemas/objects/UserCreateNestedOneWithoutSeleniumSettingsInput.schema.ts",
|
||||||
|
"schemas/objects/UserUpdateOneRequiredWithoutSeleniumSettingsNestedInput.schema.ts",
|
||||||
"schemas/objects/NestedIntFilter.schema.ts",
|
"schemas/objects/NestedIntFilter.schema.ts",
|
||||||
"schemas/objects/NestedStringFilter.schema.ts",
|
"schemas/objects/NestedStringFilter.schema.ts",
|
||||||
"schemas/objects/NestedBoolFilter.schema.ts",
|
"schemas/objects/NestedBoolFilter.schema.ts",
|
||||||
@@ -1227,6 +1278,13 @@
|
|||||||
"schemas/objects/PatientConversationUncheckedCreateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationUncheckedCreateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationCreateOrConnectWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationCreateOrConnectWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationCreateManyUserInputEnvelope.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/PatientUpsertWithWhereUniqueWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUpdateManyWithWhereWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUpdateManyWithWhereWithoutUserInput.schema.ts",
|
||||||
@@ -1311,6 +1369,14 @@
|
|||||||
"schemas/objects/PatientConversationUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationUpdateWithWhereUniqueWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUpdateManyWithWhereWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationUpdateManyWithWhereWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationScalarWhereInput.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/UserCreateWithoutPatientsInput.schema.ts",
|
||||||
"schemas/objects/UserUncheckedCreateWithoutPatientsInput.schema.ts",
|
"schemas/objects/UserUncheckedCreateWithoutPatientsInput.schema.ts",
|
||||||
"schemas/objects/UserCreateOrConnectWithoutPatientsInput.schema.ts",
|
"schemas/objects/UserCreateOrConnectWithoutPatientsInput.schema.ts",
|
||||||
@@ -1861,6 +1927,13 @@
|
|||||||
"schemas/objects/UserUpdateToOneWithWhereWithoutPatientConversationsInput.schema.ts",
|
"schemas/objects/UserUpdateToOneWithWhereWithoutPatientConversationsInput.schema.ts",
|
||||||
"schemas/objects/UserUpdateWithoutPatientConversationsInput.schema.ts",
|
"schemas/objects/UserUpdateWithoutPatientConversationsInput.schema.ts",
|
||||||
"schemas/objects/UserUncheckedUpdateWithoutPatientConversationsInput.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/NpiProviderCreateWithoutCommissionBatchesInput.schema.ts",
|
||||||
"schemas/objects/NpiProviderUncheckedCreateWithoutCommissionBatchesInput.schema.ts",
|
"schemas/objects/NpiProviderUncheckedCreateWithoutCommissionBatchesInput.schema.ts",
|
||||||
"schemas/objects/NpiProviderCreateOrConnectWithoutCommissionBatchesInput.schema.ts",
|
"schemas/objects/NpiProviderCreateOrConnectWithoutCommissionBatchesInput.schema.ts",
|
||||||
@@ -1889,6 +1962,13 @@
|
|||||||
"schemas/objects/PaymentUpdateToOneWithWhereWithoutCommissionBatchItemsInput.schema.ts",
|
"schemas/objects/PaymentUpdateToOneWithWhereWithoutCommissionBatchItemsInput.schema.ts",
|
||||||
"schemas/objects/PaymentUpdateWithoutCommissionBatchItemsInput.schema.ts",
|
"schemas/objects/PaymentUpdateWithoutCommissionBatchItemsInput.schema.ts",
|
||||||
"schemas/objects/PaymentUncheckedUpdateWithoutCommissionBatchItemsInput.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/PatientCreateManyUserInput.schema.ts",
|
||||||
"schemas/objects/AppointmentCreateManyUserInput.schema.ts",
|
"schemas/objects/AppointmentCreateManyUserInput.schema.ts",
|
||||||
"schemas/objects/StaffCreateManyUserInput.schema.ts",
|
"schemas/objects/StaffCreateManyUserInput.schema.ts",
|
||||||
@@ -1905,6 +1985,7 @@
|
|||||||
"schemas/objects/CommunicationCreateManyUserInput.schema.ts",
|
"schemas/objects/CommunicationCreateManyUserInput.schema.ts",
|
||||||
"schemas/objects/InsuranceContactCreateManyUserInput.schema.ts",
|
"schemas/objects/InsuranceContactCreateManyUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationCreateManyUserInput.schema.ts",
|
"schemas/objects/PatientConversationCreateManyUserInput.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateCreateManyUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUpdateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUncheckedUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUncheckedUpdateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientUncheckedUpdateManyWithoutUserInput.schema.ts",
|
"schemas/objects/PatientUncheckedUpdateManyWithoutUserInput.schema.ts",
|
||||||
@@ -1953,6 +2034,9 @@
|
|||||||
"schemas/objects/PatientConversationUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationUpdateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUncheckedUpdateWithoutUserInput.schema.ts",
|
"schemas/objects/PatientConversationUncheckedUpdateWithoutUserInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationUncheckedUpdateManyWithoutUserInput.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/AppointmentCreateManyPatientInput.schema.ts",
|
||||||
"schemas/objects/AppointmentProcedureCreateManyPatientInput.schema.ts",
|
"schemas/objects/AppointmentProcedureCreateManyPatientInput.schema.ts",
|
||||||
"schemas/objects/ClaimCreateManyPatientInput.schema.ts",
|
"schemas/objects/ClaimCreateManyPatientInput.schema.ts",
|
||||||
@@ -2225,6 +2309,11 @@
|
|||||||
"schemas/objects/PatientConversationSumAggregateInput.schema.ts",
|
"schemas/objects/PatientConversationSumAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationMinAggregateInput.schema.ts",
|
"schemas/objects/PatientConversationMinAggregateInput.schema.ts",
|
||||||
"schemas/objects/PatientConversationMaxAggregateInput.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/CommissionBatchCountAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchAvgAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchAvgAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchSumAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchSumAggregateInput.schema.ts",
|
||||||
@@ -2235,6 +2324,11 @@
|
|||||||
"schemas/objects/CommissionBatchItemSumAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchItemSumAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemMinAggregateInput.schema.ts",
|
"schemas/objects/CommissionBatchItemMinAggregateInput.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemMaxAggregateInput.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/UserCountOutputTypeSelect.schema.ts",
|
||||||
"schemas/objects/PatientCountOutputTypeSelect.schema.ts",
|
"schemas/objects/PatientCountOutputTypeSelect.schema.ts",
|
||||||
"schemas/objects/AppointmentCountOutputTypeSelect.schema.ts",
|
"schemas/objects/AppointmentCountOutputTypeSelect.schema.ts",
|
||||||
@@ -2264,6 +2358,7 @@
|
|||||||
"schemas/objects/UserCountOutputTypeCountCommunicationsArgs.schema.ts",
|
"schemas/objects/UserCountOutputTypeCountCommunicationsArgs.schema.ts",
|
||||||
"schemas/objects/UserCountOutputTypeCountInsuranceContactsArgs.schema.ts",
|
"schemas/objects/UserCountOutputTypeCountInsuranceContactsArgs.schema.ts",
|
||||||
"schemas/objects/UserCountOutputTypeCountPatientConversationsArgs.schema.ts",
|
"schemas/objects/UserCountOutputTypeCountPatientConversationsArgs.schema.ts",
|
||||||
|
"schemas/objects/UserCountOutputTypeCountLabRxTemplatesArgs.schema.ts",
|
||||||
"schemas/objects/PatientCountOutputTypeArgs.schema.ts",
|
"schemas/objects/PatientCountOutputTypeArgs.schema.ts",
|
||||||
"schemas/objects/PatientCountOutputTypeCountAppointmentsArgs.schema.ts",
|
"schemas/objects/PatientCountOutputTypeCountAppointmentsArgs.schema.ts",
|
||||||
"schemas/objects/PatientCountOutputTypeCountProceduresArgs.schema.ts",
|
"schemas/objects/PatientCountOutputTypeCountProceduresArgs.schema.ts",
|
||||||
@@ -2335,8 +2430,10 @@
|
|||||||
"schemas/objects/InsuranceContactSelect.schema.ts",
|
"schemas/objects/InsuranceContactSelect.schema.ts",
|
||||||
"schemas/objects/ProcedureTimeslotSelect.schema.ts",
|
"schemas/objects/ProcedureTimeslotSelect.schema.ts",
|
||||||
"schemas/objects/PatientConversationSelect.schema.ts",
|
"schemas/objects/PatientConversationSelect.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateSelect.schema.ts",
|
||||||
"schemas/objects/CommissionBatchSelect.schema.ts",
|
"schemas/objects/CommissionBatchSelect.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemSelect.schema.ts",
|
"schemas/objects/CommissionBatchItemSelect.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsSelect.schema.ts",
|
||||||
"schemas/objects/UserArgs.schema.ts",
|
"schemas/objects/UserArgs.schema.ts",
|
||||||
"schemas/objects/PatientArgs.schema.ts",
|
"schemas/objects/PatientArgs.schema.ts",
|
||||||
"schemas/objects/AppointmentArgs.schema.ts",
|
"schemas/objects/AppointmentArgs.schema.ts",
|
||||||
@@ -2369,8 +2466,10 @@
|
|||||||
"schemas/objects/InsuranceContactArgs.schema.ts",
|
"schemas/objects/InsuranceContactArgs.schema.ts",
|
||||||
"schemas/objects/ProcedureTimeslotArgs.schema.ts",
|
"schemas/objects/ProcedureTimeslotArgs.schema.ts",
|
||||||
"schemas/objects/PatientConversationArgs.schema.ts",
|
"schemas/objects/PatientConversationArgs.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateArgs.schema.ts",
|
||||||
"schemas/objects/CommissionBatchArgs.schema.ts",
|
"schemas/objects/CommissionBatchArgs.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemArgs.schema.ts",
|
"schemas/objects/CommissionBatchItemArgs.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsArgs.schema.ts",
|
||||||
"schemas/objects/UserInclude.schema.ts",
|
"schemas/objects/UserInclude.schema.ts",
|
||||||
"schemas/objects/PatientInclude.schema.ts",
|
"schemas/objects/PatientInclude.schema.ts",
|
||||||
"schemas/objects/AppointmentInclude.schema.ts",
|
"schemas/objects/AppointmentInclude.schema.ts",
|
||||||
@@ -2402,8 +2501,10 @@
|
|||||||
"schemas/objects/InsuranceContactInclude.schema.ts",
|
"schemas/objects/InsuranceContactInclude.schema.ts",
|
||||||
"schemas/objects/ProcedureTimeslotInclude.schema.ts",
|
"schemas/objects/ProcedureTimeslotInclude.schema.ts",
|
||||||
"schemas/objects/PatientConversationInclude.schema.ts",
|
"schemas/objects/PatientConversationInclude.schema.ts",
|
||||||
|
"schemas/objects/LabRxTemplateInclude.schema.ts",
|
||||||
"schemas/objects/CommissionBatchInclude.schema.ts",
|
"schemas/objects/CommissionBatchInclude.schema.ts",
|
||||||
"schemas/objects/CommissionBatchItemInclude.schema.ts",
|
"schemas/objects/CommissionBatchItemInclude.schema.ts",
|
||||||
|
"schemas/objects/SeleniumSettingsInclude.schema.ts",
|
||||||
"schemas/findUniqueUser.schema.ts",
|
"schemas/findUniqueUser.schema.ts",
|
||||||
"schemas/findUniqueOrThrowUser.schema.ts",
|
"schemas/findUniqueOrThrowUser.schema.ts",
|
||||||
"schemas/findFirstUser.schema.ts",
|
"schemas/findFirstUser.schema.ts",
|
||||||
@@ -2948,6 +3049,23 @@
|
|||||||
"schemas/upsertOnePatientConversation.schema.ts",
|
"schemas/upsertOnePatientConversation.schema.ts",
|
||||||
"schemas/aggregatePatientConversation.schema.ts",
|
"schemas/aggregatePatientConversation.schema.ts",
|
||||||
"schemas/groupByPatientConversation.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/findUniqueCommissionBatch.schema.ts",
|
||||||
"schemas/findUniqueOrThrowCommissionBatch.schema.ts",
|
"schemas/findUniqueOrThrowCommissionBatch.schema.ts",
|
||||||
"schemas/findFirstCommissionBatch.schema.ts",
|
"schemas/findFirstCommissionBatch.schema.ts",
|
||||||
@@ -2982,6 +3100,23 @@
|
|||||||
"schemas/upsertOneCommissionBatchItem.schema.ts",
|
"schemas/upsertOneCommissionBatchItem.schema.ts",
|
||||||
"schemas/aggregateCommissionBatchItem.schema.ts",
|
"schemas/aggregateCommissionBatchItem.schema.ts",
|
||||||
"schemas/groupByCommissionBatchItem.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/UserFindUniqueResult.schema.ts",
|
||||||
"schemas/results/UserFindFirstResult.schema.ts",
|
"schemas/results/UserFindFirstResult.schema.ts",
|
||||||
"schemas/results/UserFindManyResult.schema.ts",
|
"schemas/results/UserFindManyResult.schema.ts",
|
||||||
@@ -3398,6 +3533,19 @@
|
|||||||
"schemas/results/PatientConversationAggregateResult.schema.ts",
|
"schemas/results/PatientConversationAggregateResult.schema.ts",
|
||||||
"schemas/results/PatientConversationGroupByResult.schema.ts",
|
"schemas/results/PatientConversationGroupByResult.schema.ts",
|
||||||
"schemas/results/PatientConversationCountResult.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/CommissionBatchFindUniqueResult.schema.ts",
|
||||||
"schemas/results/CommissionBatchFindFirstResult.schema.ts",
|
"schemas/results/CommissionBatchFindFirstResult.schema.ts",
|
||||||
"schemas/results/CommissionBatchFindManyResult.schema.ts",
|
"schemas/results/CommissionBatchFindManyResult.schema.ts",
|
||||||
@@ -3424,6 +3572,19 @@
|
|||||||
"schemas/results/CommissionBatchItemAggregateResult.schema.ts",
|
"schemas/results/CommissionBatchItemAggregateResult.schema.ts",
|
||||||
"schemas/results/CommissionBatchItemGroupByResult.schema.ts",
|
"schemas/results/CommissionBatchItemGroupByResult.schema.ts",
|
||||||
"schemas/results/CommissionBatchItemCountResult.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/results/index.ts",
|
||||||
"schemas/index.ts",
|
"schemas/index.ts",
|
||||||
"schemas/variants/pure/User.pure.ts",
|
"schemas/variants/pure/User.pure.ts",
|
||||||
@@ -3458,8 +3619,10 @@
|
|||||||
"schemas/variants/pure/InsuranceContact.pure.ts",
|
"schemas/variants/pure/InsuranceContact.pure.ts",
|
||||||
"schemas/variants/pure/ProcedureTimeslot.pure.ts",
|
"schemas/variants/pure/ProcedureTimeslot.pure.ts",
|
||||||
"schemas/variants/pure/PatientConversation.pure.ts",
|
"schemas/variants/pure/PatientConversation.pure.ts",
|
||||||
|
"schemas/variants/pure/LabRxTemplate.pure.ts",
|
||||||
"schemas/variants/pure/CommissionBatch.pure.ts",
|
"schemas/variants/pure/CommissionBatch.pure.ts",
|
||||||
"schemas/variants/pure/CommissionBatchItem.pure.ts",
|
"schemas/variants/pure/CommissionBatchItem.pure.ts",
|
||||||
|
"schemas/variants/pure/SeleniumSettings.pure.ts",
|
||||||
"schemas/variants/pure/index.ts",
|
"schemas/variants/pure/index.ts",
|
||||||
"schemas/variants/input/User.input.ts",
|
"schemas/variants/input/User.input.ts",
|
||||||
"schemas/variants/input/Patient.input.ts",
|
"schemas/variants/input/Patient.input.ts",
|
||||||
@@ -3493,8 +3656,10 @@
|
|||||||
"schemas/variants/input/InsuranceContact.input.ts",
|
"schemas/variants/input/InsuranceContact.input.ts",
|
||||||
"schemas/variants/input/ProcedureTimeslot.input.ts",
|
"schemas/variants/input/ProcedureTimeslot.input.ts",
|
||||||
"schemas/variants/input/PatientConversation.input.ts",
|
"schemas/variants/input/PatientConversation.input.ts",
|
||||||
|
"schemas/variants/input/LabRxTemplate.input.ts",
|
||||||
"schemas/variants/input/CommissionBatch.input.ts",
|
"schemas/variants/input/CommissionBatch.input.ts",
|
||||||
"schemas/variants/input/CommissionBatchItem.input.ts",
|
"schemas/variants/input/CommissionBatchItem.input.ts",
|
||||||
|
"schemas/variants/input/SeleniumSettings.input.ts",
|
||||||
"schemas/variants/input/index.ts",
|
"schemas/variants/input/index.ts",
|
||||||
"schemas/variants/result/User.result.ts",
|
"schemas/variants/result/User.result.ts",
|
||||||
"schemas/variants/result/Patient.result.ts",
|
"schemas/variants/result/Patient.result.ts",
|
||||||
@@ -3528,8 +3693,10 @@
|
|||||||
"schemas/variants/result/InsuranceContact.result.ts",
|
"schemas/variants/result/InsuranceContact.result.ts",
|
||||||
"schemas/variants/result/ProcedureTimeslot.result.ts",
|
"schemas/variants/result/ProcedureTimeslot.result.ts",
|
||||||
"schemas/variants/result/PatientConversation.result.ts",
|
"schemas/variants/result/PatientConversation.result.ts",
|
||||||
|
"schemas/variants/result/LabRxTemplate.result.ts",
|
||||||
"schemas/variants/result/CommissionBatch.result.ts",
|
"schemas/variants/result/CommissionBatch.result.ts",
|
||||||
"schemas/variants/result/CommissionBatchItem.result.ts",
|
"schemas/variants/result/CommissionBatchItem.result.ts",
|
||||||
|
"schemas/variants/result/SeleniumSettings.result.ts",
|
||||||
"schemas/variants/result/index.ts",
|
"schemas/variants/result/index.ts",
|
||||||
"schemas/variants/index.ts"
|
"schemas/variants/index.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { Prisma } from '../../generated/prisma';
|
import type { Prisma } from '../../generated/prisma';
|
||||||
import Decimal from 'decimal.js';
|
|
||||||
|
import Decimal from "decimal.js";
|
||||||
|
|
||||||
// DECIMAL HELPERS
|
// DECIMAL HELPERS
|
||||||
//------------------------------------------------------
|
//------------------------------------------------------
|
||||||
@@ -23,7 +24,7 @@ export const isValidDecimalInput = (
|
|||||||
if (v === undefined || v === null) return false;
|
if (v === undefined || v === null) return false;
|
||||||
return (
|
return (
|
||||||
// Explicit instance checks first
|
// 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
|
// If Decimal.js is present and imported by the generator, this symbol exists at runtime
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore - Decimal may be undefined when not installed; codegen controls the import
|
// @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(),
|
procedureTimeslot: z.boolean().optional(),
|
||||||
insuranceContacts: z.boolean().optional(),
|
insuranceContacts: z.boolean().optional(),
|
||||||
patientConversations: z.boolean().optional(),
|
patientConversations: z.boolean().optional(),
|
||||||
|
labRxTemplates: z.boolean().optional(),
|
||||||
|
seleniumSettings: z.boolean().optional(),
|
||||||
_count: z.boolean().optional()
|
_count: z.boolean().optional()
|
||||||
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
||||||
|
|
||||||
@@ -76,6 +78,8 @@ export const UserFindFirstOrThrowSelectZodSchema = z.object({
|
|||||||
procedureTimeslot: z.boolean().optional(),
|
procedureTimeslot: z.boolean().optional(),
|
||||||
insuranceContacts: z.boolean().optional(),
|
insuranceContacts: z.boolean().optional(),
|
||||||
patientConversations: z.boolean().optional(),
|
patientConversations: z.boolean().optional(),
|
||||||
|
labRxTemplates: z.boolean().optional(),
|
||||||
|
seleniumSettings: z.boolean().optional(),
|
||||||
_count: z.boolean().optional()
|
_count: z.boolean().optional()
|
||||||
}).strict();
|
}).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(),
|
procedureTimeslot: z.boolean().optional(),
|
||||||
insuranceContacts: z.boolean().optional(),
|
insuranceContacts: z.boolean().optional(),
|
||||||
patientConversations: z.boolean().optional(),
|
patientConversations: z.boolean().optional(),
|
||||||
|
labRxTemplates: z.boolean().optional(),
|
||||||
|
seleniumSettings: z.boolean().optional(),
|
||||||
_count: z.boolean().optional()
|
_count: z.boolean().optional()
|
||||||
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
||||||
|
|
||||||
@@ -76,6 +78,8 @@ export const UserFindFirstSelectZodSchema = z.object({
|
|||||||
procedureTimeslot: z.boolean().optional(),
|
procedureTimeslot: z.boolean().optional(),
|
||||||
insuranceContacts: z.boolean().optional(),
|
insuranceContacts: z.boolean().optional(),
|
||||||
patientConversations: z.boolean().optional(),
|
patientConversations: z.boolean().optional(),
|
||||||
|
labRxTemplates: z.boolean().optional(),
|
||||||
|
seleniumSettings: z.boolean().optional(),
|
||||||
_count: z.boolean().optional()
|
_count: z.boolean().optional()
|
||||||
}).strict();
|
}).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(),
|
procedureTimeslot: z.boolean().optional(),
|
||||||
insuranceContacts: z.boolean().optional(),
|
insuranceContacts: z.boolean().optional(),
|
||||||
patientConversations: z.boolean().optional(),
|
patientConversations: z.boolean().optional(),
|
||||||
|
labRxTemplates: z.boolean().optional(),
|
||||||
|
seleniumSettings: z.boolean().optional(),
|
||||||
_count: z.boolean().optional()
|
_count: z.boolean().optional()
|
||||||
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
}).strict() as unknown as z.ZodType<Prisma.UserSelect>;
|
||||||
|
|
||||||
@@ -76,6 +78,8 @@ export const UserFindManySelectZodSchema = z.object({
|
|||||||
procedureTimeslot: z.boolean().optional(),
|
procedureTimeslot: z.boolean().optional(),
|
||||||
insuranceContacts: z.boolean().optional(),
|
insuranceContacts: z.boolean().optional(),
|
||||||
patientConversations: z.boolean().optional(),
|
patientConversations: z.boolean().optional(),
|
||||||
|
labRxTemplates: z.boolean().optional(),
|
||||||
|
seleniumSettings: z.boolean().optional(),
|
||||||
_count: z.boolean().optional()
|
_count: z.boolean().optional()
|
||||||
}).strict();
|
}).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/InsuranceContactScalarFieldEnum.schema'
|
||||||
export * from './enums/ProcedureTimeslotScalarFieldEnum.schema'
|
export * from './enums/ProcedureTimeslotScalarFieldEnum.schema'
|
||||||
export * from './enums/PatientConversationScalarFieldEnum.schema'
|
export * from './enums/PatientConversationScalarFieldEnum.schema'
|
||||||
|
export * from './enums/LabRxTemplateScalarFieldEnum.schema'
|
||||||
export * from './enums/CommissionBatchScalarFieldEnum.schema'
|
export * from './enums/CommissionBatchScalarFieldEnum.schema'
|
||||||
export * from './enums/CommissionBatchItemScalarFieldEnum.schema'
|
export * from './enums/CommissionBatchItemScalarFieldEnum.schema'
|
||||||
|
export * from './enums/SeleniumSettingsScalarFieldEnum.schema'
|
||||||
export * from './enums/SortOrder.schema'
|
export * from './enums/SortOrder.schema'
|
||||||
export * from './enums/NullableJsonNullValueInput.schema'
|
export * from './enums/NullableJsonNullValueInput.schema'
|
||||||
export * from './enums/JsonNullValueInput.schema'
|
export * from './enums/JsonNullValueInput.schema'
|
||||||
@@ -595,6 +597,23 @@ export * from './updateManyAndReturnPatientConversation.schema'
|
|||||||
export * from './upsertOnePatientConversation.schema'
|
export * from './upsertOnePatientConversation.schema'
|
||||||
export * from './aggregatePatientConversation.schema'
|
export * from './aggregatePatientConversation.schema'
|
||||||
export * from './groupByPatientConversation.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 './findUniqueCommissionBatch.schema'
|
||||||
export * from './findUniqueOrThrowCommissionBatch.schema'
|
export * from './findUniqueOrThrowCommissionBatch.schema'
|
||||||
export * from './findFirstCommissionBatch.schema'
|
export * from './findFirstCommissionBatch.schema'
|
||||||
@@ -629,6 +648,23 @@ export * from './updateManyAndReturnCommissionBatchItem.schema'
|
|||||||
export * from './upsertOneCommissionBatchItem.schema'
|
export * from './upsertOneCommissionBatchItem.schema'
|
||||||
export * from './aggregateCommissionBatchItem.schema'
|
export * from './aggregateCommissionBatchItem.schema'
|
||||||
export * from './groupByCommissionBatchItem.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/UserFindUniqueResult.schema'
|
||||||
export * from './results/UserFindFirstResult.schema'
|
export * from './results/UserFindFirstResult.schema'
|
||||||
export * from './results/UserFindManyResult.schema'
|
export * from './results/UserFindManyResult.schema'
|
||||||
@@ -1045,6 +1081,19 @@ export * from './results/PatientConversationDeleteManyResult.schema'
|
|||||||
export * from './results/PatientConversationAggregateResult.schema'
|
export * from './results/PatientConversationAggregateResult.schema'
|
||||||
export * from './results/PatientConversationGroupByResult.schema'
|
export * from './results/PatientConversationGroupByResult.schema'
|
||||||
export * from './results/PatientConversationCountResult.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/CommissionBatchFindUniqueResult.schema'
|
||||||
export * from './results/CommissionBatchFindFirstResult.schema'
|
export * from './results/CommissionBatchFindFirstResult.schema'
|
||||||
export * from './results/CommissionBatchFindManyResult.schema'
|
export * from './results/CommissionBatchFindManyResult.schema'
|
||||||
@@ -1071,6 +1120,19 @@ export * from './results/CommissionBatchItemDeleteManyResult.schema'
|
|||||||
export * from './results/CommissionBatchItemAggregateResult.schema'
|
export * from './results/CommissionBatchItemAggregateResult.schema'
|
||||||
export * from './results/CommissionBatchItemGroupByResult.schema'
|
export * from './results/CommissionBatchItemGroupByResult.schema'
|
||||||
export * from './results/CommissionBatchItemCountResult.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 './results/index'
|
||||||
export * from './objects/index'
|
export * from './objects/index'
|
||||||
export * from './variants/pure/User.pure'
|
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/InsuranceContact.pure'
|
||||||
export * from './variants/pure/ProcedureTimeslot.pure'
|
export * from './variants/pure/ProcedureTimeslot.pure'
|
||||||
export * from './variants/pure/PatientConversation.pure'
|
export * from './variants/pure/PatientConversation.pure'
|
||||||
|
export * from './variants/pure/LabRxTemplate.pure'
|
||||||
export * from './variants/pure/CommissionBatch.pure'
|
export * from './variants/pure/CommissionBatch.pure'
|
||||||
export * from './variants/pure/CommissionBatchItem.pure'
|
export * from './variants/pure/CommissionBatchItem.pure'
|
||||||
|
export * from './variants/pure/SeleniumSettings.pure'
|
||||||
export * from './variants/pure/index'
|
export * from './variants/pure/index'
|
||||||
export * from './variants/input/User.input'
|
export * from './variants/input/User.input'
|
||||||
export * from './variants/input/Patient.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/InsuranceContact.input'
|
||||||
export * from './variants/input/ProcedureTimeslot.input'
|
export * from './variants/input/ProcedureTimeslot.input'
|
||||||
export * from './variants/input/PatientConversation.input'
|
export * from './variants/input/PatientConversation.input'
|
||||||
|
export * from './variants/input/LabRxTemplate.input'
|
||||||
export * from './variants/input/CommissionBatch.input'
|
export * from './variants/input/CommissionBatch.input'
|
||||||
export * from './variants/input/CommissionBatchItem.input'
|
export * from './variants/input/CommissionBatchItem.input'
|
||||||
|
export * from './variants/input/SeleniumSettings.input'
|
||||||
export * from './variants/input/index'
|
export * from './variants/input/index'
|
||||||
export * from './variants/result/User.result'
|
export * from './variants/result/User.result'
|
||||||
export * from './variants/result/Patient.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/InsuranceContact.result'
|
||||||
export * from './variants/result/ProcedureTimeslot.result'
|
export * from './variants/result/ProcedureTimeslot.result'
|
||||||
export * from './variants/result/PatientConversation.result'
|
export * from './variants/result/PatientConversation.result'
|
||||||
|
export * from './variants/result/LabRxTemplate.result'
|
||||||
export * from './variants/result/CommissionBatch.result'
|
export * from './variants/result/CommissionBatch.result'
|
||||||
export * from './variants/result/CommissionBatchItem.result'
|
export * from './variants/result/CommissionBatchItem.result'
|
||||||
|
export * from './variants/result/SeleniumSettings.result'
|
||||||
export * from './variants/result/index'
|
export * from './variants/result/index'
|
||||||
export * from './variants/index'
|
export * from './variants/index'
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { Prisma } from '../../../generated/prisma';
|
import type { Prisma } from '../../../generated/prisma';
|
||||||
import Decimal from 'decimal.js';
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema';
|
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { AppointmentCreateNestedOneWithoutProceduresInputObjectSchema as AppointmentCreateNestedOneWithoutProceduresInputObjectSchema } from './AppointmentCreateNestedOneWithoutProceduresInput.schema';
|
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
||||||
import { PatientCreateNestedOneWithoutProceduresInputObjectSchema as PatientCreateNestedOneWithoutProceduresInputObjectSchema } from './PatientCreateNestedOneWithoutProceduresInput.schema';
|
|
||||||
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
import { NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema as NpiProviderCreateNestedOneWithoutAppointmentProceduresInputObjectSchema } from './NpiProviderCreateNestedOneWithoutAppointmentProceduresInput.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
|
import Decimal from "decimal.js";
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
procedureCode: z.string(),
|
procedureCode: z.string(),
|
||||||
procedureLabel: z.string().optional().nullable(),
|
procedureLabel: z.string().optional().nullable(),
|
||||||
@@ -14,7 +15,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Prisma.Decimal),
|
z.instanceof(Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
message: "Field 'fee' must be a Decimal",
|
||||||
|
|||||||
+4
-3
@@ -1,9 +1,10 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { Prisma } from '../../../generated/prisma';
|
import type { Prisma } from '../../../generated/prisma';
|
||||||
import Decimal from 'decimal.js';
|
|
||||||
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
import { ProcedureSourceSchema } from '../enums/ProcedureSource.schema'
|
||||||
|
|
||||||
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
import { DecimalJSLikeSchema, isValidDecimalInput } from '../../helpers/decimal-helpers';
|
||||||
|
|
||||||
|
import Decimal from "decimal.js";
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
patientId: z.number().int(),
|
patientId: z.number().int(),
|
||||||
@@ -14,7 +15,7 @@ const makeSchema = () => z.object({
|
|||||||
z.number(),
|
z.number(),
|
||||||
z.string(),
|
z.string(),
|
||||||
z.instanceof(Decimal),
|
z.instanceof(Decimal),
|
||||||
z.instanceof(Prisma.Decimal),
|
z.instanceof(Decimal),
|
||||||
DecimalJSLikeSchema,
|
DecimalJSLikeSchema,
|
||||||
]).refine((v) => isValidDecimalInput(v), {
|
]).refine((v) => isValidDecimalInput(v), {
|
||||||
message: "Field 'fee' must be a Decimal",
|
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