import Database from 'better-sqlite3' import path from 'path' import fs from 'fs' import { IDatabase, User, Client, FitnessProfile, Attendance, Recommendation, DatabaseConfig } from './types' export class SQLiteDatabase implements IDatabase { private db: Database.Database | null = null private config: DatabaseConfig constructor(config: DatabaseConfig) { this.config = config } async connect(): Promise { try { const dbPath = this.config.connection.filename || path.join(process.cwd(), 'data', 'fitai.db') // Ensure data directory exists const dataDir = path.dirname(dbPath) if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) } this.db = new Database(dbPath) // Enable foreign keys this.db.exec('PRAGMA foreign_keys = ON') // Create tables await this.createTables() if (this.config.options?.logging) { console.log('SQLite database connected successfully at:', dbPath) } } catch (error) { console.error('Failed to connect to SQLite database:', error) throw error } } async disconnect(): Promise { if (this.db) { this.db.close() this.db = null if (this.config.options?.logging) { console.log('SQLite database disconnected') } } } private async createTables(): Promise { if (!this.db) throw new Error('Database not connected') // Users table this.db.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, firstName TEXT NOT NULL, lastName TEXT NOT NULL, password TEXT NOT NULL, phone TEXT, role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')), createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL ) `) // Clients table this.db.exec(` CREATE TABLE IF NOT EXISTS clients ( id TEXT PRIMARY KEY, userId TEXT NOT NULL, membershipType TEXT NOT NULL CHECK (membershipType IN ('basic', 'premium', 'vip')), membershipStatus TEXT NOT NULL CHECK (membershipStatus IN ('active', 'inactive', 'expired')), joinDate DATETIME NOT NULL, FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE ) `) // Fitness profiles table this.db.exec(` CREATE TABLE IF NOT EXISTS fitness_profiles ( userId TEXT PRIMARY KEY, height TEXT NOT NULL, weight TEXT NOT NULL, age TEXT NOT NULL, gender TEXT NOT NULL CHECK (gender IN ('male', 'female', 'other')), activityLevel TEXT NOT NULL CHECK (activityLevel IN ('sedentary', 'light', 'moderate', 'active', 'very_active')), fitnessGoals TEXT NOT NULL, -- JSON array exerciseHabits TEXT, dietHabits TEXT, medicalConditions TEXT, allergies TEXT, injuries TEXT, createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL, FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE ) `) // Create indexes for better performance this.db.exec(` CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_clients_userId ON clients(userId); CREATE INDEX IF NOT EXISTS idx_fitness_profiles_userId ON fitness_profiles(userId); `) // Attendance table this.db.exec(` CREATE TABLE IF NOT EXISTS attendance ( id TEXT PRIMARY KEY, clientId TEXT NOT NULL, checkInTime DATETIME NOT NULL, checkOutTime DATETIME, type TEXT NOT NULL CHECK (type IN ('gym', 'class', 'personal_training')), notes TEXT, createdAt DATETIME NOT NULL, FOREIGN KEY (clientId) REFERENCES clients (id) ON DELETE CASCADE ) `) // Recommendations table this.db.exec(` CREATE TABLE IF NOT EXISTS recommendations ( id TEXT PRIMARY KEY, userId TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('short_term', 'medium_term', 'long_term')), content TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending', 'completed')), createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL, FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE ) `) this.db.exec(` CREATE INDEX IF NOT EXISTS idx_recommendations_userId ON recommendations(userId); `) this.db.exec(` CREATE INDEX IF NOT EXISTS idx_attendance_clientId ON attendance(clientId); CREATE INDEX IF NOT EXISTS idx_attendance_checkInTime ON attendance(checkInTime); `) } // User operations async createUser(userData: Omit): Promise { if (!this.db) throw new Error('Database not connected') const id = userData.id || Math.random().toString(36).substr(2, 9) const now = new Date() const user: User = { ...userData, id, createdAt: now, updatedAt: now } const stmt = this.db.prepare( `INSERT INTO users(id, email, firstName, lastName, password, phone, role, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)` ) stmt.run( user.id, user.email, user.firstName, user.lastName, user.password, user.phone, user.role, user.createdAt.toISOString(), user.updatedAt.toISOString() ) return user } async getUserById(id: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM users WHERE id = ?') const row = stmt.get(id) return row ? this.mapRowToUser(row) : null } async getUserByEmail(email: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM users WHERE email = ?') const row = stmt.get(email) return row ? this.mapRowToUser(row) : null } async getAllUsers(): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM users ORDER BY createdAt DESC') const rows = stmt.all() return rows.map(row => this.mapRowToUser(row)) } async updateUser(id: string, updates: Partial): Promise { if (!this.db) throw new Error('Database not connected') const fields = Object.keys(updates).filter(key => key !== 'id') if (fields.length === 0) return this.getUserById(id) const setClause = fields.map(field => `${field} = ?`).join(', ') const values = fields.map(field => (updates as any)[field]) values.push(new Date().toISOString()) // updatedAt values.push(id) const stmt = this.db.prepare(`UPDATE users SET ${setClause}, updatedAt = ? WHERE id = ? `) stmt.run(values) return this.getUserById(id) } async deleteUser(id: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('DELETE FROM users WHERE id = ?') const result = stmt.run(id) return (result.changes || 0) > 0 } async migrateUserId(oldId: string, newId: string): Promise { if (!this.db) throw new Error('Database not connected') // We need to disable foreign keys temporarily if we want to update ID without cascade (if cascade isn't set) // But we should try to update and let cascade handle it if possible. // Since we didn't set ON UPDATE CASCADE, we might need to manually update references or use PRAGMA. // Simplest way: Update the ID. If it fails due to FK, we have to handle it. // For the Super Admin seed case, there are no dependencies. const stmt = this.db.prepare('UPDATE users SET id = ? WHERE id = ?') stmt.run(newId, oldId) } // Client operations async createClient(clientData: Omit): Promise { if (!this.db) throw new Error('Database not connected') const id = Math.random().toString(36).substr(2, 9) const client: Client = { id, ...clientData } const stmt = this.db.prepare( `INSERT INTO clients(id, userId, membershipType, membershipStatus, joinDate) VALUES(?, ?, ?, ?, ?)` ) stmt.run( client.id, client.userId, client.membershipType, client.membershipStatus, client.joinDate.toISOString() ) return client } async getClientById(id: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM clients WHERE id = ?') const row = stmt.get(id) return row ? this.mapRowToClient(row) : null } async getClientByUserId(userId: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM clients WHERE userId = ?') const row = stmt.get(userId) return row ? this.mapRowToClient(row) : null } async getAllClients(): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM clients ORDER BY joinDate DESC') const rows = stmt.all() return rows.map(row => this.mapRowToClient(row)) } async updateClient(id: string, updates: Partial): Promise { if (!this.db) throw new Error('Database not connected') const fields = Object.keys(updates).filter(key => key !== 'id') if (fields.length === 0) return this.getClientById(id) const setClause = fields.map(field => `${field} = ?`).join(', ') const values = fields.map(field => (updates as any)[field]) values.push(id) const stmt = this.db.prepare(`UPDATE clients SET ${setClause} WHERE id = ? `) stmt.run(values) return this.getClientById(id) } async deleteClient(id: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('DELETE FROM clients WHERE id = ?') const result = stmt.run(id) return (result.changes || 0) > 0 } // Fitness Profile operations async createFitnessProfile(profileData: Omit): Promise { if (!this.db) throw new Error('Database not connected') const now = new Date() const profile: FitnessProfile = { ...profileData, createdAt: now, updatedAt: now } const stmt = this.db.prepare( `INSERT INTO fitness_profiles (userId, height, weight, age, gender, activityLevel, fitnessGoals, exerciseHabits, dietHabits, medicalConditions, allergies, injuries, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) stmt.run( profile.userId, profile.height, profile.weight, profile.age, profile.gender, profile.activityLevel, JSON.stringify(profile.fitnessGoals), profile.exerciseHabits, profile.dietHabits, profile.medicalConditions, profile.allergies, profile.injuries, profile.createdAt.toISOString(), profile.updatedAt.toISOString() ) return profile } async getFitnessProfileByUserId(userId: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM fitness_profiles WHERE userId = ?') const row = stmt.get(userId) return row ? this.mapRowToFitnessProfile(row) : null } async getAllFitnessProfiles(): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM fitness_profiles ORDER BY createdAt DESC') const rows = stmt.all() return rows.map(row => this.mapRowToFitnessProfile(row)) } async updateFitnessProfile(userId: string, updates: Partial): Promise { if (!this.db) throw new Error('Database not connected') const fields = Object.keys(updates).filter(key => key !== 'userId' && key !== 'createdAt') if (fields.length === 0) return this.getFitnessProfileByUserId(userId) const setClause = fields.map(field => `${field} = ?`).join(', ') const values = fields.map(field => { const value = (updates as any)[field] return field === 'fitnessGoals' ? JSON.stringify(value) : value }) values.push(new Date().toISOString()) // updatedAt values.push(userId) const stmt = this.db.prepare(`UPDATE fitness_profiles SET ${setClause}, updatedAt = ? WHERE userId = ? `) stmt.run(values) return this.getFitnessProfileByUserId(userId) } async deleteFitnessProfile(userId: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('DELETE FROM fitness_profiles WHERE userId = ?') const result = stmt.run(userId) return (result.changes || 0) > 0 } // Attendance operations async checkIn(clientId: string, type: 'gym' | 'class' | 'personal_training', notes?: string): Promise { if (!this.db) throw new Error('Database not connected') const id = Math.random().toString(36).substr(2, 9) const now = new Date() const attendance: Attendance = { id, clientId, checkInTime: now, type, notes, createdAt: now } const stmt = this.db.prepare( `INSERT INTO attendance(id, clientId, checkInTime, type, notes, createdAt) VALUES(?, ?, ?, ?, ?, ?)` ) stmt.run( attendance.id, attendance.clientId, attendance.checkInTime.toISOString(), attendance.type, attendance.notes, attendance.createdAt.toISOString() ) // Update client last visit this.db.prepare('UPDATE clients SET lastVisit = ? WHERE id = ?').run( now.toISOString(), clientId ) return attendance } async checkOut(attendanceId: string): Promise { if (!this.db) throw new Error('Database not connected') const now = new Date() const stmt = this.db.prepare('UPDATE attendance SET checkOutTime = ? WHERE id = ?') stmt.run(now.toISOString(), attendanceId) return this.getAttendanceById(attendanceId) } async getAttendanceById(id: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM attendance WHERE id = ?') const row = stmt.get(id) return row ? this.mapRowToAttendance(row) : null } async getAttendanceHistory(clientId: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM attendance WHERE clientId = ? ORDER BY checkInTime DESC') const rows = stmt.all(clientId) return rows.map(row => this.mapRowToAttendance(row)) } async getAllAttendance(): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM attendance ORDER BY checkInTime DESC') const rows = stmt.all() return rows.map(row => this.mapRowToAttendance(row)) } async getActiveCheckIn(clientId: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM attendance WHERE clientId = ? AND checkOutTime IS NULL ORDER BY checkInTime DESC LIMIT 1') const row = stmt.get(clientId) return row ? this.mapRowToAttendance(row) : null } // Helper methods to map database rows to entities private mapRowToUser(row: any): User { return { id: row.id, email: row.email, firstName: row.firstName, lastName: row.lastName, password: row.password, phone: row.phone, role: row.role, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt) } } private mapRowToClient(row: any): Client { return { id: row.id, userId: row.userId, membershipType: row.membershipType, membershipStatus: row.membershipStatus, joinDate: new Date(row.joinDate), lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined } } private mapRowToFitnessProfile(row: any): FitnessProfile { return { userId: row.userId, height: row.height, weight: row.weight, age: row.age, gender: row.gender, activityLevel: row.activityLevel, fitnessGoals: JSON.parse(row.fitnessGoals || '[]'), exerciseHabits: row.exerciseHabits, dietHabits: row.dietHabits, medicalConditions: row.medicalConditions, allergies: row.allergies, injuries: row.injuries, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt) } } private mapRowToAttendance(row: any): Attendance { return { id: row.id, clientId: row.clientId, checkInTime: new Date(row.checkInTime), checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined, type: row.type, notes: row.notes, createdAt: new Date(row.createdAt) } } // Recommendation operations async createRecommendation(data: Omit): Promise { if (!this.db) throw new Error('Database not connected') const now = new Date() const recommendation: Recommendation = { ...data, createdAt: now, updatedAt: now } const stmt = this.db.prepare( `INSERT INTO recommendations (id, userId, type, content, status, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?)` ) stmt.run( recommendation.id, recommendation.userId, recommendation.type, recommendation.content, recommendation.status, recommendation.createdAt.toISOString(), recommendation.updatedAt.toISOString() ) return recommendation } async getRecommendationsByUserId(userId: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('SELECT * FROM recommendations WHERE userId = ? ORDER BY createdAt DESC') const rows = stmt.all(userId) return rows.map(row => this.mapRowToRecommendation(row)) } async updateRecommendation(id: string, updates: Partial): Promise { if (!this.db) throw new Error('Database not connected') const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'userId') if (fields.length === 0) { const stmt = this.db.prepare('SELECT * FROM recommendations WHERE id = ?') const row = stmt.get(id) return row ? this.mapRowToRecommendation(row) : null } const setClause = fields.map(field => `${field} = ?`).join(', ') const values = fields.map(field => (updates as any)[field]) values.push(new Date().toISOString()) // updatedAt values.push(id) const stmt = this.db.prepare(`UPDATE recommendations SET ${setClause}, updatedAt = ? WHERE id = ?`) stmt.run(values) const getStmt = this.db.prepare('SELECT * FROM recommendations WHERE id = ?') const row = getStmt.get(id) return row ? this.mapRowToRecommendation(row) : null } async deleteRecommendation(id: string): Promise { if (!this.db) throw new Error('Database not connected') const stmt = this.db.prepare('DELETE FROM recommendations WHERE id = ?') const result = stmt.run(id) return (result.changes || 0) > 0 } private mapRowToRecommendation(row: any): Recommendation { return { id: row.id, userId: row.userId, type: row.type, content: row.content, status: row.status, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt) } } async getDashboardStats(): Promise<{ totalUsers: number; activeClients: number; totalRevenue: number; revenueGrowth: number; }> { if (!this.db) throw new Error('Database not connected') // Total Users const userCountStmt = this.db.prepare('SELECT COUNT(*) as count FROM users') const userCount = (userCountStmt.get() as any).count // Active Clients const activeClientCountStmt = this.db.prepare("SELECT COUNT(*) as count FROM clients WHERE membershipStatus = 'active'") const activeClientCount = (activeClientCountStmt.get() as any).count // Total Revenue (assuming payments table exists, handling if it's empty) // Note: We need to create the payments table first if it doesn't exist in createTables // For now, returning 0 if table doesn't exist or is empty let totalRevenue = 0 let revenueGrowth = 0 try { const revenueStmt = this.db.prepare('SELECT SUM(amount) as total FROM payments WHERE status = "completed"') const revenueResult = revenueStmt.get() as any totalRevenue = revenueResult?.total || 0 } catch (e) { // Table might not exist yet console.warn('Payments table query failed, returning 0 revenue') } return { totalUsers: userCount, activeClients: activeClientCount, totalRevenue, revenueGrowth } } }