diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts index 9cd102e..85babd7 100644 --- a/apps/admin/src/app/api/users/route.ts +++ b/apps/admin/src/app/api/users/route.ts @@ -20,23 +20,50 @@ export async function GET(request: NextRequest) { const { password: _, ...userWithoutPassword } = user; const client = await db.getClientByUserId(user.id); - // Get active check-in status + // Get active check-in status and statistics for ALL users let isCheckedIn = false; let checkInTime = null; + let lastCheckInTime = null; + let checkInsThisWeek = 0; + let checkInsThisMonth = 0; - if (client) { - const activeCheckIn = await db.getActiveCheckIn(client.id); - if (activeCheckIn) { - isCheckedIn = true; - checkInTime = activeCheckIn.checkInTime; - } + // Query attendance by userId (works for all user types now) + const activeCheckIn = await db.getActiveCheckIn(user.id); + if (activeCheckIn) { + isCheckedIn = true; + checkInTime = activeCheckIn.checkInTime; + } + + // Get attendance history for statistics + const attendanceHistory = await db.getAttendanceHistory(user.id); + + if (attendanceHistory.length > 0) { + // Last check-in is the most recent attendance record + lastCheckInTime = attendanceHistory[0].checkInTime; + + // Calculate check-ins in last 7 days + const weekAgo = new Date(); + weekAgo.setDate(weekAgo.getDate() - 7); + checkInsThisWeek = attendanceHistory.filter( + a => new Date(a.checkInTime) >= weekAgo + ).length; + + // Calculate check-ins in last 30 days + const monthAgo = new Date(); + monthAgo.setDate(monthAgo.getDate() - 30); + checkInsThisMonth = attendanceHistory.filter( + a => new Date(a.checkInTime) >= monthAgo + ).length; } return { ...userWithoutPassword, client, isCheckedIn, - checkInTime + checkInTime, + lastCheckInTime, + checkInsThisWeek, + checkInsThisMonth }; }), ); diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx index cbd0203..b1b9da8 100644 --- a/apps/admin/src/components/users/UserManagement.tsx +++ b/apps/admin/src/components/users/UserManagement.tsx @@ -15,6 +15,9 @@ interface User { createdAt: Date; isCheckedIn?: boolean; checkInTime?: Date; + lastCheckInTime?: Date; + checkInsThisWeek?: number; + checkInsThisMonth?: number; client?: { id: string; membershipType: string; @@ -446,7 +449,7 @@ export function UserManagement() { -
+

Basic Information

@@ -502,6 +505,30 @@ export function UserManagement() {
)} + + {selectedUser.client && ( +
+

Check-In Statistics

+
+

+ Last Check-In:{" "} + {selectedUser.lastCheckInTime + ? new Date( + selectedUser.lastCheckInTime, + ).toLocaleString() + : "Never"} +

+

+ This Week:{" "} + {selectedUser.checkInsThisWeek || 0} check-ins +

+

+ This Month:{" "} + {selectedUser.checkInsThisMonth || 0} check-ins +

+
+
+ )}
diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts index 06cc9ae..efc681f 100644 --- a/apps/admin/src/lib/database/sqlite.ts +++ b/apps/admin/src/lib/database/sqlite.ts @@ -106,19 +106,61 @@ export class SQLiteDatabase implements IDatabase { 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 - ) - `) + // 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. @@ -140,16 +182,16 @@ export class SQLiteDatabase implements IDatabase { 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_clientId ON attendance(clientId); + CREATE INDEX IF NOT EXISTS idx_attendance_userId ON attendance(userId); CREATE INDEX IF NOT EXISTS idx_attendance_checkInTime ON attendance(checkInTime); - `) + `); } // User operations @@ -389,7 +431,7 @@ export class SQLiteDatabase implements IDatabase { } // Attendance operations - async checkIn(clientId: string, type: 'gym' | 'class' | 'personal_training', notes?: string): Promise { + async checkIn(userId: 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) @@ -397,7 +439,7 @@ export class SQLiteDatabase implements IDatabase { const attendance: Attendance = { id, - clientId, + userId, checkInTime: now, type, notes, @@ -405,20 +447,23 @@ export class SQLiteDatabase implements IDatabase { } const stmt = this.db.prepare( - `INSERT INTO attendance(id, clientId, checkInTime, type, notes, createdAt) + `INSERT INTO attendance(id, userId, checkInTime, type, notes, createdAt) VALUES(?, ?, ?, ?, ?, ?)` ) stmt.run( - attendance.id, attendance.clientId, attendance.checkInTime.toISOString(), + attendance.id, attendance.userId, 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 - ) + // 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 } @@ -440,10 +485,10 @@ export class SQLiteDatabase implements IDatabase { return row ? this.mapRowToAttendance(row) : null } - async getAttendanceHistory(clientId: string): Promise { + async getAttendanceHistory(userId: 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) + 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)) } @@ -454,10 +499,10 @@ export class SQLiteDatabase implements IDatabase { return rows.map(row => this.mapRowToAttendance(row)) } - async getActiveCheckIn(clientId: string): Promise { + async getActiveCheckIn(userId: 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) + 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 } @@ -509,7 +554,7 @@ export class SQLiteDatabase implements IDatabase { private mapRowToAttendance(row: any): Attendance { return { id: row.id, - clientId: row.clientId, + userId: row.userId, checkInTime: new Date(row.checkInTime), checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined, type: row.type, diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts index 608ee65..6296d4f 100644 --- a/apps/admin/src/lib/database/types.ts +++ b/apps/admin/src/lib/database/types.ts @@ -39,7 +39,7 @@ export interface FitnessProfile { export interface Attendance { id: string; - clientId: string; + userId: string; checkInTime: Date; checkOutTime?: Date; type: "gym" | "class" | "personal_training"; @@ -121,14 +121,14 @@ export interface IDatabase { // Attendance operations checkIn( - clientId: string, + userId: string, type: "gym" | "class" | "personal_training", notes?: string, ): Promise; checkOut(attendanceId: string): Promise; - getAttendanceHistory(clientId: string): Promise; + getAttendanceHistory(userId: string): Promise; getAllAttendance(): Promise; - getActiveCheckIn(clientId: string): Promise; + getActiveCheckIn(userId: string): Promise; // Recommendation operations createRecommendation(