diff --git a/backend/.env b/backend/.env index fecd0d8..5921644 100644 --- a/backend/.env +++ b/backend/.env @@ -1,3 +1 @@ -//.env file for the backend service - DATABASE_URL="postgresql://postgres:password@localhost:5432/time_booking?schema=public" diff --git a/backend/package.json b/backend/package.json index 8cd7a59..003bc19 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,9 @@ "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", "seed": "ts-node prisma/seed.ts", + "seed:dev": "NODE_ENV=development ts-node prisma/seed.ts", + "seed:test": "NODE_ENV=test ts-node prisma/seed.ts", + "db:reset": "prisma migrate reset --force && npm run seed", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "format": "prettier --write \"src/**/*.ts\"", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f8cc80e..4d97b78 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -7,56 +7,22 @@ datasource db { url = env("DATABASE_URL") } -enum UserRole { - USER - ADMIN - SUPER_ADMIN -} - -enum BookingStatus { - PENDING - CONFIRMED - CANCELLED - COMPLETED -} - -enum WaitlistStatus { - ACTIVE - FULFILLED - CANCELLED -} - -enum LabStatus { - ACTIVE - MAINTENANCE - INACTIVE -} - -enum NotificationType { - BOOKING_CONFIRMATION - BOOKING_CANCELLATION - WAITLIST_NOTIFICATION - GENERAL_ANNOUNCEMENT - SLOT_AVAILABLE - SYSTEM_NOTIFICATION -} - model User { id String @id @default(uuid()) user_name String user_email String @unique user_password String user_role UserRole @default(USER) - validation_key String? @unique // SAMAGRA ID for alternative authentication + validation_key String? @unique resetToken String? @unique resetTokenExpiry DateTime? - organizationId String? // Optional as users can book from multiple organizations - organization Organization? @relation(fields: [organizationId], references: [id]) - bookings Booking[] - waitlists Waitlist[] - notifications Notification[] + organizationId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + bookings Booking[] + notifications Notification[] + organization Organization? @relation(fields: [organizationId], references: [id]) + waitlists Waitlist[] @@index([user_email]) @@index([validation_key]) @@ -70,13 +36,13 @@ model Admin { admin_email String @unique admin_password String organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - labs Lab[] // Labs managed by this admin createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id]) + labs Lab[] @@index([admin_email]) - @@index([organizationId]) // Added index for foreign key + @@index([organizationId]) } model SuperAdmin { @@ -84,11 +50,11 @@ model SuperAdmin { super_admin_name String super_admin_email String @unique super_admin_password String - validation_key String? @unique // For system authentication if needed - organizations Organization[] - managedBookings Booking[] // Direct relation to bookings managed by this SuperAdmin + validation_key String? @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + managedBookings Booking[] + organizations Organization[] @@index([super_admin_email]) } @@ -98,30 +64,29 @@ model Organization { org_name String org_type String org_location String - users User[] - admins Admin[] - labs Lab[] - notifications OrganizationNotification[] superAdminId String? - superAdmin SuperAdmin? @relation(fields: [superAdminId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + admins Admin[] + labs Lab[] + superAdmin SuperAdmin? @relation(fields: [superAdminId], references: [id]) + notifications OrganizationNotification[] + users User[] @@index([org_name]) @@index([superAdminId]) } -// Added to match ERD relationship: Organization can receive Notification model OrganizationNotification { id String @id @default(uuid()) organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) notification_type NotificationType notification_message String notification_timestamp DateTime @default(now()) read Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id]) @@index([organizationId]) } @@ -133,49 +98,48 @@ model Lab { status LabStatus @default(ACTIVE) location String? description String? - timeSlots TimeSlot[] organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) adminId String - admin Admin @relation(fields: [adminId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + admin Admin @relation(fields: [adminId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id]) + timeSlots TimeSlot[] @@index([organizationId]) - @@index([adminId]) // Added index for foreign key + @@index([adminId]) } model TimeSlot { id String @id @default(uuid()) lab_id String - lab Lab @relation(fields: [lab_id], references: [id]) date DateTime start_time DateTime end_time DateTime status String @default("AVAILABLE") - bookings Booking[] - waitlists Waitlist[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + bookings Booking[] + lab Lab @relation(fields: [lab_id], references: [id]) + waitlists Waitlist[] @@index([lab_id]) @@index([date]) - @@index([lab_id, date]) // Added composite index for common queries + @@index([lab_id, date]) } model Booking { id String @id @default(uuid()) user_id String - user User @relation(fields: [user_id], references: [id]) slot_id String - timeSlot TimeSlot @relation(fields: [slot_id], references: [id]) booking_status BookingStatus @default(PENDING) booking_timestamp DateTime @default(now()) - // Direct relation to SuperAdmin who manages this booking managedBy String? - superAdmin SuperAdmin? @relation(fields: [managedBy], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + superAdmin SuperAdmin? @relation(fields: [managedBy], references: [id]) + timeSlot TimeSlot @relation(fields: [slot_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([user_id]) @@index([slot_id]) @@ -186,31 +150,65 @@ model Booking { model Waitlist { id String @id @default(uuid()) user_id String - user User @relation(fields: [user_id], references: [id]) slot_id String - timeSlot TimeSlot @relation(fields: [slot_id], references: [id]) waitlist_position Int waitlist_status WaitlistStatus @default(ACTIVE) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + timeSlot TimeSlot @relation(fields: [slot_id], references: [id]) + user User @relation(fields: [user_id], references: [id]) @@index([user_id]) @@index([slot_id]) - @@index([waitlist_status]) // Added index for status queries - @@index([slot_id, waitlist_position]) // Added composite index for position queries + @@index([waitlist_status]) + @@index([slot_id, waitlist_position]) } model Notification { id String @id @default(uuid()) user_id String - user User @relation(fields: [user_id], references: [id]) notification_type NotificationType notification_message String notification_timestamp DateTime @default(now()) read Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + user User @relation(fields: [user_id], references: [id]) @@index([user_id]) - @@index([read]) // Added index for unread notification queries + @@index([read]) +} + +enum UserRole { + USER + ADMIN + SUPER_ADMIN +} + +enum BookingStatus { + PENDING + CONFIRMED + CANCELLED + COMPLETED +} + +enum WaitlistStatus { + ACTIVE + FULFILLED + CANCELLED +} + +enum LabStatus { + ACTIVE + MAINTENANCE + INACTIVE +} + +enum NotificationType { + BOOKING_CONFIRMATION + BOOKING_CANCELLATION + WAITLIST_NOTIFICATION + GENERAL_ANNOUNCEMENT + SLOT_AVAILABLE + SYSTEM_NOTIFICATION } diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..d4280ee --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,124 @@ +import { PrismaClient } from '@prisma/client'; +import { seedUsers } from './seed/seed-users'; +import { seedOrganizations } from './seed/seed-orgs'; +import { seedLabs } from './seed/seed-labs'; +import { seedTimeSlots } from './seed/seed-timeslots'; +import { seedBookings } from './seed/seed-bookings'; +import { seedWaitlists } from './seed/seed-waitlists'; +import { seedNotifications } from './seed/seed-notifications'; +import { seedOrganizationNotifications } from './seed/seed-org-notifications'; +import { seedAdminOrgLinks } from './seed/seed-admin-org-links'; +import { logSeedOperation, verifyDataExists } from './seed/seed-utils'; + +async function main() { + console.log(`\n🌱 Starting database seeding for ${process.env.NODE_ENV || 'development'} environment...\n`); + + const prisma = new PrismaClient(); + const startTime = Date.now(); + + try { + // Seed in order of dependencies + await logSeedOperation('Users', () => seedUsers(prisma)); + await verifyDataExists(prisma, 'user', 'Failed to seed users'); + + await logSeedOperation('Organizations', () => seedOrganizations(prisma)); + await verifyDataExists(prisma, 'organization', 'Failed to seed organizations'); + + await logSeedOperation('Admin-Organization Links', () => seedAdminOrgLinks(prisma)); + await verifyDataExists(prisma, 'admin', 'Failed to seed admin-organization links'); + + await logSeedOperation('Labs', () => seedLabs(prisma)); + await verifyDataExists(prisma, 'lab', 'Failed to seed labs'); + + await logSeedOperation('Time Slots', () => seedTimeSlots(prisma)); + await verifyDataExists(prisma, 'timeSlot', 'Failed to seed time slots'); + + await logSeedOperation('Bookings', () => seedBookings(prisma)); + // Bookings may be empty in test environment, so no verification + + await logSeedOperation('Waitlists', () => seedWaitlists(prisma)); + // Waitlists may be empty, so no verification + + await logSeedOperation('User Notifications', () => seedNotifications(prisma)); + // Notifications may be empty, so no verification + + await logSeedOperation('Organization Notifications', () => seedOrganizationNotifications(prisma)); + // Org notifications may be empty, so no verification + + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`\nāœ… Database seeding completed successfully in ${duration}s\n`); + + // Print verification summary + await verifySeedData(prisma); + } catch (error) { + console.error('\nāŒ Database seeding failed:', error); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +async function verifySeedData(prisma: PrismaClient) { + console.log('\nšŸ“Š Seed Data Verification Summary:'); + + // Get counts of all entity types + const userCount = await prisma.user.count(); + const adminCount = await prisma.admin.count(); + const organizationCount = await prisma.organization.count(); + const labCount = await prisma.lab.count(); + const timeSlotCount = await prisma.timeSlot.count(); + const bookingCount = await prisma.booking.count(); + const waitlistCount = await prisma.waitlist.count(); + const notificationCount = await prisma.notification.count(); + const orgNotificationCount = await prisma.organizationNotification.count(); + + console.log(`Users: ${userCount}`); + console.log(`Admins: ${adminCount}`); + console.log(`Organizations: ${organizationCount}`); + console.log(`Labs: ${labCount}`); + console.log(`Time Slots: ${timeSlotCount}`); + console.log(`Bookings: ${bookingCount}`); + console.log(`Waitlists: ${waitlistCount}`); + console.log(`User Notifications: ${notificationCount}`); + console.log(`Organization Notifications: ${orgNotificationCount}`); + + // Verify key relationships + console.log('\nšŸ” Relationship Verification:'); + + // Check if admins are properly linked to organizations + const adminsWithoutOrgs = await prisma.admin.count({ + where: { + organization: { + isNot: {} + } + } + }); + + if (adminsWithoutOrgs > 0) { + console.warn(`āš ļø Found ${adminsWithoutOrgs} admins not linked to any organization`); + } else { + console.log('āœ… All admins are properly linked to organizations'); + } + + // Check if labs are properly linked to organizations and admins + const labsWithoutOrgs = await prisma.lab.count({ + where: { + organization: { + isNot: {} + } + } + }); + + if (labsWithoutOrgs > 0) { + console.warn(`āš ļø Found ${labsWithoutOrgs} labs not linked to any organization`); + } else { + console.log('āœ… All labs are properly linked to organizations'); + } + + console.log('\nšŸ“ Manual Setup Note:'); + console.log('- No additional manual steps required.'); + console.log('- User credentials are available in seed-users.ts'); + console.log('- Super admin login: superadmin@mpgovt.in / SuperAdmin123!'); +} + +main(); \ No newline at end of file diff --git a/backend/prisma/seed/index.ts b/backend/prisma/seed/index.ts new file mode 100644 index 0000000..bb74dec --- /dev/null +++ b/backend/prisma/seed/index.ts @@ -0,0 +1,42 @@ +import { PrismaClient } from '@prisma/client'; +import { seedUsers } from './seed-users'; +import { seedOrganizations } from './seed-orgs'; +import { seedLabs } from './seed-labs'; +import { seedTimeSlots } from './seed-timeslots'; +import { seedBookings } from './seed-bookings'; +import { seedWaitlists } from './seed-waitlists'; +import { seedNotifications } from './seed-notifications'; +import { seedOrganizationNotifications } from './seed-org-notifications'; +import { seedAdminOrgLinks } from './seed-admin-org-links'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Starting database seeding...'); + + try { + // Seed in the correct order to respect relationships + await seedUsers(prisma); // Create all users/admins/super admins + await seedOrganizations(prisma); // Create orgs linked to super admin + await seedAdminOrgLinks(prisma); // New function to create Admin entries with proper org links + await seedLabs(prisma); + await seedTimeSlots(prisma); + await seedBookings(prisma); + await seedWaitlists(prisma); + await seedNotifications(prisma); + await seedOrganizationNotifications(prisma); + + console.log('Database seeding completed successfully!'); + } catch (error) { + console.error('Error seeding the database:', error); + throw error; + } finally { + await prisma.$disconnect(); + } +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }); \ No newline at end of file diff --git a/backend/prisma/seed/seed-README.md b/backend/prisma/seed/seed-README.md new file mode 100644 index 0000000..13af25b --- /dev/null +++ b/backend/prisma/seed/seed-README.md @@ -0,0 +1,98 @@ +# Seed Data Documentation + +This document provides information about the seed data structure, relationships, and usage for the Time-Booking Application. + +## Overview + +The seeding system populates the database with realistic test data for development and testing purposes. The seed data is environment-aware and will create different amounts of data based on the `NODE_ENV` environment variable. + +## Running Seed Scripts + +```bash +# Seed with development data (default) +npm run seed + +# Seed with development data explicitly +npm run seed:dev + +# Seed with minimal test data +npm run seed:test + +# Reset database and re-seed +npm run db:reset +``` + +## Seed Data Structure + +The seed data follows these entity relationships: + +1. **Users** (Regular Users, Admins, SuperAdmin) + - Regular users can make bookings and be on waitlists + - Admin users are associated with organizations + - One SuperAdmin user manages the entire system + +2. **Organizations** + - Each organization is associated with the SuperAdmin + - Each admin is associated with one organization + +3. **Labs** + - Each lab belongs to an organization + - Each lab is managed by an admin + +4. **TimeSlots** + - Each time slot belongs to a lab + - Time slots span the next 14 days (or 7 days in test environment) + - Each day has 4 time slots (9-11, 11-13, 14-16, 16-18) + +5. **Bookings** + - Each booking is made by a user + - Each booking is for a specific time slot + - Bookings have various statuses (PENDING, CONFIRMED, CANCELLED, COMPLETED) + +6. **Waitlists** + - Users can be on waitlists for fully booked time slots + - Waitlists have statuses (ACTIVE, FULFILLED, CANCELLED) + +7. **Notifications** + - User notifications for various events + - Organization notifications for system-wide announcements + +## Default Credentials + +### SuperAdmin +- Email: superadmin@mpgovt.in +- Password: SuperAdmin123! + +### Admin Users +- Email: rajesh.admin@example.com +- Password: AdminPass123! + +- Email: sunita.admin@example.com +- Password: AdminPass123! + +- Email: mohan.admin@example.com +- Password: AdminPass123! + +### Regular Users +- Pattern: [name].user@example.com +- Password: Password123! + +## Configuration + +Seed data quantities are configured in `prisma/seed/seed-config.ts` and vary by environment: + +- **Development**: Comprehensive data set for full application testing +- **Test**: Minimal data set for fast unit tests +- **Production**: No seed data (empty configuration) + +## Customizing Seed Data + +To customize the seed data: + +1. Modify the configuration in `seed-config.ts` +2. Edit the individual seed files in the `prisma/seed/` directory +3. Run the appropriate seed command + +## Manual Setup Steps + +No additional manual setup is required after running the seed scripts. \ No newline at end of file diff --git a/backend/prisma/seed/seed-admin-org-links.ts b/backend/prisma/seed/seed-admin-org-links.ts new file mode 100644 index 0000000..21c4677 --- /dev/null +++ b/backend/prisma/seed/seed-admin-org-links.ts @@ -0,0 +1,70 @@ +import { PrismaClient, UserRole } from '@prisma/client'; + +/** + * Seeds the Admin table by creating Admin entries that link + * users with ADMIN role to organizations + */ +export async function seedAdminOrgLinks(prisma: PrismaClient) { + console.log('Creating Admin-Organization links...'); + + // Get admin users + const adminUsers = await prisma.user.findMany({ + where: { user_role: UserRole.ADMIN } + }); + + if (adminUsers.length === 0) { + console.warn('No admin users found. Skipping Admin-Organization linking.'); + return; + } + + // Get organizations + const organizations = await prisma.organization.findMany(); + + if (organizations.length === 0) { + throw new Error('No organizations found. Please seed organizations first.'); + } + + console.log(`Found ${adminUsers.length} admin users and ${organizations.length} organizations`); + + // Create a mapping between admin users and organizations + // This maps each admin to an organization in round-robin fashion + const adminOrgPairs = adminUsers.map((admin, index) => { + return { + admin, + organization: organizations[index % organizations.length] + }; + }); + + let createdCount = 0; + let skippedCount = 0; + + // Create Admin entries for each admin user linked to an organization + for (const { admin, organization } of adminOrgPairs) { + // Check if this admin already exists in the Admin table + const existingAdmin = await prisma.admin.findUnique({ + where: { admin_email: admin.user_email } + }); + + if (existingAdmin) { + console.log(`Admin with email ${admin.user_email} already exists. Skipping.`); + skippedCount++; + continue; + } + + try { + await prisma.admin.create({ + data: { + admin_name: admin.user_name, + admin_email: admin.user_email, + admin_password: admin.user_password, // Password already hashed in seed-users.ts + organizationId: organization.id + } + }); + createdCount++; + } catch (error) { + console.error(`Failed to create Admin entry for ${admin.user_name}:`, error); + } + } + + console.log(`Admin-Organization links created: ${createdCount}, skipped: ${skippedCount}`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-bookings.ts b/backend/prisma/seed/seed-bookings.ts new file mode 100644 index 0000000..66ede58 --- /dev/null +++ b/backend/prisma/seed/seed-bookings.ts @@ -0,0 +1,60 @@ +import { PrismaClient, BookingStatus } from '@prisma/client'; +import { getSeedConfig } from './seed-config'; + +export async function seedBookings(prisma: PrismaClient) { + console.log('Seeding bookings...'); + const config = getSeedConfig(); + + // Get users for bookings + const users = await prisma.user.findMany({ + where: { user_role: 'USER' } + }); + + if (users.length === 0) { + throw new Error('No users found. Please seed users first.'); + } + + // Get available time slots + const timeSlots = await prisma.timeSlot.findMany({ + orderBy: { start_time: 'asc' } + }); + + if (timeSlots.length === 0) { + throw new Error('No time slots found. Please seed time slots first.'); + } + + // Create bookings with different statuses + const bookingStatuses = [ + BookingStatus.CONFIRMED, + BookingStatus.PENDING, + BookingStatus.COMPLETED, + BookingStatus.CANCELLED + ]; + + let bookingsCreated = 0; + + // For each user, create the configured number of bookings + for (const user of users) { + for (let i = 0; i < config.bookingsPerUser; i++) { + // Select a time slot for this booking (ensure we don't reuse slots too much) + const slotIndex = (users.indexOf(user) * config.bookingsPerUser + i) % timeSlots.length; + const timeSlot = timeSlots[slotIndex]; + + // Select a status for this booking + const status = bookingStatuses[i % bookingStatuses.length]; + + await prisma.booking.create({ + data: { + user_id: user.id, + slot_id: timeSlot.id, + booking_status: status, + booking_timestamp: new Date() + } + }); + + bookingsCreated++; + } + } + + console.log(`Bookings seeded successfully (${bookingsCreated} bookings created)`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-config.ts b/backend/prisma/seed/seed-config.ts new file mode 100644 index 0000000..d07c225 --- /dev/null +++ b/backend/prisma/seed/seed-config.ts @@ -0,0 +1,73 @@ +/** + * Configuration for seed data quantities based on environment + */ + +export type SeedConfig = { + users: { + regular: number; + admins: number; + }; + organizations: number; + labsPerOrg: number; + timeSlotsPerLab: number; + bookingsPerUser: number; + waitlistsPerUser: number; + notificationsPerUser: number; + orgNotificationsPerOrg: number; +}; + +const developmentConfig: SeedConfig = { + users: { + regular: 5, + admins: 3 + }, + organizations: 3, + labsPerOrg: 2, + timeSlotsPerLab: 8, // 2 days x 4 slots per day + bookingsPerUser: 2, + waitlistsPerUser: 1, + notificationsPerUser: 3, + orgNotificationsPerOrg: 2 +}; + +const testConfig: SeedConfig = { + users: { + regular: 2, + admins: 1 + }, + organizations: 1, + labsPerOrg: 1, + timeSlotsPerLab: 4, // 1 day x 4 slots + bookingsPerUser: 1, + waitlistsPerUser: 1, + notificationsPerUser: 1, + orgNotificationsPerOrg: 1 +}; + +const productionConfig: SeedConfig = { + users: { + regular: 0, + admins: 0 + }, + organizations: 0, + labsPerOrg: 0, + timeSlotsPerLab: 0, + bookingsPerUser: 0, + waitlistsPerUser: 0, + notificationsPerUser: 0, + orgNotificationsPerOrg: 0 +}; + +export function getSeedConfig(): SeedConfig { + const env = process.env.NODE_ENV || 'development'; + + switch (env) { + case 'test': + return testConfig; + case 'production': + return productionConfig; + case 'development': + default: + return developmentConfig; + } +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-labs.ts b/backend/prisma/seed/seed-labs.ts new file mode 100644 index 0000000..8159508 --- /dev/null +++ b/backend/prisma/seed/seed-labs.ts @@ -0,0 +1,129 @@ +import { PrismaClient, LabStatus } from '@prisma/client'; +import { getSeedConfig } from './seed-config'; + +const labData = [ + { + lab_name: "Programming Lab Alpha", + lab_capacity: 25, + status: LabStatus.ACTIVE, + location: "Block A, 2nd Floor", + description: "Advanced programming lab with high-end systems", + organizationName: "Bhopal Technical Institute", + adminEmail: "rajesh.admin@example.com" + }, + { + lab_name: "Web Development Lab", + lab_capacity: 20, + status: LabStatus.ACTIVE, + location: "Block B, 1st Floor", + description: "Specialized for web and mobile development", + organizationName: "Bhopal Technical Institute", + adminEmail: "rajesh.admin@example.com" + }, + { + lab_name: "Data Science Lab", + lab_capacity: 15, + status: LabStatus.ACTIVE, + location: "Building 3, Room 105", + description: "Equipped with data analysis and machine learning tools", + organizationName: "Indore Engineering College", + adminEmail: "sunita.admin@example.com" + }, + { + lab_name: "IoT Lab", + lab_capacity: 18, + status: LabStatus.MAINTENANCE, + location: "Block C, Ground Floor", + description: "Lab for Internet of Things projects and experiments", + organizationName: "Gwalior Technical University", + adminEmail: "mohan.admin@example.com" + }, + { + lab_name: "Cybersecurity Lab", + lab_capacity: 12, + status: LabStatus.ACTIVE, + location: "Secure Wing, 3rd Floor", + description: "High-security lab for cybersecurity training", + organizationName: "Indore Engineering College", + adminEmail: "sunita.admin@example.com" + } +]; + +export async function seedLabs(prisma: PrismaClient) { + console.log('Seeding labs...'); + const config = getSeedConfig(); + + // Get organizations + const organizations = await prisma.organization.findMany({ + take: config.organizations + }); + + if (organizations.length === 0) { + throw new Error('No organizations found. Please seed organizations first.'); + } + + // Get admins + const admins = await prisma.admin.findMany(); + + if (admins.length === 0) { + throw new Error('No admins found. Please seed admins first.'); + } + + let labsCreated = 0; + + // For each organization, create the configured number of labs + for (const org of organizations) { + // Find an admin for this organization + const orgAdmins = admins.filter(admin => admin.organizationId === org.id); + + if (orgAdmins.length === 0) { + console.warn(`No admins found for organization ${org.org_name}, skipping labs`); + continue; + } + + // Create labs for this organization + for (let i = 0; i < config.labsPerOrg && i < labData.length; i++) { + const labTemplate = labData[i % labData.length]; // Cycle through available templates + const labName = `${labTemplate.lab_name} - ${org.org_name}`; + + // Check if lab already exists + const existingLab = await prisma.lab.findFirst({ + where: { lab_name: labName } + }); + + // Select an admin for this lab (round-robin through available org admins) + const admin = orgAdmins[i % orgAdmins.length]; + + if (existingLab) { + // Update existing lab + await prisma.lab.update({ + where: { id: existingLab.id }, + data: { + lab_capacity: labTemplate.lab_capacity, + status: labTemplate.status, + location: labTemplate.location, + description: labTemplate.description, + organizationId: org.id, + adminId: admin.id + } + }); + } else { + // Create new lab + await prisma.lab.create({ + data: { + lab_name: labName, + lab_capacity: labTemplate.lab_capacity, + status: labTemplate.status, + location: labTemplate.location, + description: labTemplate.description, + organizationId: org.id, + adminId: admin.id + } + }); + labsCreated++; + } + } + } + + console.log(`Labs seeded successfully (${labsCreated} labs created)`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-notifications.ts b/backend/prisma/seed/seed-notifications.ts new file mode 100644 index 0000000..9aea408 --- /dev/null +++ b/backend/prisma/seed/seed-notifications.ts @@ -0,0 +1,75 @@ +import { PrismaClient, NotificationType } from '@prisma/client'; +import { getSeedConfig } from './seed-config'; + +export async function seedNotifications(prisma: PrismaClient) { + console.log('Seeding notifications...'); + const config = getSeedConfig(); + + // Get users for notifications + const users = await prisma.user.findMany({ + where: { user_role: 'USER' } + }); + + if (users.length === 0) { + throw new Error('No users found. Please seed users first.'); + } + + // Sample notification templates + const notificationTemplates = [ + { + type: NotificationType.BOOKING_CONFIRMATION, + message: "Your booking for {lab} has been confirmed." + }, + { + type: NotificationType.WAITLIST_NOTIFICATION, + message: "A slot has opened up in {lab}." + }, + { + type: NotificationType.GENERAL_ANNOUNCEMENT, + message: "The lab will be closed for maintenance on Sunday." + }, + { + type: NotificationType.BOOKING_CANCELLATION, + message: "Your booking for {lab} has been cancelled." + }, + { + type: NotificationType.SLOT_AVAILABLE, + message: "New slots are available in {lab}." + } + ]; + + // Get labs for notification messages + const labs = await prisma.lab.findMany(); + + let notificationsCreated = 0; + + // For each user, create the configured number of notifications + for (const user of users) { + for (let i = 0; i < config.notificationsPerUser; i++) { + // Select a notification template + const templateIndex = i % notificationTemplates.length; + const template = notificationTemplates[templateIndex]; + + // Select a lab to mention + const labIndex = (users.indexOf(user) + i) % Math.max(1, labs.length); + const labName = labs.length > 0 ? labs[labIndex].lab_name : "Programming Lab"; + + // Create the notification message with the lab name + const message = template.message.replace("{lab}", labName); + + await prisma.notification.create({ + data: { + user_id: user.id, + notification_type: template.type, + notification_message: message, + read: Math.random() > 0.7, // 30% chance of being read + createdAt: new Date() + } + }); + + notificationsCreated++; + } + } + + console.log(`Notifications seeded successfully (${notificationsCreated} notifications created)`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-org-notifications.ts b/backend/prisma/seed/seed-org-notifications.ts new file mode 100644 index 0000000..ed8076c --- /dev/null +++ b/backend/prisma/seed/seed-org-notifications.ts @@ -0,0 +1,68 @@ +import { PrismaClient, NotificationType } from '@prisma/client'; +import { getSeedConfig } from './seed-config'; + +export async function seedOrganizationNotifications(prisma: PrismaClient) { + console.log('Seeding organization notifications...'); + + // Get organizations + const organizations = await prisma.organization.findMany({ + take: getSeedConfig().organizations + }); + + if (organizations.length === 0) { + console.warn('No organizations found. Skipping organization notifications seeding.'); + return; + } + + // Sample organization notification data + const notificationTypes = [ + NotificationType.GENERAL_ANNOUNCEMENT, + NotificationType.SYSTEM_NOTIFICATION + ]; + + const notificationContent = [ + { + title: "System Maintenance", + content: "System will be under maintenance this weekend. Please plan accordingly." + }, + { + title: "New Lab Equipment", + content: "New equipment has been installed in all labs. Training session scheduled next week." + }, + { + title: "Holiday Schedule", + content: "All labs will be closed during the upcoming holidays. See schedule for details." + }, + { + title: "Policy Update", + content: "Our booking policy has been updated. Please review the changes." + } + ]; + + let notificationsCreated = 0; + + // Create notifications for each organization + for (const org of organizations) { + const notificationsPerOrg = getSeedConfig().orgNotificationsPerOrg; + + for (let i = 0; i < notificationsPerOrg; i++) { + const notificationIndex = i % notificationContent.length; + const notificationType = notificationTypes[i % notificationTypes.length]; + + await prisma.organizationNotification.create({ + data: { + notification_message: `${notificationContent[notificationIndex].title}: ${notificationContent[notificationIndex].content}`, + notification_type: notificationType, + read: Math.random() > 0.5, // Randomly mark as read + organization: { + connect: { id: org.id } + } + } + }); + + notificationsCreated++; + } + } + + console.log(`Created ${notificationsCreated} organization notifications`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-orgs.ts b/backend/prisma/seed/seed-orgs.ts new file mode 100644 index 0000000..24aae07 --- /dev/null +++ b/backend/prisma/seed/seed-orgs.ts @@ -0,0 +1,96 @@ +import { PrismaClient } from '@prisma/client'; +import { getSeedConfig } from './seed-config'; + +const organizationData = [ + { + org_name: "Bhopal Technical Institute", + org_type: "Government", + org_location: "Bhopal, MP" + }, + { + org_name: "Indore Engineering College", + org_type: "Private", + org_location: "Indore, MP" + }, + { + org_name: "Gwalior Technical University", + org_type: "Semi-Government", + org_location: "Gwalior, MP" + } +]; + +export async function seedOrganizations(prisma: PrismaClient) { + console.log('Seeding organizations...'); + const config = getSeedConfig(); + + // First, find the User with SUPER_ADMIN role + const superAdminUser = await prisma.user.findFirst({ + where: { user_role: 'SUPER_ADMIN' } + }); + + if (!superAdminUser) { + throw new Error('Super admin user not found. Please seed users first.'); + } + + console.log(`Found Super Admin user: ${superAdminUser.user_name} (${superAdminUser.user_email})`); + + // Next, find or create a corresponding SuperAdmin record + let superAdmin = await prisma.superAdmin.findFirst({ + where: { super_admin_email: superAdminUser.user_email } + }); + + if (!superAdmin) { + console.log('Creating new SuperAdmin record from the User with SUPER_ADMIN role...'); + superAdmin = await prisma.superAdmin.create({ + data: { + super_admin_name: superAdminUser.user_name, + super_admin_email: superAdminUser.user_email, + super_admin_password: superAdminUser.user_password, + validation_key: superAdminUser.validation_key + } + }); + } + + console.log(`Using SuperAdmin ID: ${superAdmin.id} for organizations`); + + // Now seed organizations with the proper SuperAdmin ID + const orgsToCreate = Math.min(organizationData.length, config.organizations); + + let orgsCreated = 0; + + for (let i = 0; i < orgsToCreate; i++) { + const org = organizationData[i]; + + // Check if organization already exists + const existingOrg = await prisma.organization.findFirst({ + where: { org_name: org.org_name } + }); + + if (existingOrg) { + // Update existing organization with SuperAdmin link + await prisma.organization.update({ + where: { id: existingOrg.id }, + data: { + org_type: org.org_type, + org_location: org.org_location, + superAdminId: superAdmin.id + } + }); + console.log(`Updated existing organization: ${org.org_name}`); + } else { + // Create new organization with SuperAdmin link + await prisma.organization.create({ + data: { + org_name: org.org_name, + org_type: org.org_type, + org_location: org.org_location, + superAdminId: superAdmin.id + } + }); + orgsCreated++; + console.log(`Created new organization: ${org.org_name}`); + } + } + + console.log(`Organizations seeded successfully (${orgsCreated} new, ${orgsToCreate - orgsCreated} updated)`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-timeslots.ts b/backend/prisma/seed/seed-timeslots.ts new file mode 100644 index 0000000..d346f00 --- /dev/null +++ b/backend/prisma/seed/seed-timeslots.ts @@ -0,0 +1,71 @@ +import { PrismaClient } from '@prisma/client'; +import { addDays, setHours, setMinutes } from 'date-fns'; +import { getSeedConfig } from './seed-config'; + +export async function seedTimeSlots(prisma: PrismaClient) { + console.log('Seeding time slots...'); + const config = getSeedConfig(); + + // Get all labs + const labs = await prisma.lab.findMany({ + where: { status: 'ACTIVE' } + }); + + if (labs.length === 0) { + throw new Error('No active labs found. Please seed labs first.'); + } + + // Create time slots - days will vary based on environment + const today = new Date(); + const daysToSeed = config.timeSlotsPerLab / 4; // 4 slots per day + + // Define time slot configurations (start hour, end hour) + const timeSlots = [ + { startHour: 9, endHour: 11 }, + { startHour: 11, endHour: 13 }, + { startHour: 14, endHour: 16 }, + { startHour: 16, endHour: 18 } + ]; + + let slotsCreated = 0; + + for (const lab of labs) { + for (let i = 1; i <= daysToSeed; i++) { + const currentDate = addDays(today, i); + + // Skip weekends (Saturday and Sunday) if not in test mode + if ((currentDate.getDay() === 0 || currentDate.getDay() === 6) && config.timeSlotsPerLab > 4) { + continue; + } + + for (const slot of timeSlots) { + // In test mode, create all slots; otherwise randomly skip some (20% chance) + if (config.timeSlotsPerLab > 4 && Math.random() < 0.2) { + continue; + } + + const startTime = new Date(currentDate); + setHours(startTime, slot.startHour); + setMinutes(startTime, 0); + + const endTime = new Date(currentDate); + setHours(endTime, slot.endHour); + setMinutes(endTime, 0); + + await prisma.timeSlot.create({ + data: { + lab_id: lab.id, + start_time: startTime, + end_time: endTime, + date: currentDate, + status: 'AVAILABLE' + } + }); + + slotsCreated++; + } + } + } + + console.log(`Time slots seeded successfully (${slotsCreated} slots created)`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-users.ts b/backend/prisma/seed/seed-users.ts new file mode 100644 index 0000000..6921bc6 --- /dev/null +++ b/backend/prisma/seed/seed-users.ts @@ -0,0 +1,129 @@ +import { PrismaClient, UserRole } from '@prisma/client'; +import bcrypt from 'bcrypt'; +import { getSeedConfig } from './seed-config'; + +const userData = [ + { + user_name: "Rahul Sharma", + user_email: "rahul.sharma@example.com", + user_password: "Password123!", // Will be hashed + user_role: UserRole.USER + }, + { + user_name: "Priya Patel", + user_email: "priya.patel@example.com", + user_password: "Password123!", + user_role: UserRole.USER + }, + { + user_name: "Amit Kumar", + user_email: "amit.kumar@example.com", + user_password: "Password123!", + user_role: UserRole.USER + }, + { + user_name: "Neha Singh", + user_email: "neha.singh@example.com", + user_password: "Password123!", + user_role: UserRole.USER + }, + { + user_name: "Vijay Verma", + user_email: "vijay.verma@example.com", + user_password: "Password123!", + user_role: UserRole.USER + } +]; + +// Admin account data +const adminData = [ + { + user_name: "Rajesh Khanna", + user_email: "rajesh.admin@example.com", + user_password: "AdminPass123!", + user_role: UserRole.ADMIN + }, + { + user_name: "Sunita Desai", + user_email: "sunita.admin@example.com", + user_password: "AdminPass123!", + user_role: UserRole.ADMIN + }, + { + user_name: "Mohan Reddy", + user_email: "mohan.admin@example.com", + user_password: "AdminPass123!", + user_role: UserRole.ADMIN + } +]; + +// SuperAdmin account +const superAdminData = [ + { + user_name: "Anil Kapoor", + user_email: "superadmin@mpgovt.in", + user_password: "SuperAdmin123!", + user_role: UserRole.SUPER_ADMIN, + validation_key: "MPGovtAdmin2025" + } +]; + +export async function seedUsers(prisma: PrismaClient) { + console.log('Seeding users...'); + const config = getSeedConfig(); + + // Hash passwords and create users + const saltRounds = 10; + + // Seed regular users - use configured count + const usersToCreate = Math.min(userData.length, config.users.regular); + for (let i = 0; i < usersToCreate; i++) { + const user = userData[i]; + const hashedPassword = await bcrypt.hash(user.user_password, saltRounds); + await prisma.user.upsert({ + where: { user_email: user.user_email }, + update: {}, + create: { + user_name: user.user_name, + user_email: user.user_email, + user_password: hashedPassword, + user_role: user.user_role + } + }); + } + + // Seed admin users - use configured count + const adminsToCreate = Math.min(adminData.length, config.users.admins); + for (let i = 0; i < adminsToCreate; i++) { + const admin = adminData[i]; + const hashedPassword = await bcrypt.hash(admin.user_password, saltRounds); + await prisma.user.upsert({ + where: { user_email: admin.user_email }, + update: {}, + create: { + user_name: admin.user_name, + user_email: admin.user_email, + user_password: hashedPassword, + user_role: admin.user_role + } + }); + } + + // Always seed super admin (just 1) + for (const superAdmin of superAdminData) { + const hashedPassword = await bcrypt.hash(superAdmin.user_password, saltRounds); + await prisma.user.upsert({ + where: { user_email: superAdmin.user_email }, + update: {}, + create: { + user_name: superAdmin.user_name, + user_email: superAdmin.user_email, + user_password: hashedPassword, + user_role: superAdmin.user_role, + validation_key: superAdmin.validation_key + } + }); + } + + console.log(`Users seeded successfully (${usersToCreate} regular, ${adminsToCreate} admins, 1 super admin)`); +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-utils.ts b/backend/prisma/seed/seed-utils.ts new file mode 100644 index 0000000..5f5d385 --- /dev/null +++ b/backend/prisma/seed/seed-utils.ts @@ -0,0 +1,83 @@ +import { PrismaClient } from '@prisma/client'; +import { performance } from 'perf_hooks'; + +/** + * Logs a seed operation with timing information + * @param operationName Name of the seed operation + * @param operation The async function to execute + * @returns The result of the operation + */ +export async function logSeedOperation(operationName: string, operation: () => Promise): Promise { + console.log(`\nšŸ“‹ Starting seed operation: ${operationName}`); + const startTime = performance.now(); + + try { + const result = await operation(); + const endTime = performance.now(); + const duration = ((endTime - startTime) / 1000).toFixed(2); + console.log(`āœ… Completed seed operation: ${operationName} (${duration}s)`); + return result; + } catch (error) { + console.error(`āŒ Failed seed operation: ${operationName}`); + console.error(error); + throw error; // Re-throw to halt the main seed process + } +} + +/** + * Verifies data exists before proceeding with dependent seeds + * @param prisma PrismaClient instance + * @param model Model name to check + * @param errorMessage Custom error message to display + */ +export async function verifyDataExists( + prisma: PrismaClient, + model: string, + errorMessage?: string +): Promise { + // @ts-ignore - Dynamic property access + const count = await prisma[model].count(); + if (count === 0) { + throw new Error(errorMessage || `No ${model} found. Please seed ${model} first.`); + } + console.log(`āœ“ Verified ${count} ${model} records exist`); +} + +/** + * Determines if we're running in a test environment + * @returns boolean + */ +export function isTestEnvironment(): boolean { + return process.env.NODE_ENV === 'test'; +} + +/** + * Gets the seed data size based on environment + * For test environments, we use minimal data + * For development, we use more comprehensive data + */ +export function getSeedDataSize(): { + users: number; + orgs: number; + labs: number; + timeSlots: number; + bookingsPerUser: number; +} { + if (isTestEnvironment()) { + return { + users: 2, + orgs: 1, + labs: 1, + timeSlots: 5, + bookingsPerUser: 1 + }; + } + + return { + users: 5, + orgs: 3, + labs: 5, + timeSlots: 20, + bookingsPerUser: 2 + }; +} \ No newline at end of file diff --git a/backend/prisma/seed/seed-waitlists.ts b/backend/prisma/seed/seed-waitlists.ts new file mode 100644 index 0000000..88032e2 --- /dev/null +++ b/backend/prisma/seed/seed-waitlists.ts @@ -0,0 +1,68 @@ +import { PrismaClient, WaitlistStatus } from '@prisma/client'; +import { getSeedConfig } from './seed-config'; + +export async function seedWaitlists(prisma: PrismaClient) { + console.log('Seeding waitlists...'); + const config = getSeedConfig(); + + // Get users for waitlists + const users = await prisma.user.findMany({ + where: { user_role: 'USER' } + }); + + if (users.length === 0) { + throw new Error('No users found. Please seed users first.'); + } + + // Find time slots to mark as fully booked for waitlisting + const timeSlots = await prisma.timeSlot.findMany({ + take: Math.max(1, Math.ceil(config.waitlistsPerUser * users.length / 2)), // Ensure enough slots + orderBy: { start_time: 'asc' } + }); + + if (timeSlots.length === 0) { + throw new Error('No time slots found. Please seed time slots first.'); + } + + // Update the time slots to be fully booked + for (const slot of timeSlots) { + await prisma.timeSlot.update({ + where: { id: slot.id }, + data: { status: 'FULL' } + }); + } + + let waitlistsCreated = 0; + + // For each user, create the configured number of waitlist entries + for (let i = 0; i < users.length; i++) { + const user = users[i]; + + for (let j = 0; j < config.waitlistsPerUser; j++) { + // Select a time slot for this waitlist (round-robin) + const slotIndex = (i * config.waitlistsPerUser + j) % timeSlots.length; + const timeSlot = timeSlots[slotIndex]; + + // Determine position (1-3) + const position = (j % 3) + 1; + + // Determine status (first position may be FULFILLED, others ACTIVE) + const status = position === 1 && Math.random() > 0.7 + ? WaitlistStatus.FULFILLED + : WaitlistStatus.ACTIVE; + + await prisma.waitlist.create({ + data: { + user_id: user.id, + slot_id: timeSlot.id, + waitlist_position: position, + waitlist_status: status + } + }); + + waitlistsCreated++; + } + } + + console.log(`Waitlists seeded successfully (${waitlistsCreated} waitlist entries created)`); +} \ No newline at end of file diff --git a/docs/Implementation/Develpment Plan Backend.md b/docs/Implementation/Develpment Plan Backend.md index 1f9249b..e9e744f 100644 --- a/docs/Implementation/Develpment Plan Backend.md +++ b/docs/Implementation/Develpment Plan Backend.md @@ -12,33 +12,33 @@ * [x] Review existing schema for any missing fields or relationships. * [x] Verify entity relationships match the ERD documentation. * [x] Ensure all necessary indexes are defined for performance. -* [ ] Task 1.3: Generate Prisma Client +* [x] Task 1.3: Generate Prisma Client * [x] Run Prisma generate command to create type-safe client. * [x] Verify client generation completes without errors. * [x] Ensure generated types are properly recognized by TypeScript. -* [ ] Task 1.4: Initial Migration Creation +* [x] Task 1.4: Initial Migration Creation * [x] Create and apply initial migration to establish database schema. * [x] Check migration logs for any issues. * [x] Verify tables are created with correct structure. **Day 2: Seed Data Implementation** -* [ ] Task 2.1: Define Seed Data Requirements - * [ ] Identify necessary seed data for development (users, labs, time slots). - * [ ] Determine admin and superadmin test accounts. - * [ ] Plan realistic lab and time slot configurations. -* [ ] Task 2.2: Create Seed Script Structure - * [ ] Define seed script file structure in prisma directory. - * [ ] Implement password hashing integration for user accounts. - * [ ] Ensure seed data covers all entity types. -* [ ] Task 2.3: Implement Testing Configuration - * [ ] Add seed script to npm scripts for easy execution. - * [ ] Create separate seed data for testing environment. - * [ ] Document seed data for development team reference. -* [ ] Task 2.4: Run and Verify Seed Data - * [ ] Execute seed script and verify data insertion. - * [ ] Check relationships between seeded entities. - * [ ] Document any required manual setup steps. +* [x] Task 2.1: Define Seed Data Requirements + * [x] Identify necessary seed data for development (users, labs, time slots). + * [x] Determine admin and superadmin test accounts. + * [x] Plan realistic lab and time slot configurations. +* [x] Task 2.2: Create Seed Script Structure + * [x] Define seed script file structure in prisma directory. + * [x] Implement password hashing integration for user accounts. + * [x] Ensure seed data covers all entity types. +* [x] Task 2.3: Implement Testing Configuration + * [x] Add seed script to npm scripts for easy execution. + * [x] Create separate seed data for testing environment. + * [x] Document seed data for development team reference. +* [x] Task 2.4: Run and Verify Seed Data + * [x] Execute seed script and verify data insertion. + * [x] Check relationships between seeded entities. + * [x] Document any required manual setup steps. **Day 3: Database Testing & Optimization**