import { IDatabase, User, Client, FitnessProfile, Attendance, Recommendation, FitnessGoal, Notification, DailyNutrition, DailyHydration, MealEntry, FitnessProfileHistory, TrainerClientAssignment, DatabaseConfig, } from "./types"; import { db as defaultDb, users, clients, fitnessProfiles, attendance, recommendations, fitnessGoals, notifications, dailyNutrition, dailyHydration, mealEntries, fitnessProfileHistory, trainerClientAssignments, eq, and, desc, asc, sql, inArray, gte, lte, or, like, } from "@fitai/database"; import { SQL } from "drizzle-orm"; import type { SortConfig, FilterCondition } from "../filtering"; import { buildWhereConditions, buildSearchCondition } from "../filtering"; import log from "../logger"; // Database row types (before mapping to domain types) interface UserRow { id: string; email: string; firstName: string; lastName: string; password: string | null; phone: string | null; role: string; gymId?: string | null; gym_id?: string | null; // Alternative column name createdAt: number | Date; updatedAt: number | Date; } interface ClientRow { id: string; userId: string; membershipType: string; membershipStatus: string; joinDate: number | Date; lastVisit?: number | Date | null; emergencyContactName?: string | null; emergencyContactPhone?: string | null; emergencyContactRelationship?: string | null; createdAt: number | Date; updatedAt: number | Date; } interface FitnessProfileRow { id: string; userId: string; height?: number | null; weight?: number | null; age?: number | null; goals?: string | null; medicalConditions?: string | null; dietaryRestrictions?: string | null; activityLevel?: string | null; createdAt: number | Date; updatedAt: number | Date; } interface AttendanceRow { id: string; userId: string; type: string; checkInTime: number | Date; checkOutTime?: number | Date | null; notes?: string | null; createdAt: number | Date; } interface RecommendationRow { id: string; userId: string; fitnessProfileId: string; recommendationText: string; activityPlan: string; dietPlan: string; status: string; generatedAt: number | Date; approvedBy?: string | null; approvedAt?: number | Date | null; createdAt: number | Date; updatedAt: number | Date; } interface FitnessGoalRow { id: string; userId: string; fitnessProfileId?: string | null; goalType: string; title: string; description?: string | null; targetValue?: number | null; currentValue?: number | null; unit?: string | null; status: string; progress: number | null; priority: string; startDate: number | Date; targetDate?: number | Date | null; completedDate?: number | Date | null; notes?: string | null; createdAt: number | Date; updatedAt: number | Date; } // Type for PRAGMA table_info result interface TableInfoRow { name: string; type: string; notnull: number; dflt_value: string | null; pk: number; } // Type for raw SQL results with unknown structure type RawSQLRow = Record; export class DrizzleDatabase implements IDatabase { private config: DatabaseConfig; private db: typeof defaultDb; constructor(config: DatabaseConfig, db?: typeof defaultDb) { this.config = config; this.db = db || defaultDb; } async connect(): Promise { // Drizzle with better-sqlite3 connects synchronously on initialization // We can just log here if needed if (this.config.options?.logging) { log.info("Drizzle database connected"); } await this.createTables(); } async disconnect(): Promise { // better-sqlite3 handle is managed by Drizzle, usually no explicit disconnect needed for connection pooling // but we can close the underlying sqlite instance if we had access to it. // For now, we'll assume it's handled. if (this.config.options?.logging) { log.info("Drizzle database disconnected"); } } private async createTables(): Promise { // Users table await this.db.run(sql` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, password TEXT, phone TEXT, role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')), gym_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); // Migration: ensure gym_id column exists on users (for existing DBs) const userCols = await this.db.all(sql`PRAGMA table_info('users')`); if (!userCols.some((c: any) => c.name === "gym_id")) { await this.db.run(sql`ALTER TABLE users ADD COLUMN gym_id TEXT`); } // Clients table await this.db.run(sql` CREATE TABLE IF NOT EXISTS clients ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, membership_type TEXT NOT NULL CHECK (membership_type IN ('basic', 'premium', 'vip')), membership_status TEXT NOT NULL CHECK (membership_status IN ('active', 'inactive', 'suspended')), join_date INTEGER NOT NULL, last_visit INTEGER, emergency_contact_name TEXT, emergency_contact_phone TEXT, emergency_contact_relationship TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) `); // Fitness profiles table await this.db.run(sql` CREATE TABLE IF NOT EXISTS fitness_profiles ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL UNIQUE, height REAL, weight REAL, age INTEGER, gender TEXT CHECK (gender IN ('male', 'female', 'other', 'prefer_not_to_say')), activity_level TEXT CHECK (activity_level IN ('sedentary', 'lightly_active', 'moderately_active', 'very_active', 'extremely_active')), fitness_goals TEXT, exercise_habits TEXT, diet_habits TEXT, medical_conditions TEXT, allergies TEXT, injuries TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) `); // Attendance table await this.db.run(sql` CREATE TABLE IF NOT EXISTS attendance ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, check_in_time INTEGER NOT NULL, check_out_time INTEGER, type TEXT NOT NULL CHECK (type IN ('gym', 'class', 'personal_training')), notes TEXT, created_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) `); // Recommendations table await this.db.run(sql` CREATE TABLE IF NOT EXISTS recommendations ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, fitness_profile_id TEXT NOT NULL, recommendation_text TEXT NOT NULL, activity_plan TEXT NOT NULL, diet_plan TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')) DEFAULT 'pending', generated_at INTEGER NOT NULL, approved_at INTEGER, approved_by TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE ) `); // Fitness Goals table await this.db.run(sql` CREATE TABLE IF NOT EXISTS fitness_goals ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, fitness_profile_id TEXT, goal_type TEXT NOT NULL, title TEXT NOT NULL, description TEXT, target_value REAL, current_value REAL, unit TEXT, start_date INTEGER NOT NULL, target_date INTEGER, completed_date INTEGER, status TEXT NOT NULL DEFAULT 'active', progress REAL DEFAULT 0, priority TEXT DEFAULT 'medium', notes TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE ) `); } // User operations async createUser( userData: Omit & { id?: string }, ): Promise { const id = userData.id || Math.random().toString(36).substr(2, 9); const now = new Date(); const newUser = { ...userData, id, createdAt: now, updatedAt: now, }; await this.db.insert(users).values(newUser); return newUser; } async getUserById(id: string): Promise { const result = await this.db .select() .from(users) .where(eq(users.id, id)) .get(); return result ? this.mapUser(result) : null; } async getUserByEmail(email: string): Promise { const result = await this.db .select() .from(users) .where(eq(users.email, email)) .get(); return result ? this.mapUser(result) : null; } async getAllUsers(): Promise { const results = await this.db .select() .from(users) .orderBy(desc(users.createdAt)) .all(); return results.map(this.mapUser); } async updateUser(id: string, updates: Partial): Promise { const { id: _, ...updateData } = updates; if (Object.keys(updateData).length === 0) return this.getUserById(id); await this.db .update(users) .set({ ...updateData, updatedAt: new Date() }) .where(eq(users.id, id)) .run(); return this.getUserById(id); } async deleteUser(id: string): Promise { const result = await this.db.delete(users).where(eq(users.id, id)).run(); return result.changes > 0; } async migrateUserId(oldId: string, newId: string): Promise { await this.db .update(users) .set({ id: newId }) .where(eq(users.id, oldId)) .run(); } /** * Get users with pagination, sorting, and filtering * Uses SQL LIMIT/OFFSET for efficient pagination */ async getUsersWithPagination(params: { page: number; limit: number; role?: string; sort?: SortConfig; filters?: FilterCondition[]; search?: string; }): Promise<{ users: User[]; total: number }> { const { page, limit, role, sort, filters, search } = params; const offset = (page - 1) * limit; // Build WHERE conditions const whereConditions: any[] = []; // Add role filter if provided if (role) { whereConditions.push(eq(users.role, role as User["role"])); } // Add filter conditions from query params if (filters && filters.length > 0) { const columnMap = { role: users.role, email: users.email, firstName: users.firstName, lastName: users.lastName, phone: users.phone, gymId: users.gymId, createdAt: users.createdAt, updatedAt: users.updatedAt, }; const filterCondition = buildWhereConditions(filters, columnMap); if (filterCondition) { whereConditions.push(filterCondition); } } // Add search condition if (search) { const searchFields = ["email", "firstName", "lastName", "phone"]; const searchCondition = buildSearchCondition(search, searchFields, { email: users.email, firstName: users.firstName, lastName: users.lastName, phone: users.phone, }); if (searchCondition) { whereConditions.push(searchCondition); } } const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined; // Build ORDER BY clause const sortColumn = sort?.field ? { email: users.email, firstName: users.firstName, lastName: users.lastName, role: users.role, createdAt: users.createdAt, updatedAt: users.updatedAt, }[sort.field] : users.createdAt; const orderBy = sort?.direction === "asc" ? asc(sortColumn || users.createdAt) : desc(sortColumn || users.createdAt); // Get total count (without pagination) const countQuery = whereClause ? this.db .select({ count: sql`count(*)` }) .from(users) .where(whereClause) : this.db.select({ count: sql`count(*)` }).from(users); const countResult = await countQuery.get(); const total = countResult?.count ?? 0; // Get paginated results const query = this.db .select() .from(users) .orderBy(orderBy) .limit(limit) .offset(offset); const results = whereClause ? await query.where(whereClause).all() : await query.all(); return { users: results.map(this.mapUser), total, }; } /** * Get users with their client and attendance data in optimized batches * Avoids N+1 query problem by batching related data queries * Now supports sorting, filtering, and search */ async getUsersWithRelatedData(params?: { page?: number; limit?: number; role?: string; sort?: SortConfig; filters?: FilterCondition[]; search?: string; }): Promise<{ users: Array< User & { client?: Client | null; isCheckedIn?: boolean; checkInTime?: Date | null; lastCheckInTime?: Date | null; checkInsThisWeek?: number; checkInsThisMonth?: number; } >; total?: number; }> { const page = params?.page ?? 1; const limit = params?.limit ?? 1000; const role = params?.role; const sort = params?.sort; const filters = params?.filters; const search = params?.search; // Step 1: Get paginated users with sorting and filtering const { users: paginatedUsers, total } = await this.getUsersWithPagination({ page, limit, role, sort, filters, search, }); if (paginatedUsers.length === 0) { return { users: [], total: 0 }; } const userIds = paginatedUsers.map((u) => u.id); // Step 2: Batch fetch clients for all users (1 query instead of N) const allClients = await this.db.select().from(clients).all(); const clientsByUserId = new Map( allClients.map((c) => [c.userId, this.mapClient(c)]), ); // Step 3: Batch fetch attendance stats (2 queries instead of 2N) const attendanceStats = await this.getAttendanceStatsBatch(userIds); // Step 4: Combine all data const usersWithData = paginatedUsers.map((user) => { const client = clientsByUserId.get(user.id) || null; const stats = attendanceStats.get(user.id) || { isCheckedIn: false, checkInTime: null, lastCheckInTime: null, checkInsThisWeek: 0, checkInsThisMonth: 0, }; return { ...user, client, ...stats, }; }); return { users: usersWithData, total }; } /** * Get attendance statistics for multiple users efficiently * Uses aggregation queries to avoid N+1 problem */ async getAttendanceStatsBatch(userIds: string[]): Promise< Map< string, { isCheckedIn: boolean; checkInTime: Date | null; lastCheckInTime: Date | null; checkInsThisWeek: number; checkInsThisMonth: number; } > > { if (userIds.length === 0) { return new Map(); } // Fetch ALL attendance records for these users (2 queries total instead of 2N) const allAttendance = await this.db.select().from(attendance).all(); // Filter to relevant users const relevantAttendance = allAttendance.filter((a) => userIds.includes(a.userId), ); const statsMap = new Map(); const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); for (const userId of userIds) { const userAttendance = relevantAttendance.filter( (a) => a.userId === userId, ); // Find active check-in const activeCheckIn = userAttendance.find((a) => !a.checkOutTime); // Get last check-in time const sortedAttendance = userAttendance.sort( (a, b) => new Date(b.checkInTime).getTime() - new Date(a.checkInTime).getTime(), ); const lastCheckInTime = sortedAttendance[0]?.checkInTime || null; // Count check-ins const checkInsThisWeek = userAttendance.filter( (a) => new Date(a.checkInTime) >= weekAgo, ).length; const checkInsThisMonth = userAttendance.filter( (a) => new Date(a.checkInTime) >= monthAgo, ).length; statsMap.set(userId, { isCheckedIn: !!activeCheckIn, checkInTime: activeCheckIn?.checkInTime || null, lastCheckInTime, checkInsThisWeek, checkInsThisMonth, }); } // Fill in missing users with default values for (const userId of userIds) { if (!statsMap.has(userId)) { statsMap.set(userId, { isCheckedIn: false, checkInTime: null, lastCheckInTime: null, checkInsThisWeek: 0, checkInsThisMonth: 0, }); } } return statsMap; } // Client operations async createClient(clientData: Omit): Promise { const id = Math.random().toString(36).substr(2, 9); const newClient = { id, ...clientData, createdAt: new Date(), updatedAt: new Date(), }; await this.db.insert(clients).values(newClient as any); return this.mapClient(newClient); } async getClientById(id: string): Promise { const result = await this.db .select() .from(clients) .where(eq(clients.id, id)) .get(); return result ? this.mapClient(result) : null; } async getClientByUserId(userId: string): Promise { const result = await this.db .select() .from(clients) .where(eq(clients.userId, userId)) .get(); return result ? this.mapClient(result) : null; } async getClientsByUserIds(userIds: string[]): Promise { if (userIds.length === 0) return []; const results = await this.db .select() .from(clients) .where(inArray(clients.userId, userIds)) .all(); return results.map((r) => this.mapClient(r)); } async getAllClients(): Promise { const results = await this.db .select() .from(clients) .orderBy(desc(clients.joinDate)) .all(); return results.map(this.mapClient); } /** * Get clients with pagination, sorting, and filtering */ async getClientsWithPagination(params: { page: number; limit: number; sort?: SortConfig; filters?: FilterCondition[]; search?: string; }): Promise<{ clients: Client[]; total: number }> { const { page, limit, sort, filters, search } = params; const offset = (page - 1) * limit; // Build WHERE conditions const whereConditions: any[] = []; // Add filter conditions if (filters && filters.length > 0) { const columnMap = { userId: clients.userId, membershipType: clients.membershipType, membershipStatus: clients.membershipStatus, joinDate: clients.joinDate, lastVisit: clients.lastVisit, }; const filterCondition = buildWhereConditions(filters, columnMap); if (filterCondition) { whereConditions.push(filterCondition); } } // Add search condition (search by user ID or emergency contact) if (search) { const searchCondition = buildSearchCondition( search, ["userId", "emergencyContactName", "emergencyContactPhone"], { userId: clients.userId, emergencyContactName: clients.emergencyContactName, emergencyContactPhone: clients.emergencyContactPhone, }, ); if (searchCondition) { whereConditions.push(searchCondition); } } const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined; // Build ORDER BY clause const sortColumn = sort?.field ? { joinDate: clients.joinDate, lastVisit: clients.lastVisit, membershipType: clients.membershipType, membershipStatus: clients.membershipStatus, userId: clients.userId, }[sort.field] : clients.joinDate; const orderBy = sort?.direction === "asc" ? asc(sortColumn || clients.joinDate) : desc(sortColumn || clients.joinDate); // Get total count const countQuery = whereClause ? this.db .select({ count: sql`count(*)` }) .from(clients) .where(whereClause) : this.db.select({ count: sql`count(*)` }).from(clients); const countResult = await countQuery.get(); const total = countResult?.count ?? 0; // Get paginated results const query = this.db .select() .from(clients) .orderBy(orderBy) .limit(limit) .offset(offset); const results = whereClause ? await query.where(whereClause).all() : await query.all(); return { clients: results.map(this.mapClient), total, }; } async updateClient( id: string, updates: Partial, ): Promise { const { id: _, ...updateData } = updates; if (Object.keys(updateData).length === 0) return this.getClientById(id); await this.db .update(clients) .set({ ...updateData, updatedAt: new Date() } as any) .where(eq(clients.id, id)) .run(); return this.getClientById(id); } async deleteClient(id: string): Promise { const result = await this.db .delete(clients) .where(eq(clients.id, id)) .run(); return result.changes > 0; } // Fitness Profile operations async createFitnessProfile( profileData: Omit, ): Promise { const now = new Date(); const id = Math.random().toString(36).substr(2, 9); const newProfile = { id, ...profileData, createdAt: now, updatedAt: now, }; await this.db.insert(fitnessProfiles).values(newProfile as any); return { id, ...profileData, createdAt: now, updatedAt: now }; } async getFitnessProfileByUserId( userId: string, ): Promise { const result = await this.db .select() .from(fitnessProfiles) .where(eq(fitnessProfiles.userId, userId)) .get(); return result ? this.mapFitnessProfile(result) : null; } async getAllFitnessProfiles(): Promise { const results = await this.db .select() .from(fitnessProfiles) .orderBy(desc(fitnessProfiles.createdAt)) .all(); return results.map(this.mapFitnessProfile); } async updateFitnessProfile( userId: string, updates: Partial, ): Promise { const { userId: _, ...updateData } = updates; if (Object.keys(updateData).length === 0) return this.getFitnessProfileByUserId(userId); const dbUpdates = { ...updateData } as any; if (updateData.fitnessGoals) { dbUpdates.fitnessGoals = JSON.stringify(updateData.fitnessGoals); } await this.db .update(fitnessProfiles) .set({ ...dbUpdates, updatedAt: new Date() }) .where(eq(fitnessProfiles.userId, userId)) .run(); return this.getFitnessProfileByUserId(userId); } async deleteFitnessProfile(userId: string): Promise { const result = await this.db .delete(fitnessProfiles) .where(eq(fitnessProfiles.userId, userId)) .run(); return result.changes > 0; } // Attendance operations async checkIn( userId: string, type: "gym" | "class" | "personal_training", notes?: string, ): Promise { const id = Math.random().toString(36).substr(2, 9); const now = new Date(); const newAttendance = { id, userId, checkInTime: now, type, notes, createdAt: now, }; await this.db.insert(attendance).values(newAttendance as any); // Update client last visit const client = await this.getClientByUserId(userId); if (client) { await this.updateClient(client.id, { lastVisit: now }); } return newAttendance; } async checkOut(attendanceId: string): Promise { const now = new Date(); await this.db .update(attendance) .set({ checkOutTime: now }) .where(eq(attendance.id, attendanceId)) .run(); return this.getAttendanceById(attendanceId); } async getAttendanceById(id: string): Promise { const result = await this.db .select() .from(attendance) .where(eq(attendance.id, id)) .get(); return result ? this.mapAttendance(result) : null; } async getAttendanceHistory(userId: string): Promise { const results = await this.db .select() .from(attendance) .where(eq(attendance.userId, userId)) .orderBy(desc(attendance.checkInTime)) .all(); return results.map(this.mapAttendance); } async getAllAttendance(): Promise { const results = await this.db .select() .from(attendance) .orderBy(desc(attendance.checkInTime)) .all(); return results.map(this.mapAttendance); } /** * Get attendance records with pagination, sorting, and filtering */ async getAttendanceWithPagination(params: { page: number; limit: number; sort?: SortConfig; filters?: FilterCondition[]; search?: string; }): Promise<{ attendance: Attendance[]; total: number }> { const { page, limit, sort, filters, search } = params; const offset = (page - 1) * limit; // Build WHERE conditions const whereConditions: any[] = []; // Add filter conditions if (filters && filters.length > 0) { const columnMap = { userId: attendance.userId, type: attendance.type, checkInTime: attendance.checkInTime, checkOutTime: attendance.checkOutTime, }; const filterCondition = buildWhereConditions(filters, columnMap); if (filterCondition) { whereConditions.push(filterCondition); } } // Add search condition (search by user ID or notes) if (search) { const searchCondition = buildSearchCondition( search, ["userId", "notes"], { userId: attendance.userId, notes: attendance.notes, }, ); if (searchCondition) { whereConditions.push(searchCondition); } } const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined; // Build ORDER BY clause const sortColumn = sort?.field ? { checkInTime: attendance.checkInTime, checkOutTime: attendance.checkOutTime, type: attendance.type, userId: attendance.userId, }[sort.field] : attendance.checkInTime; const orderBy = sort?.direction === "asc" ? asc(sortColumn || attendance.checkInTime) : desc(sortColumn || attendance.checkInTime); // Get total count const countQuery = whereClause ? this.db .select({ count: sql`count(*)` }) .from(attendance) .where(whereClause) : this.db.select({ count: sql`count(*)` }).from(attendance); const countResult = await countQuery.get(); const total = countResult?.count ?? 0; // Get paginated results const query = this.db .select() .from(attendance) .orderBy(orderBy) .limit(limit) .offset(offset); const results = whereClause ? await query.where(whereClause).all() : await query.all(); return { attendance: results.map(this.mapAttendance), total, }; } async getActiveCheckIn(userId: string): Promise { // Drizzle doesn't support IS NULL in where directly with simple syntax sometimes, but eq(col, null) works or isNull(col) // We need to check how to filter for null checkOutTime. // In Drizzle, we can filter in JS or use isNull operator if imported. // Let's fetch recent and filter for now to be safe, or use raw sql if needed, but better to use Drizzle operators. // Actually, we can just fetch all for user and filter in memory since it's unlikely to be huge for active checkins, // but correct way is `isNull(attendance.checkOutTime)`. // Since I didn't import `isNull`, I'll fetch recent history and find first active. const history = await this.getAttendanceHistory(userId); return history.find((a) => !a.checkOutTime) || null; } async getAttendanceHistoriesByUserIds( userIds: string[], ): Promise { if (userIds.length === 0) return []; const results = await this.db .select() .from(attendance) .where(inArray(attendance.userId, userIds)) .orderBy(desc(attendance.checkInTime)) .all(); return results.map((r) => this.mapAttendance(r)); } async getActiveCheckInsByUserIds(userIds: string[]): Promise { if (userIds.length === 0) return []; // Get all attendance records for these users and filter for active check-ins const results = await this.db .select() .from(attendance) .where(inArray(attendance.userId, userIds)) .all(); // Filter for active check-ins (no checkout time) return results .filter((r) => !r.checkOutTime) .map((r) => this.mapAttendance(r)); } // Recommendation operations async createRecommendation( data: Omit, ): Promise { const now = new Date(); const newRec = { ...data, createdAt: now, status: data.status || "pending", }; await this.db.insert(recommendations).values(newRec as any); return newRec as Recommendation; } async getRecommendationsByUserId(userId: string): Promise { const results = await this.db .select() .from(recommendations) .where(eq(recommendations.userId, userId)) .orderBy(desc(recommendations.createdAt)) .all(); return results.map(this.mapRecommendation); } async getAllRecommendations(): Promise { const results = await this.db .select() .from(recommendations) .orderBy(desc(recommendations.createdAt)) .all(); return results.map(this.mapRecommendation); } async updateRecommendation( id: string, updates: Partial, ): Promise { const { id: _, ...updateData } = updates; if (Object.keys(updateData).length === 0) return this.getRecommendationById(id); await this.db .update(recommendations) .set(updateData as any) .where(eq(recommendations.id, id)) .run(); return this.getRecommendationById(id); } async deleteRecommendation(id: string): Promise { const result = await this.db .delete(recommendations) .where(eq(recommendations.id, id)) .run(); return result.changes > 0; } async getRecommendationById(id: string): Promise { const result = await this.db .select() .from(recommendations) .where(eq(recommendations.id, id)) .get(); return result ? this.mapRecommendation(result) : null; } async getRecommendationsByUserIds( userIds: string[], ): Promise { if (userIds.length === 0) return []; const results = await this.db .select() .from(recommendations) .where(inArray(recommendations.userId, userIds)) .orderBy(desc(recommendations.createdAt)) .all(); return results.map((r) => this.mapRecommendation(r)); } // Fitness Goals operations async createFitnessGoal( goalData: Omit, ): Promise { const now = new Date(); const newGoal = { ...goalData, createdAt: now, updatedAt: now, }; await this.db.insert(fitnessGoals).values(newGoal as any); return newGoal as FitnessGoal; } async getFitnessGoalById(id: string): Promise { const result = await this.db .select() .from(fitnessGoals) .where(eq(fitnessGoals.id, id)) .get(); return result ? this.mapFitnessGoal(result) : null; } async getFitnessGoalsByUserId( userId: string, status?: string, ): Promise { let query = this.db .select() .from(fitnessGoals) .where(eq(fitnessGoals.userId, userId)); if (status) { query = this.db .select() .from(fitnessGoals) .where( and( eq(fitnessGoals.userId, userId), eq(fitnessGoals.status, status as any), ), ); } const results = await query.orderBy(desc(fitnessGoals.createdAt)).all(); return results.map(this.mapFitnessGoal); } async updateFitnessGoal( id: string, updates: Partial, ): Promise { const { id: _, ...updateData } = updates; if (Object.keys(updateData).length === 0) return this.getFitnessGoalById(id); await this.db .update(fitnessGoals) .set({ ...updateData, updatedAt: new Date() } as any) .where(eq(fitnessGoals.id, id)) .run(); return this.getFitnessGoalById(id); } async deleteFitnessGoal(id: string): Promise { const result = await this.db .delete(fitnessGoals) .where(eq(fitnessGoals.id, id)) .run(); return result.changes > 0; } async updateGoalProgress( id: string, currentValue: number, ): Promise { const goal = await this.getFitnessGoalById(id); if (!goal) return null; let progress = goal.progress; if (goal.targetValue && goal.targetValue > 0) { progress = Math.min(100, (currentValue / goal.targetValue) * 100); } await this.db .update(fitnessGoals) .set({ currentValue, progress, updatedAt: new Date() }) .where(eq(fitnessGoals.id, id)) .run(); return this.getFitnessGoalById(id); } async completeGoal(id: string): Promise { const now = new Date(); await this.db .update(fitnessGoals) .set({ status: "completed", progress: 100, completedDate: now, updatedAt: now, }) .where(eq(fitnessGoals.id, id)) .run(); return this.getFitnessGoalById(id); } async getFitnessGoalsByUserIds(userIds: string[]): Promise { if (userIds.length === 0) return []; const results = await this.db .select() .from(fitnessGoals) .where(inArray(fitnessGoals.userId, userIds)) .orderBy(desc(fitnessGoals.createdAt)) .all(); return results.map((r) => this.mapFitnessGoal(r)); } async getDashboardStats(): Promise<{ totalUsers: number; activeClients: number; totalRevenue: number; revenueGrowth: number; }> { // Placeholder implementation as per original sqlite.ts (which didn't implement this either in the viewed snippet, // but interface requires it. I'll provide a basic implementation or mock). // The original sqlite.ts snippet ended before showing this method, but the interface has it. // I'll implement a basic count. const allUsers = await this.db.select().from(users).all(); const activeClients = await this.db .select() .from(clients) .where(eq(clients.membershipStatus, "active")) .all(); return { totalUsers: allUsers.length, activeClients: activeClients.length, totalRevenue: 0, // Not tracking payments yet revenueGrowth: 0, }; } // Mappers - using Record for database rows since Drizzle types vary private mapUser(row: Record): User { return { id: String(row.id), email: String(row.email), firstName: String(row.firstName), lastName: String(row.lastName), password: String(row.password ?? ""), phone: row.phone ? String(row.phone) : undefined, role: String(row.role) as User["role"], gymId: row.gymId ? String(row.gymId) : row.gym_id ? String(row.gym_id) : undefined, createdAt: new Date(row.createdAt as number | Date), updatedAt: new Date(row.updatedAt as number | Date), }; } private mapClient(row: Record): Client { return { id: String(row.id), userId: String(row.userId), membershipType: String(row.membershipType) as Client["membershipType"], membershipStatus: String( row.membershipStatus, ) as Client["membershipStatus"], joinDate: typeof row.joinDate === "number" ? new Date(row.joinDate * 1000) : new Date(row.joinDate as Date), lastVisit: row.lastVisit ? typeof row.lastVisit === "number" ? new Date(row.lastVisit * 1000) : new Date(row.lastVisit as Date) : undefined, emergencyContact: row.emergencyContactName ? { name: String(row.emergencyContactName), phone: String(row.emergencyContactPhone ?? ""), relationship: String(row.emergencyContactRelationship ?? ""), } : undefined, }; } private mapFitnessProfile(row: Record): FitnessProfile { return { id: String(row.id), userId: String(row.userId), height: typeof row.height === "number" ? row.height : undefined, weight: typeof row.weight === "number" ? row.weight : undefined, age: typeof row.age === "number" ? row.age : undefined, gender: row.gender ? (String(row.gender) as FitnessProfile["gender"]) : undefined, fitnessGoals: row.goals ? [String(row.goals)] : undefined, medicalConditions: row.medicalConditions ? String(row.medicalConditions) : undefined, allergies: row.dietaryRestrictions ? String(row.dietaryRestrictions) : undefined, injuries: undefined, activityLevel: row.activityLevel ? (String(row.activityLevel) as FitnessProfile["activityLevel"]) : undefined, createdAt: new Date(row.createdAt as number | Date), updatedAt: new Date(row.updatedAt as number | Date), }; } private mapAttendance(row: Record): Attendance { return { id: String(row.id), userId: String(row.userId), type: String(row.type) as Attendance["type"], checkInTime: typeof row.checkInTime === "number" ? new Date(row.checkInTime * 1000) : new Date(row.checkInTime as Date), checkOutTime: row.checkOutTime ? typeof row.checkOutTime === "number" ? new Date(row.checkOutTime * 1000) : new Date(row.checkOutTime as Date) : undefined, notes: row.notes ? String(row.notes) : undefined, createdAt: typeof row.createdAt === "number" ? new Date(row.createdAt * 1000) : new Date(row.createdAt as Date), }; } private mapRecommendation(row: Record): Recommendation { return { id: String(row.id), userId: String(row.userId), fitnessProfileId: String(row.fitnessProfileId), recommendationText: String(row.recommendationText), activityPlan: String(row.activityPlan), dietPlan: String(row.dietPlan), status: String(row.status) as Recommendation["status"], generatedAt: new Date(row.generatedAt as number | Date), approvedBy: row.approvedBy ? String(row.approvedBy) : undefined, approvedAt: row.approvedAt ? new Date(row.approvedAt as number | Date) : undefined, createdAt: new Date(row.createdAt as number | Date), updatedAt: new Date(row.updatedAt as number | Date), }; } private mapFitnessGoal(row: Record): FitnessGoal { return { id: String(row.id), userId: String(row.userId), fitnessProfileId: row.fitnessProfileId ? String(row.fitnessProfileId) : undefined, goalType: String(row.goalType) as FitnessGoal["goalType"], title: String(row.title), description: row.description ? String(row.description) : undefined, targetValue: typeof row.targetValue === "number" ? row.targetValue : undefined, currentValue: typeof row.currentValue === "number" ? row.currentValue : undefined, unit: row.unit ? String(row.unit) : undefined, status: String(row.status) as FitnessGoal["status"], progress: typeof row.progress === "number" ? row.progress : 0, priority: String(row.priority) as FitnessGoal["priority"], startDate: new Date(row.startDate as number | Date), targetDate: row.targetDate ? new Date(row.targetDate as number | Date) : undefined, completedDate: row.completedDate ? new Date(row.completedDate as number | Date) : undefined, notes: row.notes ? String(row.notes) : undefined, createdAt: new Date(row.createdAt as number | Date), updatedAt: new Date(row.updatedAt as number | Date), }; } // Notification operations async createNotification( notification: Omit, ): Promise { const newNotification = { ...notification, createdAt: new Date(), }; const result = await this.db.insert(notifications).values(newNotification); log.debug("Notification created", { id: notification.id }); return newNotification; } async getNotificationsByUserId(userId: string): Promise { const rows = await this.db .select() .from(notifications) .where(eq(notifications.userId, userId)) .orderBy(desc(notifications.createdAt)) .all(); return rows.map((row) => this.mapNotification(row)); } async getUnreadNotificationCount(userId: string): Promise { const result = await this.db .select({ count: sql`count(*)` }) .from(notifications) .where( and(eq(notifications.userId, userId), eq(notifications.read, false)), ); return Number(result[0]?.count || 0); } async markNotificationAsRead(id: string): Promise { await this.db .update(notifications) .set({ read: true }) .where(eq(notifications.id, id)); const rows = await this.db .select() .from(notifications) .where(eq(notifications.id, id)); if (rows.length === 0) return null; return this.mapNotification(rows[0]); } async markAllNotificationsAsRead(userId: string): Promise { await this.db .update(notifications) .set({ read: true }) .where( and(eq(notifications.userId, userId), eq(notifications.read, false)), ); log.debug("All notifications marked as read", { userId }); } async deleteNotification(id: string): Promise { const result = await this.db .delete(notifications) .where(eq(notifications.id, id)); return result.changes > 0; } private mapNotification(row: Record): Notification { const createdAtValue = row.createdAt as number | Date; const createdAt = typeof createdAtValue === "number" ? new Date(createdAtValue * 1000) // SQLite stores in seconds, convert to milliseconds : createdAtValue; return { id: String(row.id), userId: String(row.userId || row.user_id), title: String(row.title), message: String(row.message), type: String(row.type) as Notification["type"], read: Boolean(row.read), createdAt, }; } // ==================== DAILY NUTRITION OPERATIONS ==================== async createDailyNutrition( nutrition: Omit, ): Promise { const id = `nutrition_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const now = new Date(); const newNutrition = { id, ...nutrition, createdAt: now, updatedAt: now, }; await this.db.insert(dailyNutrition).values(newNutrition as any); return this.mapDailyNutrition(newNutrition); } async getDailyNutrition( userId: string, date: string, ): Promise { const results = await this.db .select() .from(dailyNutrition) .where( and(eq(dailyNutrition.userId, userId), eq(dailyNutrition.date, date)), ) .limit(1) .all(); return results.length > 0 ? this.mapDailyNutrition(results[0]) : null; } async getDailyNutritionById(id: string): Promise { const results = await this.db .select() .from(dailyNutrition) .where(eq(dailyNutrition.id, id)) .limit(1) .all(); return results.length > 0 ? this.mapDailyNutrition(results[0]) : null; } async getDailyNutritionRange( userId: string, startDate: string, endDate: string, ): Promise { const results = await this.db .select() .from(dailyNutrition) .where( and( eq(dailyNutrition.userId, userId), gte(dailyNutrition.date, startDate), lte(dailyNutrition.date, endDate), ), ) .orderBy(dailyNutrition.date) .all(); return results.map((row) => this.mapDailyNutrition(row)); } async updateDailyNutrition( id: string, updates: Partial, ): Promise { const { id: _, ...updateData } = updates as any; if (Object.keys(updateData).length === 0) { const existing = await this.db .select() .from(dailyNutrition) .where(eq(dailyNutrition.id, id)) .limit(1) .all(); return existing.length > 0 ? this.mapDailyNutrition(existing[0]) : null; } await this.db .update(dailyNutrition) .set({ ...updateData, updatedAt: new Date() }) .where(eq(dailyNutrition.id, id)) .run(); const results = await this.db .select() .from(dailyNutrition) .where(eq(dailyNutrition.id, id)) .limit(1) .all(); return results.length > 0 ? this.mapDailyNutrition(results[0]) : null; } async deleteDailyNutrition(id: string): Promise { const result = await this.db .delete(dailyNutrition) .where(eq(dailyNutrition.id, id)) .run(); return result.changes > 0; } private mapDailyNutrition(row: Record): DailyNutrition { const createdAtValue = row.createdAt as number | Date; const updatedAtValue = row.updatedAt as number | Date; return { id: String(row.id), userId: String(row.userId), date: String(row.date), totalCalories: Number(row.totalCalories || 0), calorieGoal: Number(row.calorieGoal || 2000), meals: row.meals ? (JSON.parse(String(row.meals)) as any) : undefined, createdAt: typeof createdAtValue === "number" ? new Date(createdAtValue * 1000) : new Date(createdAtValue), updatedAt: typeof updatedAtValue === "number" ? new Date(updatedAtValue * 1000) : new Date(updatedAtValue), }; } // ==================== MEAL ENTRY OPERATIONS ==================== async createMealEntry( meal: Omit, ): Promise { const id = `meal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const now = new Date(); const newMeal = { id, ...meal, createdAt: now, }; await this.db.insert(mealEntries).values(newMeal as any); return this.mapMealEntry(newMeal); } async getMealEntriesByDate( userId: string, date: string, ): Promise { // Parse date string to get start and end of day in seconds const startOfDay = new Date(date); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(date); endOfDay.setHours(23, 59, 59, 999); const startTimestamp = Math.floor(startOfDay.getTime() / 1000); const endTimestamp = Math.floor(endOfDay.getTime() / 1000); const results = await this.db .select() .from(mealEntries) .where( and( eq(mealEntries.userId, userId), sql`${mealEntries.timestamp} >= ${startTimestamp}`, sql`${mealEntries.timestamp} <= ${endTimestamp}`, ), ) .orderBy(mealEntries.timestamp) .all(); return results.map((row) => this.mapMealEntry(row)); } async getMealEntryById(id: string): Promise { const results = await this.db .select() .from(mealEntries) .where(eq(mealEntries.id, id)) .limit(1) .all(); return results.length > 0 ? this.mapMealEntry(results[0]) : null; } async deleteMealEntry(id: string): Promise { const result = await this.db .delete(mealEntries) .where(eq(mealEntries.id, id)) .run(); return result.changes > 0; } private mapMealEntry(row: Record): MealEntry { const timestampValue = row.timestamp as number | Date; const createdAtValue = row.createdAt as number | Date; return { id: String(row.id), userId: String(row.userId), dailyNutritionId: row.dailyNutritionId ? String(row.dailyNutritionId) : undefined, mealType: String(row.mealType) as MealEntry["mealType"], foodName: String(row.foodName), calories: Number(row.calories), protein: row.protein ? Number(row.protein) : undefined, carbs: row.carbs ? Number(row.carbs) : undefined, fats: row.fats ? Number(row.fats) : undefined, timestamp: typeof timestampValue === "number" ? new Date(timestampValue * 1000) : new Date(timestampValue), createdAt: typeof createdAtValue === "number" ? new Date(createdAtValue * 1000) : new Date(createdAtValue), }; } // ==================== DAILY HYDRATION OPERATIONS ==================== async createDailyHydration( hydration: Omit, ): Promise { const id = `hydration_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const now = new Date(); const newHydration = { id, ...hydration, createdAt: now, updatedAt: now, }; await this.db.insert(dailyHydration).values(newHydration as any); return this.mapDailyHydration(newHydration); } async getDailyHydration( userId: string, date: string, ): Promise { const results = await this.db .select() .from(dailyHydration) .where( and(eq(dailyHydration.userId, userId), eq(dailyHydration.date, date)), ) .limit(1) .all(); return results.length > 0 ? this.mapDailyHydration(results[0]) : null; } async getDailyHydrationById(id: string): Promise { const results = await this.db .select() .from(dailyHydration) .where(eq(dailyHydration.id, id)) .limit(1) .all(); return results.length > 0 ? this.mapDailyHydration(results[0]) : null; } async getDailyHydrationRange( userId: string, startDate: string, endDate: string, ): Promise { const results = await this.db .select() .from(dailyHydration) .where( and( eq(dailyHydration.userId, userId), gte(dailyHydration.date, startDate), lte(dailyHydration.date, endDate), ), ) .orderBy(dailyHydration.date) .all(); return results.map((row) => this.mapDailyHydration(row)); } async updateDailyHydration( id: string, updates: Partial, ): Promise { const { id: _, ...updateData } = updates as any; if (Object.keys(updateData).length === 0) { const existing = await this.db .select() .from(dailyHydration) .where(eq(dailyHydration.id, id)) .limit(1) .all(); return existing.length > 0 ? this.mapDailyHydration(existing[0]) : null; } await this.db .update(dailyHydration) .set({ ...updateData, updatedAt: new Date() }) .where(eq(dailyHydration.id, id)) .run(); const results = await this.db .select() .from(dailyHydration) .where(eq(dailyHydration.id, id)) .limit(1) .all(); return results.length > 0 ? this.mapDailyHydration(results[0]) : null; } async deleteDailyHydration(id: string): Promise { const result = await this.db .delete(dailyHydration) .where(eq(dailyHydration.id, id)) .run(); return result.changes > 0; } private mapDailyHydration(row: Record): DailyHydration { const createdAtValue = row.createdAt as number | Date; const updatedAtValue = row.updatedAt as number | Date; return { id: String(row.id), userId: String(row.userId), date: String(row.date), totalWater: Number(row.totalWater || 0), waterGoal: Number(row.waterGoal || 2000), entries: row.entries ? (JSON.parse(String(row.entries)) as any) : undefined, createdAt: typeof createdAtValue === "number" ? new Date(createdAtValue * 1000) : new Date(createdAtValue), updatedAt: typeof updatedAtValue === "number" ? new Date(updatedAtValue * 1000) : new Date(updatedAtValue), }; } // ==================== FITNESS PROFILE HISTORY OPERATIONS ==================== async createFitnessProfileHistory( history: Omit, ): Promise { const id = `profile_history_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const now = new Date(); const newHistory = { id, ...history, createdAt: now, }; await this.db.insert(fitnessProfileHistory).values(newHistory as any); return this.mapFitnessProfileHistory(newHistory); } async getFitnessProfileHistory( userId: string, startDate?: Date, endDate?: Date, ): Promise { const conditions = [eq(fitnessProfileHistory.userId, userId)]; if (startDate) { const startTimestamp = Math.floor(startDate.getTime() / 1000); conditions.push( sql`${fitnessProfileHistory.changedAt} >= ${startTimestamp}`, ); } if (endDate) { const endTimestamp = Math.floor(endDate.getTime() / 1000); conditions.push( sql`${fitnessProfileHistory.changedAt} <= ${endTimestamp}`, ); } const results = await this.db .select() .from(fitnessProfileHistory) .where(and(...conditions)) .orderBy(desc(fitnessProfileHistory.changedAt)) .all(); return results.map((row) => this.mapFitnessProfileHistory(row)); } async getWeightHistory( userId: string, startDate: Date, endDate: Date, ): Promise { const startTimestamp = Math.floor(startDate.getTime() / 1000); const endTimestamp = Math.floor(endDate.getTime() / 1000); const results = await this.db .select() .from(fitnessProfileHistory) .where( and( eq(fitnessProfileHistory.userId, userId), eq(fitnessProfileHistory.changeType, "weight"), sql`${fitnessProfileHistory.changedAt} >= ${startTimestamp}`, sql`${fitnessProfileHistory.changedAt} <= ${endTimestamp}`, ), ) .orderBy(fitnessProfileHistory.changedAt) .all(); return results.map((row) => this.mapFitnessProfileHistory(row)); } private mapFitnessProfileHistory( row: Record, ): FitnessProfileHistory { const changedAtValue = row.changedAt as number | Date; const createdAtValue = row.createdAt as number | Date; return { id: String(row.id), userId: String(row.userId), fitnessProfileId: String(row.fitnessProfileId), changeType: String(row.changeType) as FitnessProfileHistory["changeType"], fieldName: String(row.fieldName), previousValue: row.previousValue ? String(row.previousValue) : undefined, newValue: row.newValue ? String(row.newValue) : undefined, changedAt: typeof changedAtValue === "number" ? new Date(changedAtValue * 1000) : new Date(changedAtValue), createdAt: typeof createdAtValue === "number" ? new Date(createdAtValue * 1000) : new Date(createdAtValue), }; } // ==================== TRAINER-CLIENT ASSIGNMENT OPERATIONS ==================== async createTrainerClientAssignment( assignment: Omit, ): Promise { const id = `assignment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const now = new Date(); const newAssignment = { id, ...assignment, createdAt: now, updatedAt: now, }; await this.db.insert(trainerClientAssignments).values(newAssignment as any); return this.mapTrainerClientAssignment(newAssignment); } async getTrainerClientAssignments( trainerId: string, ): Promise { const results = await this.db .select() .from(trainerClientAssignments) .where( and( eq(trainerClientAssignments.trainerId, trainerId), eq(trainerClientAssignments.isActive, true), ), ) .orderBy(desc(trainerClientAssignments.assignedAt)) .all(); return results.map((row) => this.mapTrainerClientAssignment(row)); } async getAllTrainerClientAssignments(): Promise { const results = await this.db .select() .from(trainerClientAssignments) .orderBy(desc(trainerClientAssignments.assignedAt)) .all(); return results.map((row) => this.mapTrainerClientAssignment(row)); } async getClientTrainerAssignment( clientId: string, ): Promise { const results = await this.db .select() .from(trainerClientAssignments) .where( and( eq(trainerClientAssignments.clientId, clientId), eq(trainerClientAssignments.isActive, true), ), ) .limit(1) .all(); return results.length > 0 ? this.mapTrainerClientAssignment(results[0]) : null; } async deleteTrainerClientAssignment(id: string): Promise { const result = await this.db .delete(trainerClientAssignments) .where(eq(trainerClientAssignments.id, id)) .run(); return result.changes > 0; } async deactivateTrainerClientAssignment( id: string, ): Promise { await this.db .update(trainerClientAssignments) .set({ isActive: false, updatedAt: new Date() }) .where(eq(trainerClientAssignments.id, id)) .run(); const results = await this.db .select() .from(trainerClientAssignments) .where(eq(trainerClientAssignments.id, id)) .limit(1) .all(); return results.length > 0 ? this.mapTrainerClientAssignment(results[0]) : null; } private mapTrainerClientAssignment( row: Record, ): TrainerClientAssignment { const assignedAtValue = row.assignedAt as number | Date; const createdAtValue = row.createdAt as number | Date; const updatedAtValue = row.updatedAt as number | Date; return { id: String(row.id), trainerId: String(row.trainerId), clientId: String(row.clientId), assignedAt: typeof assignedAtValue === "number" ? new Date(assignedAtValue * 1000) : new Date(assignedAtValue), assignedBy: row.assignedBy ? String(row.assignedBy) : undefined, isActive: Boolean(row.isActive), createdAt: typeof createdAtValue === "number" ? new Date(createdAtValue * 1000) : new Date(createdAtValue), updatedAt: typeof updatedAtValue === "number" ? new Date(updatedAtValue * 1000) : new Date(updatedAtValue), }; } // ==================== ENHANCED ATTENDANCE OPERATIONS FOR REPORTS ==================== async getAttendanceByWeek( userId: string, weekStart: Date, ): Promise { // Calculate week end (Sunday at 23:59:59) const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 6); weekEnd.setHours(23, 59, 59, 999); const startTimestamp = Math.floor(weekStart.getTime() / 1000); const endTimestamp = Math.floor(weekEnd.getTime() / 1000); const results = await this.db .select() .from(attendance) .where( and( eq(attendance.userId, userId), sql`${attendance.checkInTime} >= ${startTimestamp}`, sql`${attendance.checkInTime} <= ${endTimestamp}`, ), ) .orderBy(attendance.checkInTime) .all(); return results.map((row) => this.mapAttendance(row)); } async calculateWeeklyCheckInStats( userId: string, weekStart: Date, ): Promise<{ totalCheckIns: number; totalTimeSpent: number; avgSessionDuration: number; byType: { gym: number; class: number; personal_training: number }; }> { const attendanceRecords = await this.getAttendanceByWeek(userId, weekStart); const stats = { totalCheckIns: attendanceRecords.length, totalTimeSpent: 0, avgSessionDuration: 0, byType: { gym: 0, class: 0, personal_training: 0, }, }; let completedSessions = 0; for (const record of attendanceRecords) { // Count by type if (record.type === "gym") stats.byType.gym++; else if (record.type === "class") stats.byType.class++; else if (record.type === "personal_training") stats.byType.personal_training++; // Calculate time spent (only for completed sessions with check-out) if (record.checkOutTime) { const duration = (record.checkOutTime.getTime() - record.checkInTime.getTime()) / (1000 * 60); // in minutes stats.totalTimeSpent += duration; completedSessions++; } } // Calculate average session duration if (completedSessions > 0) { stats.avgSessionDuration = stats.totalTimeSpent / completedSessions; } return stats; } }