diff --git a/.turbo/preferences/tui.json b/.turbo/preferences/tui.json index efb36255..f46ddd7d 100644 --- a/.turbo/preferences/tui.json +++ b/.turbo/preferences/tui.json @@ -1,4 +1,4 @@ { "is_task_list_visible": true, - "active_task": "frontend#dev" + "active_task": "backend#dev" } \ No newline at end of file diff --git a/apps/Backend/src/ai/internal-chat-graph.ts b/apps/Backend/src/ai/internal-chat-graph.ts index 0af064f8..8fbd17e3 100644 --- a/apps/Backend/src/ai/internal-chat-graph.ts +++ b/apps/Backend/src/ai/internal-chat-graph.ts @@ -102,8 +102,12 @@ Intents: e.g. "claim #13 crown for flor and claim d5212 for bian" e.g. "claim perio exam for Maria and claim adult prophy for John" e.g. "claim D0120 for Jane and D1110 for Bob" + e.g. "claim post #6 for A and comp #10 DL for B, all on 6/23/26" + → claimGroups: [{patientName:"A", procedureNames:["post #6"]}, {patientName:"B", procedureNames:["comp #10 DL"]}], appointmentDate: "2026-06-23" Use this when each patient has their OWN distinct set of procedures. Put each patient+procedures group into the "claimGroups" array. + A trailing date phrase like "all on " or "on " at the end of the message sets + appointmentDate for EVERY group — do not skip it just because it comes after multiple patients. Do NOT use batch_claim for this — batch_claim is ONLY for the SAME procedures applied to ALL patients. - batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy" @@ -192,9 +196,13 @@ Rules: Extract just the name (without "Dr." prefix unless it's part of the name), omit if not mentioned - Keep fallbackReply to 1-2 sentences - For navigate intents, fallbackReply = "Opening the [page] page..." (e.g. "Opening the eligibility page...") -- appointmentDate applies to BOTH schedule_appointment AND claim_only/check_and_claim: +- appointmentDate applies to ALL claim/scheduling intents — schedule_appointment, claim_only, + check_and_claim, batch_claim, multi_claim, batch_check_and_claim, and preauth: always set it to today's date (${today}) when the user says "today", "this visit", or similar set it to the specified date when the user mentions a date (e.g. "05/15/2026") + This applies even when the date is mentioned once at the END of the message and covers + multiple patients/groups (e.g. "...claim X for A and Y for B, all on 6/23/26" → appointmentDate + applies to every group, not just the last one mentioned) omit it only when no date is mentioned at all (the backend will find the last appointment) - IMPORTANT: Users type dates in American M/D/YYYY format (month first, then day). e.g. "6/12/2026" means June 12 2026 (NOT December 6). "1/5/2026" means January 5 2026. diff --git a/packages/db/prisma.config.js b/packages/db/prisma.config.js new file mode 100644 index 00000000..a2622144 --- /dev/null +++ b/packages/db/prisma.config.js @@ -0,0 +1,15 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv_1 = __importDefault(require("dotenv")); +const path_1 = __importDefault(require("path")); +const config_1 = require("prisma/config"); +dotenv_1.default.config({ path: path_1.default.resolve(__dirname, ".env") }); +exports.default = (0, config_1.defineConfig)({ + schema: "prisma/schema.prisma", + datasource: { + url: (0, config_1.env)("DATABASE_URL"), + }, +}); diff --git a/packages/db/prisma/prisma.config.js b/packages/db/prisma/prisma.config.js new file mode 100644 index 00000000..bedb4328 --- /dev/null +++ b/packages/db/prisma/prisma.config.js @@ -0,0 +1,18 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv_1 = __importDefault(require("dotenv")); +const path_1 = __importDefault(require("path")); +const config_1 = require("prisma/config"); +dotenv_1.default.config({ path: path_1.default.resolve(__dirname, ".env") }); +exports.default = (0, config_1.defineConfig)({ + schema: "schema.prisma", + datasource: { + url: (0, config_1.env)("DATABASE_URL"), + }, + migrations: { + seed: "ts-node prisma/seed.ts", + }, +}); diff --git a/packages/db/prisma/seed.js b/packages/db/prisma/seed.js new file mode 100644 index 00000000..06f52326 --- /dev/null +++ b/packages/db/prisma/seed.js @@ -0,0 +1,54 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv_1 = __importDefault(require("dotenv")); +const path_1 = __importDefault(require("path")); +dotenv_1.default.config({ path: path_1.default.resolve(__dirname, ".env") }); +const prisma_1 = require("../generated/prisma"); +const adapter_pg_1 = require("@prisma/adapter-pg"); +const bcrypt_1 = __importDefault(require("bcrypt")); +const adapter = new adapter_pg_1.PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new prisma_1.PrismaClient({ adapter }); +function main() { + return __awaiter(this, void 0, void 0, function* () { + const hashedPassword = yield bcrypt_1.default.hash("123456", 10); + const admin = yield prisma.user.upsert({ + where: { username: "admin" }, + update: {}, + create: { username: "admin", password: hashedPassword }, + }); + console.log("Seed complete: admin user created (username: admin, password: 123456)"); + // Seed 5 default staff members — rename these to real staff names in Settings + const defaultStaff = ["A", "B", "C", "D", "E"]; + for (const name of defaultStaff) { + const existing = yield prisma.staff.findFirst({ + where: { userId: admin.id, name }, + }); + if (!existing) { + yield prisma.staff.create({ + data: { userId: admin.id, name, role: "Staff" }, + }); + } + } + console.log("Seed complete: 5 default staff members created (A, B, C, D, E)"); + }); +} +main() + .catch((e) => { + console.error(e); + process.exit(1); +}) + .finally(() => __awaiter(void 0, void 0, void 0, function* () { + yield prisma.$disconnect(); +})); diff --git a/packages/db/scripts/patch-prisma-imports.js b/packages/db/scripts/patch-prisma-imports.js new file mode 100644 index 00000000..1f999e3c --- /dev/null +++ b/packages/db/scripts/patch-prisma-imports.js @@ -0,0 +1,223 @@ +#!/usr/bin/env ts-node +"use strict"; +/** + * patch-prisma-imports.ts (SAFE) + * + * - Converts value-level imports/exports of `Prisma` -> type-only imports/exports + * (splits mixed imports). + * - Replaces runtime usages of `Prisma.Decimal` -> `Decimal`. + * - Ensures exactly one `import Decimal from "decimal.js";` per file. + * - DEDICATED: only modifies TypeScript source files (.ts/.tsx). + * - SKIPS: files under packages/db/generated/prisma (the Prisma runtime package). + * + * Usage: + * npx ts-node packages/db/scripts/patch-prisma-imports.ts + * + * Run after `prisma generate` (and make sure generated runtime .js are restored + * if they were modified — see notes below). + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const fast_glob_1 = __importDefault(require("fast-glob")); +const repoRoot = process.cwd(); +const GENERATED_FRAGMENT = path_1.default.join("packages", "db", "generated", "prisma"); +// Only operate on TS sources (do NOT touch .js) +const GLOBS = [ + "packages/db/shared/**/*.ts", + "packages/db/shared/**/*.tsx", + "packages/db/generated/**/*.ts", + "packages/db/generated/**/*.tsx", +]; +// -------------------- helpers -------------------- +function isFromGeneratedPrisma(fromPath) { + // match relative imports that include generated/prisma + return (fromPath.includes("generated/prisma") || + fromPath.includes("/generated/prisma") || + fromPath.includes("\\generated\\prisma")); +} +function splitSpecifiers(list) { + return list + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +} +function buildNamedImport(specs) { + return `{ ${specs.join(", ")} }`; +} +function extractDecimalLines(src) { + const lines = src.split(/\r?\n/); + const matches = []; + const regexes = [ + /^import\s+Decimal\s+from\s+['"]decimal\.js['"]\s*;?/, + /^import\s+\{\s*Decimal\s*\}\s+from\s+['"]decimal\.js['"]\s*;?/, + /^import\s+\*\s+as\s+Decimal\s+from\s+['"]decimal\.js['"]\s*;?/, + /^(const|let|var)\s+Decimal\s*=\s*require\(\s*['"]decimal\.js['"]\s*\)\s*;?/, + /^(const|let|var)\s+Decimal\s*=\s*require\(\s*['"]decimal\.js['"]\s*\)\.default\s*;?/, + ]; + lines.forEach((line, i) => { + for (const re of regexes) { + if (re.test(line)) { + matches.push(i); + break; + } + } + }); + return { lines, matches }; +} +function ensureSingleDecimalImport(src) { + const { lines, matches } = extractDecimalLines(src); + if (matches.length === 0) + return src; + // remove all matched import/require lines + // do in reverse index order to keep indices valid + matches + .slice() + .sort((a, b) => b - a) + .forEach((idx) => lines.splice(idx, 1)); + let result = lines.join("\n"); + // insert single canonical import if missing + if (!/import\s+Decimal\s+from\s+['"]decimal\.js['"]/.test(result)) { + const importBlockMatch = result.match(/^(?:\s*import[\s\S]*?;\r?\n)+/); + if (importBlockMatch && importBlockMatch.index !== undefined) { + const idx = importBlockMatch[0].length; + result = + result.slice(0, idx) + + `\nimport Decimal from "decimal.js";\n` + + result.slice(idx); + } + else { + result = `import Decimal from "decimal.js";\n` + result; + } + } + // collapse excessive blank lines + result = result.replace(/\n{3,}/g, "\n\n"); + return result; +} +function replacePrismaDecimalRuntime(src) { + if (!/\bPrisma\.Decimal\b/.test(src)) + return { out: src, changed: false }; + // mask import/export-from lines so we don't accidentally change them + const placeholder = "__MASK_IMPORT_EXPORT__" + Math.random().toString(36).slice(2); + const saved = []; + const masked = src.replace(/(^\s*(?:import|export)\s+[\s\S]*?from\s+['"][^'"]+['"]\s*;?)/gm, (m) => { + saved.push(m); + return `${placeholder}${saved.length - 1}__\n`; + }); + const replaced = masked.replace(/\bPrisma\.Decimal\b/g, "Decimal"); + const restored = replaced.replace(new RegExp(`${placeholder}(\\d+)__\\n`, "g"), (_m, i) => saved[Number(i)] || ""); + return { out: restored, changed: true }; +} +// -------------------- patching logic -------------------- +function patchFileContent(src, filePath) { + // safety: do not edit runtime prisma package files + const normalized = path_1.default.normalize(filePath); + if (normalized.includes(path_1.default.normalize(GENERATED_FRAGMENT))) { + // skip any files inside packages/db/generated/prisma + return { out: src, changed: false, skipped: true }; + } + let out = src; + let changed = false; + // 1) Named imports + out = out.replace(/import\s+(?!type)(\{[^}]+\})\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, specBlock, q, fromPath) => { + if (!isFromGeneratedPrisma(fromPath)) + return match; + const specList = specBlock.replace(/^\{|\}$/g, "").trim(); + const specs = splitSpecifiers(specList); + const prismaEntries = specs.filter((s) => /^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s)); + const otherEntries = specs.filter((s) => !/^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s)); + if (prismaEntries.length === 0) + return match; + changed = true; + let replacement = `import type ${buildNamedImport(prismaEntries)} from ${q}${fromPath}${q};`; + if (otherEntries.length > 0) { + replacement += `\nimport ${buildNamedImport(otherEntries)} from ${q}${fromPath}${q};`; + } + return replacement; + }); + // 2) Named exports + out = out.replace(/export\s+(?!type)(\{[^}]+\})\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, specBlock, q, fromPath) => { + if (!isFromGeneratedPrisma(fromPath)) + return match; + const specList = specBlock.replace(/^\{|\}$/g, "").trim(); + const specs = splitSpecifiers(specList); + const prismaEntries = specs.filter((s) => /^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s)); + const otherEntries = specs.filter((s) => !/^\s*Prisma(\s+as\s+\w+)?\s*$/.test(s)); + if (prismaEntries.length === 0) + return match; + changed = true; + let replacement = `export type ${buildNamedImport(prismaEntries)} from ${q}${fromPath}${q};`; + if (otherEntries.length > 0) { + replacement += `\nexport ${buildNamedImport(otherEntries)} from ${q}${fromPath}${q};`; + } + return replacement; + }); + // 3) Namespace imports + out = out.replace(/import\s+\*\s+as\s+([A-Za-z0-9_$]+)\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, ns, q, fromPath) => { + if (!isFromGeneratedPrisma(fromPath)) + return match; + changed = true; + return `import type * as ${ns} from ${q}${fromPath}${q};`; + }); + // 4) Default imports + out = out.replace(/import\s+(?!type)([A-Za-z0-9_$]+)\s+from\s+(['"])([^'"]+)\2\s*;?/gm, (match, binding, q, fromPath) => { + if (!isFromGeneratedPrisma(fromPath)) + return match; + changed = true; + return `import type ${binding} from ${q}${fromPath}${q};`; + }); + // 5) Replace Prisma.Decimal -> Decimal safely + if (/\bPrisma\.Decimal\b/.test(out)) { + const { out: decimalOut, changed: decimalChanged } = replacePrismaDecimalRuntime(out); + out = decimalOut; + if (decimalChanged) + changed = true; + // Ensure a single Decimal import exists + out = ensureSingleDecimalImport(out); + } + return { out, changed, skipped: false }; +} +// -------------------- runner -------------------- +function run() { + return __awaiter(this, void 0, void 0, function* () { + const files = yield (0, fast_glob_1.default)(GLOBS, { absolute: true, cwd: repoRoot, dot: true }); + if (!files || files.length === 0) { + console.warn("No files matched. Check the GLOBS patterns and run from repo root."); + return; + } + for (const file of files) { + try { + const src = fs_1.default.readFileSync(file, "utf8"); + const { out, changed, skipped } = patchFileContent(src, file); + if (skipped) { + // intentionally skipped runtime-prisma files + continue; + } + if (changed && out !== src) { + fs_1.default.writeFileSync(file, out, "utf8"); + console.log("patched:", path_1.default.relative(repoRoot, file)); + } + } + catch (err) { + console.error("failed patching", file, err); + } + } + console.log("done."); + }); +} +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/db/scripts/patch-zod-buffer.js b/packages/db/scripts/patch-zod-buffer.js new file mode 100644 index 00000000..74bf6ba1 --- /dev/null +++ b/packages/db/scripts/patch-zod-buffer.js @@ -0,0 +1,19 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const dir = path_1.default.resolve(__dirname, '../shared/schemas/objects'); +fs_1.default.readdirSync(dir).forEach(file => { + if (!file.endsWith('.schema.ts')) + return; + const full = path_1.default.join(dir, file); + let content = fs_1.default.readFileSync(full, 'utf8'); + if (content.includes('z.instanceof(Buffer)') && !content.includes("import { Buffer")) { + content = `import { Buffer } from 'buffer';\n` + content; + fs_1.default.writeFileSync(full, content); + console.log('Patched:', file); + } +}); diff --git a/packages/db/types/index.js b/packages/db/types/index.js index 94ca0aa9..0ba7336d 100644 --- a/packages/db/types/index.js +++ b/packages/db/types/index.js @@ -30,3 +30,4 @@ __exportStar(require("./payments-reports-types"), exports); __exportStar(require("./patientConnection-types"), exports); __exportStar(require("./npiProviders-types"), exports); __exportStar(require("./patientDocument-types"), exports); +__exportStar(require("./shopping-vendor-types"), exports); diff --git a/packages/db/types/patient-types.js b/packages/db/types/patient-types.js index 95825d89..8ee6d1fc 100644 --- a/packages/db/types/patient-types.js +++ b/packages/db/types/patient-types.js @@ -35,7 +35,12 @@ exports.insertPatientSchema = usedSchemas_1.PatientUncheckedCreateInputObjectSch createdAt: true, }) .extend({ - insuranceId: exports.insuranceIdSchema, // enforce numeric insuranceId + firstName: zod_1.z.string().optional().default(""), + lastName: zod_1.z.string().optional().default(""), + dateOfBirth: zod_1.z.preprocess((val) => (val === null || val === undefined || val === "" ? undefined : val), zod_1.z.coerce.date({ required_error: "Date of birth is required" })), + gender: zod_1.z.string().optional().nullable(), + phone: zod_1.z.string().optional().nullable(), + insuranceId: exports.insuranceIdSchema, }); exports.updatePatientSchema = usedSchemas_1.PatientUncheckedCreateInputObjectSchema .omit({ diff --git a/packages/db/types/shopping-vendor-types.js b/packages/db/types/shopping-vendor-types.js new file mode 100644 index 00000000..4f55b30f --- /dev/null +++ b/packages/db/types/shopping-vendor-types.js @@ -0,0 +1,5 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.insertShoppingVendorSchema = void 0; +const usedSchemas_1 = require("@repo/db/usedSchemas"); +exports.insertShoppingVendorSchema = usedSchemas_1.ShoppingVendorUncheckedCreateInputObjectSchema.omit({ id: true });