feat: add Lab RX feature with template management and auto-seed defaults

- New lab_rx_template table with sortOrder and Prisma migrations
- Backend CRUD + reorder routes at /api/lab-rx/templates
- Auto-seeds 3 Highland Dental Studio defaults on first use per user
- LabRxModal: template picker, inline rename, up/down reorder, full
  template form with sections (Template Name / Lab Info / Case Details)
- RX popup: Tooth Number, Shade, Due Date, Doctor (from NPI providers),
  Instructions pre-filled with "Please make / Thank you!", print output
- lab-rx-templates.ts config file for future git-managed additions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 23:20:44 -04:00
co-authored by Claude Sonnet 4.6
parent ac4d8c4c74
commit abb2a0ac96
908 changed files with 4858 additions and 14271 deletions
@@ -1,22 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const AiSettingsResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
apiKey: z.string(),
aiEnabled: z.boolean(),
openAiKey: z.string(),
openAiEnabled: z.boolean(),
claudeAiKey: z.string(),
claudeAiEnabled: z.boolean(),
claudeAiModel: z.string(),
openAiModel: z.string(),
googleAiModel: z.string(),
dentalMgmtKey: z.string(),
dentalMgmtEnabled: z.boolean(),
afterHoursEnabled: z.boolean(),
openPhoneReply: z.boolean(),
user: z.unknown()
}).strict();
export type AiSettingsResultType = z.infer<typeof AiSettingsResultSchema>;
@@ -1,29 +0,0 @@
import * as z from 'zod';
import { PatientStatusSchema } from '../../enums/PatientStatus.schema';
// prettier-ignore
export const AppointmentResultSchema = z.object({
id: z.number().int(),
patientId: z.number().int(),
userId: z.number().int(),
staffId: z.number().int(),
title: z.string(),
date: z.date(),
startTime: z.string(),
endTime: z.string(),
type: z.string(),
typeLocked: z.boolean(),
notes: z.string().nullable(),
procedureCodeNotes: z.string().nullable(),
status: z.string(),
movedByAi: z.boolean(),
createdAt: z.date(),
eligibilityStatus: PatientStatusSchema,
patient: z.unknown(),
user: z.unknown(),
staff: z.unknown().nullable(),
procedures: z.array(z.unknown()),
claims: z.array(z.unknown()),
files: z.array(z.unknown())
}).strict();
export type AppointmentResultType = z.infer<typeof AppointmentResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const AppointmentFileResultSchema = z.object({
id: z.number().int(),
appointmentId: z.number().int(),
filename: z.string(),
mimeType: z.string().nullable(),
filePath: z.string().nullable(),
appointment: z.unknown()
}).strict();
export type AppointmentFileResultType = z.infer<typeof AppointmentFileResultSchema>;
@@ -1,24 +0,0 @@
import * as z from 'zod';
import { ProcedureSourceSchema } from '../../enums/ProcedureSource.schema';
// prettier-ignore
export const AppointmentProcedureResultSchema = z.object({
id: z.number().int(),
appointmentId: z.number().int(),
patientId: z.number().int(),
npiProviderId: z.number().int().nullable(),
procedureCode: z.string(),
procedureLabel: z.string().nullable(),
fee: z.number().nullable(),
category: z.string().nullable(),
toothNumber: z.string().nullable(),
toothSurface: z.string().nullable(),
oralCavityArea: z.string().nullable(),
source: ProcedureSourceSchema,
comboKey: z.string().nullable(),
createdAt: z.date(),
appointment: z.unknown(),
patient: z.unknown(),
npiProvider: z.unknown().nullable()
}).strict();
export type AppointmentProcedureResultType = z.infer<typeof AppointmentProcedureResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const BackupDestinationResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
path: z.string(),
isActive: z.boolean(),
createdAt: z.date(),
user: z.unknown()
}).strict();
export type BackupDestinationResultType = z.infer<typeof BackupDestinationResultSchema>;
@@ -1,35 +0,0 @@
import * as z from 'zod';
import { MissingTeethStatusSchema } from '../../enums/MissingTeethStatus.schema';
import { ClaimStatusSchema } from '../../enums/ClaimStatus.schema';
// prettier-ignore
export const ClaimResultSchema = z.object({
id: z.number().int(),
patientId: z.number().int(),
appointmentId: z.number().int().int().nullable(),
userId: z.number().int(),
staffId: z.number().int(),
patientName: z.string(),
memberId: z.string(),
dateOfBirth: z.date(),
remarks: z.string(),
missingTeethStatus: MissingTeethStatusSchema,
missingTeeth: z.unknown().nullable(),
serviceDate: z.date(),
insuranceProvider: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
status: ClaimStatusSchema,
claimNumber: z.string().nullable(),
preAuthNumber: z.string().nullable(),
npiProviderId: z.number().int().nullable(),
patient: z.unknown(),
appointment: z.unknown().nullable(),
user: z.unknown().nullable(),
staff: z.unknown().nullable(),
npiProvider: z.unknown().nullable(),
serviceLines: z.array(z.unknown()),
claimFiles: z.array(z.unknown()),
payment: z.unknown().nullable()
}).strict();
export type ClaimResultType = z.infer<typeof ClaimResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const ClaimFileResultSchema = z.object({
id: z.number().int(),
claimId: z.number().int(),
filename: z.string(),
mimeType: z.string(),
filePath: z.string().nullable(),
claim: z.unknown()
}).strict();
export type ClaimFileResultType = z.infer<typeof ClaimFileResultSchema>;
@@ -1,20 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const CloudFileResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
name: z.string(),
mimeType: z.string().nullable(),
fileSize: z.bigint(),
folderId: z.number().int().nullable(),
isComplete: z.boolean(),
totalChunks: z.number().int().nullable(),
diskPath: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
user: z.unknown(),
folder: z.unknown().nullable(),
chunks: z.array(z.unknown())
}).strict();
export type CloudFileResultType = z.infer<typeof CloudFileResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const CloudFileChunkResultSchema = z.object({
id: z.number().int(),
fileId: z.number().int(),
seq: z.number().int(),
data: z.instanceof(Uint8Array),
createdAt: z.date(),
file: z.unknown()
}).strict();
export type CloudFileChunkResultType = z.infer<typeof CloudFileChunkResultSchema>;
@@ -1,18 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const CloudFolderResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
name: z.string(),
parentId: z.number().int().nullable(),
patientId: z.number().int().nullable(),
parent: z.unknown().nullable(),
children: z.array(z.unknown()),
user: z.unknown(),
patient: z.unknown().nullable(),
files: z.array(z.unknown()),
createdAt: z.date(),
updatedAt: z.date()
}).strict();
export type CloudFolderResultType = z.infer<typeof CloudFolderResultSchema>;
@@ -1,14 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const CommissionBatchResultSchema = z.object({
id: z.number().int(),
npiProviderId: z.number().int(),
totalCollection: z.number(),
commissionAmount: z.number(),
notes: z.string().nullable(),
createdAt: z.date(),
npiProvider: z.unknown(),
items: z.array(z.unknown())
}).strict();
export type CommissionBatchResultType = z.infer<typeof CommissionBatchResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const CommissionBatchItemResultSchema = z.object({
id: z.number().int(),
commissionBatchId: z.number().int(),
paymentId: z.number().int(),
collectionAmount: z.number(),
commissionBatch: z.unknown(),
payment: z.unknown()
}).strict();
export type CommissionBatchItemResultType = z.infer<typeof CommissionBatchItemResultSchema>;
@@ -1,21 +0,0 @@
import * as z from 'zod';
import { CommunicationChannelSchema } from '../../enums/CommunicationChannel.schema';
import { CommunicationDirectionSchema } from '../../enums/CommunicationDirection.schema';
import { CommunicationStatusSchema } from '../../enums/CommunicationStatus.schema';
// prettier-ignore
export const CommunicationResultSchema = z.object({
id: z.number().int(),
patientId: z.number().int(),
userId: z.number().int().nullable(),
channel: CommunicationChannelSchema,
direction: CommunicationDirectionSchema,
status: CommunicationStatusSchema,
body: z.string().nullable(),
callDuration: z.number().int().nullable(),
twilioSid: z.string().nullable(),
createdAt: z.date(),
patient: z.unknown(),
user: z.unknown().nullable()
}).strict();
export type CommunicationResultType = z.infer<typeof CommunicationResultSchema>;
@@ -1,13 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const CronJobLogResultSchema = z.object({
id: z.number().int(),
jobName: z.string(),
status: z.string(),
startedAt: z.date(),
completedAt: z.date().nullable(),
durationMs: z.number().int().nullable(),
errorMessage: z.string().nullable()
}).strict();
export type CronJobLogResultType = z.infer<typeof CronJobLogResultSchema>;
@@ -1,10 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const DatabaseBackupResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
createdAt: z.date(),
user: z.unknown()
}).strict();
export type DatabaseBackupResultType = z.infer<typeof DatabaseBackupResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const InsuranceContactResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
name: z.string(),
phoneNumber: z.string().nullable(),
createdAt: z.date(),
user: z.unknown()
}).strict();
export type InsuranceContactResultType = z.infer<typeof InsuranceContactResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const InsuranceCredentialResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
siteKey: z.string(),
username: z.string(),
password: z.string(),
user: z.unknown()
}).strict();
export type InsuranceCredentialResultType = z.infer<typeof InsuranceCredentialResultSchema>;
@@ -1,14 +0,0 @@
import * as z from 'zod';
import { NotificationTypesSchema } from '../../enums/NotificationTypes.schema';
// prettier-ignore
export const NotificationResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
type: NotificationTypesSchema,
message: z.string(),
createdAt: z.date(),
read: z.boolean(),
user: z.unknown()
}).strict();
export type NotificationResultType = z.infer<typeof NotificationResultSchema>;
@@ -1,17 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const NpiProviderResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
npiNumber: z.string(),
providerName: z.string(),
sortOrder: z.number().int(),
createdAt: z.date(),
user: z.unknown(),
claims: z.array(z.unknown()),
payments: z.array(z.unknown()),
commissionBatches: z.array(z.unknown()),
appointmentProcedures: z.array(z.unknown())
}).strict();
export type NpiProviderResultType = z.infer<typeof NpiProviderResultSchema>;
@@ -1,19 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const OfficeContactResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
officeName: z.string().nullable(),
receptionistName: z.string().nullable(),
dentistName: z.string().nullable(),
phoneNumber: z.string().nullable(),
email: z.string().nullable(),
fax: z.string().nullable(),
streetAddress: z.string().nullable(),
city: z.string().nullable(),
state: z.string().nullable(),
zipCode: z.string().nullable(),
user: z.unknown()
}).strict();
export type OfficeContactResultType = z.infer<typeof OfficeContactResultSchema>;
@@ -1,10 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const OfficeHoursResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
data: z.unknown(),
user: z.unknown()
}).strict();
export type OfficeHoursResultType = z.infer<typeof OfficeHoursResultSchema>;
@@ -1,38 +0,0 @@
import * as z from 'zod';
import { PatientStatusSchema } from '../../enums/PatientStatus.schema';
// prettier-ignore
export const PatientResultSchema = z.object({
id: z.number().int(),
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.date().nullable(),
gender: z.string(),
phone: z.string(),
email: z.string().nullable(),
address: z.string().nullable(),
city: z.string().nullable(),
zipCode: z.string().nullable(),
insuranceProvider: z.string().nullable(),
insuranceId: z.string().nullable(),
groupNumber: z.string().nullable(),
policyHolder: z.string().nullable(),
allergies: z.string().nullable(),
medicalConditions: z.string().nullable(),
preferredLanguage: z.string().nullable(),
status: PatientStatusSchema,
userId: z.number().int(),
createdAt: z.date(),
updatedAt: z.date(),
user: z.unknown(),
appointments: z.array(z.unknown()),
procedures: z.array(z.unknown()),
claims: z.array(z.unknown()),
groups: z.array(z.unknown()),
payment: z.array(z.unknown()),
communications: z.array(z.unknown()),
documents: z.array(z.unknown()),
conversation: z.unknown().nullable(),
cloudFolders: z.array(z.unknown())
}).strict();
export type PatientResultType = z.infer<typeof PatientResultSchema>;
@@ -1,14 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const PatientConversationResultSchema = z.object({
id: z.number().int(),
patientId: z.number().int(),
userId: z.number().int(),
stage: z.string(),
aiHandoff: z.boolean(),
updatedAt: z.date(),
patient: z.unknown(),
user: z.unknown()
}).strict();
export type PatientConversationResultType = z.infer<typeof PatientConversationResultSchema>;
@@ -1,16 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const PatientDocumentResultSchema = z.object({
id: z.number().int(),
patientId: z.number().int(),
filename: z.string(),
originalName: z.string(),
mimeType: z.string(),
fileSize: z.bigint(),
filePath: z.string(),
uploadedAt: z.date(),
updatedAt: z.date(),
patient: z.unknown()
}).strict();
export type PatientDocumentResultType = z.infer<typeof PatientDocumentResultSchema>;
@@ -1,32 +0,0 @@
import * as z from 'zod';
import { PaymentStatusSchema } from '../../enums/PaymentStatus.schema';
// prettier-ignore
export const PaymentResultSchema = z.object({
id: z.number().int(),
claimId: z.number().int().nullable(),
patientId: z.number().int(),
userId: z.number().int(),
updatedById: z.number().int().nullable(),
npiProviderId: z.number().int().nullable(),
totalBilled: z.number(),
totalPaid: z.number(),
totalAdjusted: z.number(),
totalDue: z.number(),
mhPaidAmount: z.number().nullable(),
copayment: z.number(),
adjustment: z.number(),
status: PaymentStatusSchema,
notes: z.string().nullable(),
icn: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
claim: z.unknown().nullable(),
patient: z.unknown(),
updatedBy: z.unknown().nullable(),
npiProvider: z.unknown().nullable(),
serviceLineTransactions: z.array(z.unknown()),
serviceLines: z.array(z.unknown()),
commissionBatchItems: z.array(z.unknown())
}).strict();
export type PaymentResultType = z.infer<typeof PaymentResultSchema>;
@@ -1,12 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const PdfFileResultSchema = z.object({
id: z.number().int(),
filename: z.string(),
pdfData: z.instanceof(Uint8Array),
uploadedAt: z.date(),
groupId: z.number().int(),
group: z.unknown()
}).strict();
export type PdfFileResultType = z.infer<typeof PdfFileResultSchema>;
@@ -1,14 +0,0 @@
import * as z from 'zod';
import { PdfTitleKeySchema } from '../../enums/PdfTitleKey.schema';
// prettier-ignore
export const PdfGroupResultSchema = z.object({
id: z.number().int(),
title: z.string(),
titleKey: PdfTitleKeySchema,
createdAt: z.date(),
patientId: z.number().int(),
patient: z.unknown(),
pdfs: z.array(z.unknown())
}).strict();
export type PdfGroupResultType = z.infer<typeof PdfGroupResultSchema>;
@@ -1,10 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const ProcedureTimeslotResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
data: z.unknown(),
user: z.unknown()
}).strict();
export type ProcedureTimeslotResultType = z.infer<typeof ProcedureTimeslotResultSchema>;
@@ -1,27 +0,0 @@
import * as z from 'zod';
import { ServiceLineStatusSchema } from '../../enums/ServiceLineStatus.schema';
// prettier-ignore
export const ServiceLineResultSchema = z.object({
id: z.number().int(),
claimId: z.number().int().nullable(),
paymentId: z.number().int().nullable(),
procedureCode: z.string(),
procedureDate: z.date(),
quad: z.string().nullable(),
arch: z.string().nullable(),
toothNumber: z.string().nullable(),
toothSurface: z.string().nullable(),
icn: z.string().nullable(),
paidCode: z.string().nullable(),
allowedAmount: z.number().nullable(),
totalBilled: z.number(),
totalPaid: z.number(),
totalAdjusted: z.number(),
totalDue: z.number(),
status: ServiceLineStatusSchema,
claim: z.unknown().nullable(),
payment: z.unknown().nullable(),
serviceLineTransactions: z.array(z.unknown())
}).strict();
export type ServiceLineResultType = z.infer<typeof ServiceLineResultSchema>;
@@ -1,20 +0,0 @@
import * as z from 'zod';
import { PaymentMethodSchema } from '../../enums/PaymentMethod.schema';
// prettier-ignore
export const ServiceLineTransactionResultSchema = z.object({
id: z.number().int(),
paymentId: z.number().int(),
serviceLineId: z.number().int(),
transactionId: z.string().nullable(),
paidAmount: z.number(),
adjustedAmount: z.number(),
method: PaymentMethodSchema,
receivedDate: z.date(),
payerName: z.string().nullable(),
notes: z.string().nullable(),
createdAt: z.date(),
payment: z.unknown(),
serviceLine: z.unknown()
}).strict();
export type ServiceLineTransactionResultType = z.infer<typeof ServiceLineTransactionResultSchema>;
@@ -1,13 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const ShoppingVendorResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
vendorName: z.string(),
websiteUrl: z.string(),
loginUsername: z.string(),
loginPassword: z.string(),
user: z.unknown()
}).strict();
export type ShoppingVendorResultType = z.infer<typeof ShoppingVendorResultSchema>;
@@ -1,16 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const StaffResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
name: z.string(),
email: z.string().nullable(),
role: z.string(),
phone: z.string().nullable(),
createdAt: z.date(),
user: z.unknown().nullable(),
appointments: z.array(z.unknown()),
claims: z.array(z.unknown())
}).strict();
export type StaffResultType = z.infer<typeof StaffResultSchema>;
@@ -1,14 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const TwilioSettingsResultSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
accountSid: z.string(),
authToken: z.string(),
phoneNumber: z.string(),
greetingMessage: z.string().nullable(),
templates: z.unknown().nullable(),
user: z.unknown()
}).strict();
export type TwilioSettingsResultType = z.infer<typeof TwilioSettingsResultSchema>;
@@ -1,37 +0,0 @@
import * as z from 'zod';
// prettier-ignore
export const UserResultSchema = z.object({
id: z.number().int(),
username: z.string(),
password: z.string(),
autoBackupEnabled: z.boolean(),
autoBackupHour: z.number().int(),
usbBackupEnabled: z.boolean(),
usbBackupHour: z.number().int(),
autoMhCheckEnabled: z.boolean(),
autoMhCheckDayOfWeek: z.number().int(),
autoMhCheckHour: z.number().int(),
patients: z.array(z.unknown()),
appointments: z.array(z.unknown()),
staff: z.array(z.unknown()),
npiProviders: z.array(z.unknown()),
claims: z.array(z.unknown()),
insuranceCredentials: z.array(z.unknown()),
shoppingVendors: z.array(z.unknown()),
updatedPayments: z.array(z.unknown()),
backups: z.array(z.unknown()),
backupDestinations: z.array(z.unknown()),
notifications: z.array(z.unknown()),
cloudFolders: z.array(z.unknown()),
cloudFiles: z.array(z.unknown()),
communications: z.array(z.unknown()),
twilioSettings: z.unknown().nullable(),
aiSettings: z.unknown().nullable(),
officeHours: z.unknown().nullable(),
officeContact: z.unknown().nullable(),
procedureTimeslot: z.unknown().nullable(),
insuranceContacts: z.array(z.unknown()),
patientConversations: z.array(z.unknown())
}).strict();
export type UserResultType = z.infer<typeof UserResultSchema>;
@@ -1,39 +0,0 @@
/**
* Result Variant Schemas
* Auto-generated - do not edit manually
*/
export { UserResultSchema } from './User.result';
export { PatientResultSchema } from './Patient.result';
export { AppointmentResultSchema } from './Appointment.result';
export { AppointmentFileResultSchema } from './AppointmentFile.result';
export { StaffResultSchema } from './Staff.result';
export { NpiProviderResultSchema } from './NpiProvider.result';
export { AppointmentProcedureResultSchema } from './AppointmentProcedure.result';
export { ClaimResultSchema } from './Claim.result';
export { ServiceLineResultSchema } from './ServiceLine.result';
export { ClaimFileResultSchema } from './ClaimFile.result';
export { InsuranceCredentialResultSchema } from './InsuranceCredential.result';
export { ShoppingVendorResultSchema } from './ShoppingVendor.result';
export { PdfGroupResultSchema } from './PdfGroup.result';
export { PdfFileResultSchema } from './PdfFile.result';
export { PaymentResultSchema } from './Payment.result';
export { ServiceLineTransactionResultSchema } from './ServiceLineTransaction.result';
export { DatabaseBackupResultSchema } from './DatabaseBackup.result';
export { BackupDestinationResultSchema } from './BackupDestination.result';
export { NotificationResultSchema } from './Notification.result';
export { CronJobLogResultSchema } from './CronJobLog.result';
export { CloudFolderResultSchema } from './CloudFolder.result';
export { CloudFileResultSchema } from './CloudFile.result';
export { CloudFileChunkResultSchema } from './CloudFileChunk.result';
export { CommunicationResultSchema } from './Communication.result';
export { PatientDocumentResultSchema } from './PatientDocument.result';
export { TwilioSettingsResultSchema } from './TwilioSettings.result';
export { AiSettingsResultSchema } from './AiSettings.result';
export { OfficeHoursResultSchema } from './OfficeHours.result';
export { OfficeContactResultSchema } from './OfficeContact.result';
export { InsuranceContactResultSchema } from './InsuranceContact.result';
export { ProcedureTimeslotResultSchema } from './ProcedureTimeslot.result';
export { PatientConversationResultSchema } from './PatientConversation.result';
export { CommissionBatchResultSchema } from './CommissionBatch.result';
export { CommissionBatchItemResultSchema } from './CommissionBatchItem.result';