868 lines
29 KiB
TypeScript
868 lines
29 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
if (this.db) {
|
|
this.db.close()
|
|
this.db = null
|
|
if (this.config.options?.logging) {
|
|
console.log('SQLite database disconnected')
|
|
}
|
|
}
|
|
}
|
|
|
|
private async createTables(): Promise<void> {
|
|
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 migration: change from clientId to userId
|
|
// Check if old table exists and migrate
|
|
const tableInfo = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='attendance'").get() as any;
|
|
|
|
if (tableInfo) {
|
|
// Check if table has clientId column (old schema)
|
|
const columns = this.db.prepare("PRAGMA table_info(attendance)").all() as any[];
|
|
const hasClientId = columns.some((col: any) => col.name === 'clientId');
|
|
|
|
if (hasClientId) {
|
|
console.log('Migrating attendance table from clientId to userId...');
|
|
|
|
// Create new table with userId
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS attendance_new (
|
|
id TEXT PRIMARY KEY,
|
|
userId 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 (userId) REFERENCES users (id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// Migrate data: map clientId to userId via clients table
|
|
this.db.exec(`
|
|
INSERT INTO attendance_new (id, userId, checkInTime, checkOutTime, type, notes, createdAt)
|
|
SELECT a.id, c.userId, a.checkInTime, a.checkOutTime, a.type, a.notes, a.createdAt
|
|
FROM attendance a
|
|
JOIN clients c ON a.clientId = c.id
|
|
`);
|
|
|
|
// Drop old table and rename new one
|
|
this.db.exec(`DROP TABLE attendance`);
|
|
this.db.exec(`ALTER TABLE attendance_new RENAME TO attendance`);
|
|
|
|
console.log('Attendance table migration completed');
|
|
}
|
|
} else {
|
|
// Create new attendance table with userId
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS attendance (
|
|
id TEXT PRIMARY KEY,
|
|
userId 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 (userId) REFERENCES users (id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
}
|
|
|
|
// Recommendations table
|
|
// Removed DROP TABLE to persist data. Schema is now stable.
|
|
// this.db.exec(`DROP TABLE IF EXISTS recommendations`)
|
|
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS recommendations (
|
|
id TEXT PRIMARY KEY,
|
|
userId TEXT NOT NULL,
|
|
fitnessProfileId TEXT,
|
|
type TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
activityPlan TEXT,
|
|
dietPlan TEXT,
|
|
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected', 'completed')),
|
|
createdAt DATETIME NOT NULL,
|
|
approvedAt DATETIME,
|
|
approvedBy TEXT,
|
|
FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE,
|
|
FOREIGN KEY (fitnessProfileId) REFERENCES fitness_profiles (userId)
|
|
)
|
|
`);
|
|
|
|
this.db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_recommendations_userId ON recommendations(userId);
|
|
`);
|
|
|
|
this.db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_attendance_userId ON attendance(userId);
|
|
CREATE INDEX IF NOT EXISTS idx_attendance_checkInTime ON attendance(checkInTime);
|
|
`);
|
|
}
|
|
|
|
// User operations
|
|
async createUser(
|
|
userData: Omit<User, "createdAt" | "updatedAt" | "id"> & { id?: string },
|
|
): Promise<User> {
|
|
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<User | null> {
|
|
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<User | null> {
|
|
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<User[]> {
|
|
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<User>): Promise<User | null> {
|
|
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<boolean> {
|
|
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<void> {
|
|
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<Client, 'id'>): Promise<Client> {
|
|
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<Client | null> {
|
|
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<Client | null> {
|
|
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<Client[]> {
|
|
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<Client>): Promise<Client | null> {
|
|
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<boolean> {
|
|
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<FitnessProfile, 'createdAt' | 'updatedAt'>): Promise<FitnessProfile> {
|
|
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<FitnessProfile | null> {
|
|
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<FitnessProfile[]> {
|
|
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<FitnessProfile>): Promise<FitnessProfile | null> {
|
|
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<boolean> {
|
|
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(userId: string, type: 'gym' | 'class' | 'personal_training', notes?: string): Promise<Attendance> {
|
|
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,
|
|
userId,
|
|
checkInTime: now,
|
|
type,
|
|
notes,
|
|
createdAt: now
|
|
}
|
|
|
|
const stmt = this.db.prepare(
|
|
`INSERT INTO attendance(id, userId, checkInTime, type, notes, createdAt)
|
|
VALUES(?, ?, ?, ?, ?, ?)`
|
|
)
|
|
|
|
stmt.run(
|
|
attendance.id, attendance.userId, attendance.checkInTime.toISOString(),
|
|
attendance.type, attendance.notes, attendance.createdAt.toISOString()
|
|
)
|
|
|
|
// Update client last visit if user is a client
|
|
const client = await this.getClientByUserId(userId);
|
|
if (client) {
|
|
this.db.prepare('UPDATE clients SET lastVisit = ? WHERE id = ?').run(
|
|
now.toISOString(),
|
|
client.id
|
|
);
|
|
}
|
|
|
|
return attendance
|
|
}
|
|
|
|
async checkOut(attendanceId: string): Promise<Attendance | null> {
|
|
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<Attendance | null> {
|
|
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(userId: string): Promise<Attendance[]> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
const stmt = this.db.prepare('SELECT * FROM attendance WHERE userId = ? ORDER BY checkInTime DESC')
|
|
const rows = stmt.all(userId)
|
|
return rows.map(row => this.mapRowToAttendance(row))
|
|
}
|
|
|
|
async getAllAttendance(): Promise<Attendance[]> {
|
|
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(userId: string): Promise<Attendance | null> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
const stmt = this.db.prepare('SELECT * FROM attendance WHERE userId = ? AND checkOutTime IS NULL ORDER BY checkInTime DESC LIMIT 1')
|
|
const row = stmt.get(userId)
|
|
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,
|
|
userId: row.userId,
|
|
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<Recommendation, 'createdAt' | 'approvedAt' | 'approvedBy'>): Promise<Recommendation> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const now = new Date()
|
|
const recommendation: Recommendation = {
|
|
...data,
|
|
createdAt: now,
|
|
status: data.status || 'pending'
|
|
}
|
|
|
|
const stmt = this.db.prepare(
|
|
`INSERT INTO recommendations (
|
|
id, userId, fitnessProfileId, type, content,
|
|
activityPlan, dietPlan, status, createdAt
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
)
|
|
|
|
stmt.run(
|
|
recommendation.id, recommendation.userId, recommendation.fitnessProfileId,
|
|
recommendation.type, recommendation.content, recommendation.activityPlan,
|
|
recommendation.dietPlan, recommendation.status,
|
|
recommendation.createdAt.toISOString()
|
|
)
|
|
|
|
return recommendation
|
|
}
|
|
|
|
async getRecommendationsByUserId(userId: string): Promise<Recommendation[]> {
|
|
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 getAllRecommendations(): Promise<Recommendation[]> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const stmt = this.db.prepare('SELECT * FROM recommendations ORDER BY createdAt DESC')
|
|
const rows = stmt.all()
|
|
|
|
return rows.map(row => this.mapRowToRecommendation(row))
|
|
}
|
|
|
|
async updateRecommendation(id: string, updates: Partial<Recommendation>): Promise<Recommendation | null> {
|
|
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 => {
|
|
const val = (updates as any)[field]
|
|
return val instanceof Date ? val.toISOString() : val
|
|
})
|
|
values.push(id)
|
|
|
|
const stmt = this.db.prepare(`UPDATE recommendations SET ${setClause} 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<boolean> {
|
|
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,
|
|
fitnessProfileId: row.fitnessProfileId,
|
|
type: row.type,
|
|
content: row.content,
|
|
activityPlan: row.activityPlan,
|
|
dietPlan: row.dietPlan,
|
|
status: row.status,
|
|
createdAt: new Date(row.createdAt),
|
|
approvedAt: row.approvedAt ? new Date(row.approvedAt) : undefined,
|
|
approvedBy: row.approvedBy
|
|
}
|
|
}
|
|
|
|
// Fitness Goals operations
|
|
async createFitnessGoal(goalData: Omit<import('./types').FitnessGoal, 'createdAt' | 'updatedAt'>): Promise<import('./types').FitnessGoal> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const now = new Date()
|
|
const goal: import('./types').FitnessGoal = {
|
|
...goalData,
|
|
createdAt: now,
|
|
updatedAt: now
|
|
}
|
|
|
|
const stmt = this.db.prepare(
|
|
`INSERT INTO fitness_goals (
|
|
id, user_id, fitness_profile_id, goal_type, title, description,
|
|
target_value, current_value, unit, start_date, target_date,
|
|
completed_date, status, progress, priority, notes, created_at, updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
)
|
|
|
|
stmt.run(
|
|
goal.id,
|
|
goal.userId,
|
|
goal.fitnessProfileId || null,
|
|
goal.goalType,
|
|
goal.title,
|
|
goal.description || null,
|
|
goal.targetValue || null,
|
|
goal.currentValue || null,
|
|
goal.unit || null,
|
|
goal.startDate.toISOString(),
|
|
goal.targetDate ? goal.targetDate.toISOString() : null,
|
|
goal.completedDate ? goal.completedDate.toISOString() : null,
|
|
goal.status,
|
|
goal.progress,
|
|
goal.priority,
|
|
goal.notes || null,
|
|
goal.createdAt.toISOString(),
|
|
goal.updatedAt.toISOString()
|
|
)
|
|
|
|
return goal
|
|
}
|
|
|
|
async getFitnessGoalById(id: string): Promise<import('./types').FitnessGoal | null> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const stmt = this.db.prepare('SELECT * FROM fitness_goals WHERE id = ?')
|
|
const row = stmt.get(id)
|
|
|
|
return row ? this.mapRowToFitnessGoal(row) : null
|
|
}
|
|
|
|
async getFitnessGoalsByUserId(userId: string, status?: string): Promise<import('./types').FitnessGoal[]> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
let query = 'SELECT * FROM fitness_goals WHERE user_id = ?'
|
|
const params: any[] = [userId]
|
|
|
|
if (status) {
|
|
query += ' AND status = ?'
|
|
params.push(status)
|
|
}
|
|
|
|
query += ' ORDER BY created_at DESC'
|
|
|
|
const stmt = this.db.prepare(query)
|
|
const rows = stmt.all(...params)
|
|
|
|
return rows.map(row => this.mapRowToFitnessGoal(row))
|
|
}
|
|
|
|
async updateFitnessGoal(id: string, updates: Partial<import('./types').FitnessGoal>): Promise<import('./types').FitnessGoal | null> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'createdAt')
|
|
if (fields.length === 0) return this.getFitnessGoalById(id)
|
|
|
|
// Map camelCase to snake_case for database columns
|
|
const columnMap: Record<string, string> = {
|
|
userId: 'user_id',
|
|
fitnessProfileId: 'fitness_profile_id',
|
|
goalType: 'goal_type',
|
|
targetValue: 'target_value',
|
|
currentValue: 'current_value',
|
|
startDate: 'start_date',
|
|
targetDate: 'target_date',
|
|
completedDate: 'completed_date',
|
|
updatedAt: 'updated_at'
|
|
}
|
|
|
|
const setClause = fields.map(field => `${columnMap[field] || field} = ?`).join(', ')
|
|
const values = fields.map(field => {
|
|
const val = (updates as any)[field]
|
|
return val instanceof Date ? val.toISOString() : val
|
|
})
|
|
values.push(new Date().toISOString()) // updatedAt
|
|
values.push(id)
|
|
|
|
const stmt = this.db.prepare(`UPDATE fitness_goals SET ${setClause}, updated_at = ? WHERE id = ?`)
|
|
stmt.run(values)
|
|
|
|
return this.getFitnessGoalById(id)
|
|
}
|
|
|
|
async deleteFitnessGoal(id: string): Promise<boolean> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const stmt = this.db.prepare('DELETE FROM fitness_goals WHERE id = ?')
|
|
const result = stmt.run(id)
|
|
return (result.changes || 0) > 0
|
|
}
|
|
|
|
async updateGoalProgress(id: string, currentValue: number): Promise<import('./types').FitnessGoal | null> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
// Get the goal to calculate progress
|
|
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)
|
|
}
|
|
|
|
const stmt = this.db.prepare(
|
|
'UPDATE fitness_goals SET current_value = ?, progress = ?, updated_at = ? WHERE id = ?'
|
|
)
|
|
stmt.run(currentValue, progress, new Date().toISOString(), id)
|
|
|
|
return this.getFitnessGoalById(id)
|
|
}
|
|
|
|
async completeGoal(id: string): Promise<import('./types').FitnessGoal | null> {
|
|
if (!this.db) throw new Error('Database not connected')
|
|
|
|
const now = new Date()
|
|
const stmt = this.db.prepare(
|
|
'UPDATE fitness_goals SET status = ?, progress = ?, completed_date = ?, updated_at = ? WHERE id = ?'
|
|
)
|
|
stmt.run('completed', 100, now.toISOString(), now.toISOString(), id)
|
|
|
|
return this.getFitnessGoalById(id)
|
|
}
|
|
|
|
private mapRowToFitnessGoal(row: any): import('./types').FitnessGoal {
|
|
return {
|
|
id: row.id,
|
|
userId: row.user_id,
|
|
fitnessProfileId: row.fitness_profile_id,
|
|
goalType: row.goal_type,
|
|
title: row.title,
|
|
description: row.description,
|
|
targetValue: row.target_value,
|
|
currentValue: row.current_value,
|
|
unit: row.unit,
|
|
startDate: new Date(row.start_date),
|
|
targetDate: row.target_date ? new Date(row.target_date) : undefined,
|
|
completedDate: row.completed_date ? new Date(row.completed_date) : undefined,
|
|
status: row.status,
|
|
progress: row.progress,
|
|
priority: row.priority,
|
|
notes: row.notes,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
} |