Dental Care

This commit is contained in:
Iliyan Angelov
2025-11-16 14:29:51 +02:00
commit 39077550ef
194 changed files with 43197 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
/**
* Cleanup script to remove orphaned appointments
*
* Orphaned appointments are appointments that reference non-existent:
* - patients (patientId doesn't exist in User table)
* - dentists (dentistId doesn't exist in User table)
* - services (serviceId doesn't exist in Service table)
*
* Run with: npx ts-node --project prisma/tsconfig.json prisma/cleanup-orphaned-appointments.ts
*/
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function cleanupOrphanedAppointments() {
console.log("🔍 Starting cleanup of orphaned appointments...\n");
try {
// Get all appointments
const allAppointments = await prisma.appointment.findMany({
select: {
id: true,
patientId: true,
dentistId: true,
serviceId: true,
date: true,
status: true,
},
});
console.log(`📊 Found ${allAppointments.length} total appointments\n`);
// Get all valid IDs
const [validPatientIds, validDentistIds, validServiceIds] = await Promise.all([
prisma.user.findMany({
select: { id: true },
where: { role: "patient" },
}),
prisma.user.findMany({
select: { id: true },
where: { role: "dentist" },
}),
prisma.service.findMany({
select: { id: true },
}),
]);
const validPatientIdSet = new Set(validPatientIds.map((u) => u.id));
const validDentistIdSet = new Set(validDentistIds.map((u) => u.id));
const validServiceIdSet = new Set(validServiceIds.map((s) => s.id));
// Find orphaned appointments
const orphanedAppointments = allAppointments.filter(
(apt) =>
!validPatientIdSet.has(apt.patientId) ||
!validDentistIdSet.has(apt.dentistId) ||
!validServiceIdSet.has(apt.serviceId)
);
if (orphanedAppointments.length === 0) {
console.log("✅ No orphaned appointments found. Database is clean!\n");
return;
}
console.log(`⚠️ Found ${orphanedAppointments.length} orphaned appointments:\n`);
// Log details
orphanedAppointments.forEach((apt) => {
const issues: string[] = [];
if (!validPatientIdSet.has(apt.patientId)) {
issues.push(`invalid patientId: ${apt.patientId}`);
}
if (!validDentistIdSet.has(apt.dentistId)) {
issues.push(`invalid dentistId: ${apt.dentistId}`);
}
if (!validServiceIdSet.has(apt.serviceId)) {
issues.push(`invalid serviceId: ${apt.serviceId}`);
}
console.log(` - Appointment ${apt.id}: ${issues.join(", ")} (Status: ${apt.status}, Date: ${apt.date.toISOString()})`);
});
console.log("\n🗑 Deleting orphaned appointments...\n");
// Delete orphaned appointments
const orphanedIds = orphanedAppointments.map((apt) => apt.id);
const result = await prisma.appointment.deleteMany({
where: {
id: { in: orphanedIds },
},
});
console.log(`✅ Successfully deleted ${result.count} orphaned appointment(s)\n`);
console.log("🎉 Cleanup complete!\n");
} catch (error) {
console.error("❌ Error during cleanup:", error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Run cleanup
cleanupOrphanedAppointments();

95
prisma/create-indexes.ts Normal file
View File

@@ -0,0 +1,95 @@
import { MongoClient } from "mongodb";
/**
* Creates database indexes for optimal query performance
* Run with: npx ts-node prisma/create-indexes.ts
*/
async function createIndexes() {
if (!process.env.DATABASE_URL) {
console.error("❌ DATABASE_URL environment variable is not set");
process.exit(1);
}
const client = new MongoClient(process.env.DATABASE_URL);
try {
await client.connect();
console.log("✅ Connected to MongoDB");
const db = client.db();
console.log("📊 Creating indexes...");
// Appointment indexes for status, date, and user filtering
console.log(" - Creating appointment indexes...");
await db.collection("appointment").createIndex({ status: 1 });
await db.collection("appointment").createIndex({ date: 1 });
await db.collection("appointment").createIndex({ createdAt: -1 });
await db
.collection("appointment")
.createIndex({ patientId: 1, status: 1, date: 1 });
await db
.collection("appointment")
.createIndex({ dentistId: 1, status: 1, date: 1 });
await db
.collection("appointment")
.createIndex({ status: 1, updatedAt: -1 });
// User indexes for role-based queries
console.log(" - Creating user indexes...");
await db.collection("user").createIndex({ role: 1 });
await db.collection("user").createIndex({ role: 1, createdAt: -1 });
await db.collection("user").createIndex({ emailVerified: 1 });
await db.collection("user").createIndex({ role: 1, isAvailable: 1 });
// Payment indexes for status and user queries
console.log(" - Creating payment indexes...");
await db.collection("payment").createIndex({ status: 1 });
await db.collection("payment").createIndex({ userId: 1, status: 1 });
await db.collection("payment").createIndex({ paidAt: -1 });
await db.collection("payment").createIndex({ status: 1, paidAt: -1 });
// Service indexes
console.log(" - Creating service indexes...");
await db.collection("service").createIndex({ isActive: 1 });
await db.collection("service").createIndex({ category: 1, isActive: 1 });
// Session indexes for auth performance
console.log(" - Creating session indexes...");
await db.collection("session").createIndex({ expiresAt: 1 });
await db.collection("session").createIndex({ userId: 1 });
console.log("\n✅ All indexes created successfully!");
// Display created indexes
console.log("\n📋 Index Summary:");
const collections = [
"appointment",
"user",
"payment",
"service",
"session",
];
for (const collectionName of collections) {
const indexes = await db.collection(collectionName).indexes();
console.log(`\n${collectionName}:`);
indexes.forEach((index) => {
const keyStr = Object.entries(index.key)
.map(([k, v]) => `${k}:${v}`)
.join(", ");
console.log(` - ${index.name}: { ${keyStr} }`);
});
}
} catch (error) {
console.error("❌ Error creating indexes:", error);
process.exit(1);
} finally {
await client.close();
console.log("\n✅ Disconnected from MongoDB");
}
}
createIndexes().catch((error) => {
console.error("❌ Fatal error:", error);
process.exit(1);
});

View File

@@ -0,0 +1,152 @@
import { PrismaClient } from "@prisma/client";
import { doctors } from"lib/types/doctor";
import { allServices } from "lib/types/services-data";
const prisma = new PrismaClient();
// Enhanced dentist data with full profiles
const dentistProfiles = [
{
...doctors[0],
email: "kath.estrada@dentalucare.com",
phone: "+63 912 345 6789",
qualifications: "Doctor of Dental Medicine (DMD), Orthodontics Specialist",
experience: 8,
},
{
...doctors[1],
email: "clyrelle.cervantes@dentalucare.com",
phone: "+63 912 345 6790",
qualifications:
"Doctor of Dental Surgery (DDS), Cosmetic Dentistry Specialist",
experience: 6,
},
{
...doctors[2],
email: "von.arguelles@dentalucare.com",
phone: "+63 912 345 6791",
qualifications: "Doctor of Dental Medicine (DMD), Oral Surgery Specialist",
experience: 10,
},
{
...doctors[3],
email: "dexter.cabanag@dentalucare.com",
phone: "+63 912 345 6792",
qualifications: "Doctor of Dental Surgery (DDS), Periodontics Specialist",
experience: 7,
},
];
async function main() {
console.log("🌱 Starting database seeding...");
// Clear existing data (optional - comment out if you want to keep existing data)
console.log("🗑️ Cleaning existing data...");
await prisma.service.deleteMany({});
await prisma.user.deleteMany({
where: {
role: "dentist",
},
});
// Seed Dentists
console.log("👨‍⚕️ Seeding dentists...");
const createdDentists = [];
const password = "dentalucaredentist@123"; // Plain text password (better-auth will hash it)
for (const doctor of dentistProfiles) {
const dentist = await prisma.user.create({
data: {
id: doctor.id,
name: doctor.name,
email: doctor.email,
emailVerified: true,
role: "dentist",
image: doctor.avatar,
specialization: doctor.role,
isAvailable: true,
phone: doctor.phone,
qualifications: doctor.qualifications,
experience: doctor.experience,
workingHours: {
monday: { start: "09:00", end: "17:00" },
tuesday: { start: "09:00", end: "17:00" },
wednesday: { start: "09:00", end: "17:00" },
thursday: { start: "09:00", end: "17:00" },
friday: { start: "09:00", end: "17:00" },
saturday: { start: "09:00", end: "13:00" },
sunday: { closed: true },
},
},
});
// Create account with password for the dentist (better-auth uses "credential" as providerId)
await prisma.account.create({
data: {
id: `${dentist.id}:credential`,
userId: dentist.id,
accountId: `${dentist.id}:credential`,
providerId: "credential",
password: password,
},
});
createdDentists.push(dentist);
console.log(
` ✓ Created dentist: ${dentist.name} (${dentist.specialization}) - email: ${doctor.email}`
);
console.log(` Password: dentalucaredentist@123`);
}
// Seed Services
console.log("🦷 Seeding services...");
const createdServices = [];
for (const service of allServices) {
const createdService = await prisma.service.create({
data: {
id: service.id, // Use the ID from the service data
name: service.name,
description: service.description || service.name,
duration: service.duration,
price: service.price, // Keep price as string (e.g., "₱1,500 ₱3,000")
category: service.category,
isActive: service.isActive,
},
});
createdServices.push(createdService);
console.log(
` ✓ Created service: ${createdService.name} (${createdService.price})`
);
}
console.log("\n✅ Seeding completed successfully!");
console.log(`📊 Summary:`);
console.log(` - Dentists created: ${createdDentists.length}`);
console.log(` - Services created: ${createdServices.length}`);
console.log("\n👨 Dentists:");
createdDentists.forEach((dentist) => {
console.log(` - ${dentist.name} (${dentist.specialization})`);
});
console.log("\n🦷 Service Categories:");
const categoryCounts = createdServices.reduce(
(acc: Record<string, number>, service) => {
acc[service.category] = (acc[service.category] || 0) + 1;
return acc;
},
{}
);
Object.entries(categoryCounts).forEach(([category, count]) => {
console.log(` - ${category}: ${count} services`);
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error("❌ Error during seeding:", e);
await prisma.$disconnect();
process.exit(1);
});

228
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,228 @@
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
enum role {
patient
dentist
admin
}
enum AppointmentStatus {
pending
confirmed
cancelled
completed
rescheduled
}
enum PaymentStatus {
pending
paid
failed
refunded
}
enum PaymentMethod {
card
e_wallet
bank_transfer
cash
}
model User {
id String @id @map("_id")
name String
email String
emailVerified Boolean @default(false)
image String?
role role @default(patient)
phone String?
address String?
dateOfBirth DateTime?
medicalHistory String?
stripeCustomerId String? // Stripe customer ID
// Dentist-specific fields
specialization String?
qualifications String?
experience Int? // Years of experience
workingHours Json? // {monday: {start: "9:00", end: "17:00"}, ...}
isAvailable Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
sessions Session[]
accounts Account[]
// Relations
appointmentsAsPatient Appointment[] @relation("PatientAppointments")
appointmentsAsDentist Appointment[] @relation("DentistAppointments")
payments Payment[]
notifications Notification[]
chatMessages ChatMessage[]
@@unique([email])
@@map("user")
}
model Service {
id String @id @map("_id") // Use custom ID from seed data
name String
description String?
duration Int // in minutes
price String // Price range or single price (e.g., "₱500 ₱1,500" or "₱1,500")
category String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appointments Appointment[]
@@map("service")
}
model Appointment {
id String @id @default(auto()) @map("_id") @db.ObjectId
patientId String
dentistId String
serviceId String // Changed to String to match Service.id
date DateTime
timeSlot String // e.g., "09:00-10:00"
status AppointmentStatus @default(pending)
notes String?
cancelReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
patient User @relation("PatientAppointments", fields: [patientId], references: [id], onDelete: Cascade)
dentist User @relation("DentistAppointments", fields: [dentistId], references: [id], onDelete: Cascade)
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
payment Payment?
@@map("appointment")
}
model Payment {
id String @id @default(auto()) @map("_id") @db.ObjectId
appointmentId String @unique @db.ObjectId
userId String
amount Float
method PaymentMethod
status PaymentStatus @default(pending)
transactionId String?
paidAt DateTime?
stripeCustomerId String?
stripeServicesId String?
stripePaymentIntentId String?
stripePriceId String?
stripeSubscriptionId String?
stripeInvoiceId String?
stripeRefundId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("payment")
}
model Notification {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
title String
message String
type String // email, sms, push
isRead Boolean @default(false)
sentAt DateTime @default(now())
createdAt DateTime @default(now())
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("notification")
}
model ChatMessage {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
message String
isFromUser Boolean @default(true)
createdAt DateTime @default(now())
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("chat_message")
}
model Session {
id String @id @map("_id")
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
activeOrganizationId String?
impersonatedBy String?
@@unique([token])
@@map("session")
}
model Account {
id String @id @map("_id")
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("account")
}
model Verification {
id String @id @map("_id")
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("verification")
}
model Subscription {
id String @id @default(auto()) @map("_id") @db.ObjectId
plan String
referenceId String @unique
stripeCustomerId String?
stripeSubscriptionId String?
status String @default("incomplete")
periodStart DateTime?
periodEnd DateTime?
cancelAtPeriodEnd Boolean @default(false)
seats Int?
trialStart DateTime?
trialEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("subscription")
}

152
prisma/seed.ts Normal file
View File

@@ -0,0 +1,152 @@
import { PrismaClient } from "@prisma/client";
import { doctors } from "../lib/types/doctor";
import { allServices } from "../lib/types/services-data";
const prisma = new PrismaClient();
// Enhanced dentist data with full profiles
const dentistProfiles = [
{
...doctors[0],
email: "kath.estrada@dentalucare.com",
phone: "+63 912 345 6789",
qualifications: "Doctor of Dental Medicine (DMD), Orthodontics Specialist",
experience: 8,
},
{
...doctors[1],
email: "clyrelle.cervantes@dentalucare.com",
phone: "+63 912 345 6790",
qualifications:
"Doctor of Dental Surgery (DDS), Cosmetic Dentistry Specialist",
experience: 6,
},
{
...doctors[2],
email: "von.arguelles@dentalucare.com",
phone: "+63 912 345 6791",
qualifications: "Doctor of Dental Medicine (DMD), Oral Surgery Specialist",
experience: 10,
},
{
...doctors[3],
email: "dexter.cabanag@dentalucare.com",
phone: "+63 912 345 6792",
qualifications: "Doctor of Dental Surgery (DDS), Periodontics Specialist",
experience: 7,
},
];
async function main() {
console.log("🌱 Starting database seeding...");
// Clear existing data (optional - comment out if you want to keep existing data)
console.log("🗑️ Cleaning existing data...");
await prisma.service.deleteMany({});
await prisma.user.deleteMany({
where: {
role: "dentist",
},
});
// Seed Dentists
console.log("👨‍⚕️ Seeding dentists...");
const createdDentists = [];
const password = "dentalucaredentist@123"; // Plain text password (better-auth will hash it)
for (const doctor of dentistProfiles) {
const dentist = await prisma.user.create({
data: {
id: doctor.id,
name: doctor.name,
email: doctor.email,
emailVerified: true,
role: "dentist",
image: doctor.avatar,
specialization: doctor.role,
isAvailable: true,
phone: doctor.phone,
qualifications: doctor.qualifications,
experience: doctor.experience,
workingHours: {
monday: { start: "09:00", end: "17:00" },
tuesday: { start: "09:00", end: "17:00" },
wednesday: { start: "09:00", end: "17:00" },
thursday: { start: "09:00", end: "17:00" },
friday: { start: "09:00", end: "17:00" },
saturday: { start: "09:00", end: "13:00" },
sunday: { closed: true },
},
},
});
// Create account with password for the dentist (better-auth uses "credential" as providerId)
await prisma.account.create({
data: {
id: `${dentist.id}:credential`,
userId: dentist.id,
accountId: `${dentist.id}:credential`,
providerId: "credential",
password: password,
},
});
createdDentists.push(dentist);
console.log(
` ✓ Created dentist: ${dentist.name} (${dentist.specialization}) - email: ${doctor.email}`
);
console.log(` Password: dentalucaredentist@123`);
}
// Seed Services
console.log("🦷 Seeding services...");
const createdServices = [];
for (const service of allServices) {
const createdService = await prisma.service.create({
data: {
id: service.id, // Use the ID from the service data
name: service.name,
description: service.description || service.name,
duration: service.duration,
price: service.price, // Keep price as string (e.g., "₱1,500 ₱3,000")
category: service.category,
isActive: service.isActive,
},
});
createdServices.push(createdService);
console.log(
` ✓ Created service: ${createdService.name} (${createdService.price})`
);
}
console.log("\n✅ Seeding completed successfully!");
console.log(`📊 Summary:`);
console.log(` - Dentists created: ${createdDentists.length}`);
console.log(` - Services created: ${createdServices.length}`);
console.log("\n👨 Dentists:");
createdDentists.forEach((dentist) => {
console.log(` - ${dentist.name} (${dentist.specialization})`);
});
console.log("\n🦷 Service Categories:");
const categoryCounts = createdServices.reduce(
(acc: Record<string, number>, service) => {
acc[service.category] = (acc[service.category] || 0) + 1;
return acc;
},
{}
);
Object.entries(categoryCounts).forEach(([category, count]) => {
console.log(` - ${category}: ${count} services`);
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error("❌ Error during seeding:", e);
await prisma.$disconnect();
process.exit(1);
});

8
prisma/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true
}
}