import { IDatabase, User, Client, FitnessProfile, Attendance, Recommendation, FitnessGoal, DatabaseConfig, } from "./types"; import { db as defaultDb, users, clients, fitnessProfiles, attendance, recommendations, fitnessGoals, eq, and, desc, sql, } from "@fitai/database"; import { InferSelectModel } from "drizzle-orm"; 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) { console.log("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) { console.log("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(); } // 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 getAllClients(): Promise { const results = await this.db .select() .from(clients) .orderBy(desc(clients.joinDate)) .all(); return results.map(this.mapClient); } 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); } 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; } // 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; } // 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 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 private mapUser(row: any): User { return { ...row, gymId: (row as any).gymId ?? (row as any).gym_id ?? undefined, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), }; } private mapClient(row: any): Client { return { ...row, joinDate: new Date(row.joinDate), lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined, }; } private mapFitnessProfile(row: any): FitnessProfile { return { ...row, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), }; } private mapAttendance(row: any): Attendance { return { ...row, checkInTime: new Date(row.checkInTime), checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined, createdAt: new Date(row.createdAt), }; } private mapRecommendation(row: any): Recommendation { return { ...row, createdAt: new Date(row.createdAt), approvedAt: row.approvedAt ? new Date(row.approvedAt) : undefined, }; } private mapFitnessGoal(row: any): FitnessGoal { return { ...row, startDate: new Date(row.startDate), targetDate: row.targetDate ? new Date(row.targetDate) : undefined, completedDate: row.completedDate ? new Date(row.completedDate) : undefined, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), }; } }