Compare commits

...

2 Commits

Author SHA1 Message Date
d7d270510f c 2025-12-01 21:10:04 +01:00
a5c297b911 ups 2025-12-01 21:09:35 +01:00
5 changed files with 145 additions and 46 deletions

Binary file not shown.

View File

@ -20,23 +20,50 @@ export async function GET(request: NextRequest) {
const { password: _, ...userWithoutPassword } = user; const { password: _, ...userWithoutPassword } = user;
const client = await db.getClientByUserId(user.id); 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 isCheckedIn = false;
let checkInTime = null; let checkInTime = null;
let lastCheckInTime = null;
let checkInsThisWeek = 0;
let checkInsThisMonth = 0;
if (client) { // Query attendance by userId (works for all user types now)
const activeCheckIn = await db.getActiveCheckIn(client.id); const activeCheckIn = await db.getActiveCheckIn(user.id);
if (activeCheckIn) { if (activeCheckIn) {
isCheckedIn = true; isCheckedIn = true;
checkInTime = activeCheckIn.checkInTime; 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 { return {
...userWithoutPassword, ...userWithoutPassword,
client, client,
isCheckedIn, isCheckedIn,
checkInTime checkInTime,
lastCheckInTime,
checkInsThisWeek,
checkInsThisMonth
}; };
}), }),
); );

View File

@ -15,6 +15,9 @@ interface User {
createdAt: Date; createdAt: Date;
isCheckedIn?: boolean; isCheckedIn?: boolean;
checkInTime?: Date; checkInTime?: Date;
lastCheckInTime?: Date;
checkInsThisWeek?: number;
checkInsThisMonth?: number;
client?: { client?: {
id: string; id: string;
membershipType: string; membershipType: string;
@ -446,7 +449,7 @@ export function UserManagement() {
</a> </a>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-3 gap-4">
<div> <div>
<h4 className="font-medium mb-2">Basic Information</h4> <h4 className="font-medium mb-2">Basic Information</h4>
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
@ -502,6 +505,30 @@ export function UserManagement() {
</div> </div>
</div> </div>
)} )}
{selectedUser.client && (
<div>
<h4 className="font-medium mb-2">Check-In Statistics</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Last Check-In:</span>{" "}
{selectedUser.lastCheckInTime
? new Date(
selectedUser.lastCheckInTime,
).toLocaleString()
: "Never"}
</p>
<p>
<span className="font-medium">This Week:</span>{" "}
{selectedUser.checkInsThisWeek || 0} check-ins
</p>
<p>
<span className="font-medium">This Month:</span>{" "}
{selectedUser.checkInsThisMonth || 0} check-ins
</p>
</div>
</div>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -106,19 +106,61 @@ export class SQLiteDatabase implements IDatabase {
CREATE INDEX IF NOT EXISTS idx_fitness_profiles_userId ON fitness_profiles(userId); CREATE INDEX IF NOT EXISTS idx_fitness_profiles_userId ON fitness_profiles(userId);
`) `)
// Attendance table // 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(` this.db.exec(`
CREATE TABLE IF NOT EXISTS attendance ( CREATE TABLE IF NOT EXISTS attendance_new (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
clientId TEXT NOT NULL, userId TEXT NOT NULL,
checkInTime DATETIME NOT NULL, checkInTime DATETIME NOT NULL,
checkOutTime DATETIME, checkOutTime DATETIME,
type TEXT NOT NULL CHECK (type IN ('gym', 'class', 'personal_training')), type TEXT NOT NULL CHECK (type IN ('gym', 'class', 'personal_training')),
notes TEXT, notes TEXT,
createdAt DATETIME NOT NULL, createdAt DATETIME NOT NULL,
FOREIGN KEY (clientId) REFERENCES clients (id) ON DELETE CASCADE 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 // Recommendations table
// Removed DROP TABLE to persist data. Schema is now stable. // 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 (userId) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (fitnessProfileId) REFERENCES fitness_profiles (userId) FOREIGN KEY (fitnessProfileId) REFERENCES fitness_profiles (userId)
) )
`) `);
this.db.exec(` this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_recommendations_userId ON recommendations(userId); CREATE INDEX IF NOT EXISTS idx_recommendations_userId ON recommendations(userId);
`) `);
this.db.exec(` 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); CREATE INDEX IF NOT EXISTS idx_attendance_checkInTime ON attendance(checkInTime);
`) `);
} }
// User operations // User operations
@ -389,7 +431,7 @@ export class SQLiteDatabase implements IDatabase {
} }
// Attendance operations // Attendance operations
async checkIn(clientId: string, type: 'gym' | 'class' | 'personal_training', notes?: string): Promise<Attendance> { async checkIn(userId: string, type: 'gym' | 'class' | 'personal_training', notes?: string): Promise<Attendance> {
if (!this.db) throw new Error('Database not connected') if (!this.db) throw new Error('Database not connected')
const id = Math.random().toString(36).substr(2, 9) const id = Math.random().toString(36).substr(2, 9)
@ -397,7 +439,7 @@ export class SQLiteDatabase implements IDatabase {
const attendance: Attendance = { const attendance: Attendance = {
id, id,
clientId, userId,
checkInTime: now, checkInTime: now,
type, type,
notes, notes,
@ -405,20 +447,23 @@ export class SQLiteDatabase implements IDatabase {
} }
const stmt = this.db.prepare( const stmt = this.db.prepare(
`INSERT INTO attendance(id, clientId, checkInTime, type, notes, createdAt) `INSERT INTO attendance(id, userId, checkInTime, type, notes, createdAt)
VALUES(?, ?, ?, ?, ?, ?)` VALUES(?, ?, ?, ?, ?, ?)`
) )
stmt.run( stmt.run(
attendance.id, attendance.clientId, attendance.checkInTime.toISOString(), attendance.id, attendance.userId, attendance.checkInTime.toISOString(),
attendance.type, attendance.notes, attendance.createdAt.toISOString() attendance.type, attendance.notes, attendance.createdAt.toISOString()
) )
// Update client last visit // 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( this.db.prepare('UPDATE clients SET lastVisit = ? WHERE id = ?').run(
now.toISOString(), now.toISOString(),
clientId client.id
) );
}
return attendance return attendance
} }
@ -440,10 +485,10 @@ export class SQLiteDatabase implements IDatabase {
return row ? this.mapRowToAttendance(row) : null return row ? this.mapRowToAttendance(row) : null
} }
async getAttendanceHistory(clientId: string): Promise<Attendance[]> { async getAttendanceHistory(userId: string): Promise<Attendance[]> {
if (!this.db) throw new Error('Database not connected') if (!this.db) throw new Error('Database not connected')
const stmt = this.db.prepare('SELECT * FROM attendance WHERE clientId = ? ORDER BY checkInTime DESC') const stmt = this.db.prepare('SELECT * FROM attendance WHERE userId = ? ORDER BY checkInTime DESC')
const rows = stmt.all(clientId) const rows = stmt.all(userId)
return rows.map(row => this.mapRowToAttendance(row)) return rows.map(row => this.mapRowToAttendance(row))
} }
@ -454,10 +499,10 @@ export class SQLiteDatabase implements IDatabase {
return rows.map(row => this.mapRowToAttendance(row)) return rows.map(row => this.mapRowToAttendance(row))
} }
async getActiveCheckIn(clientId: string): Promise<Attendance | null> { async getActiveCheckIn(userId: string): Promise<Attendance | null> {
if (!this.db) throw new Error('Database not connected') 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 stmt = this.db.prepare('SELECT * FROM attendance WHERE userId = ? AND checkOutTime IS NULL ORDER BY checkInTime DESC LIMIT 1')
const row = stmt.get(clientId) const row = stmt.get(userId)
return row ? this.mapRowToAttendance(row) : null return row ? this.mapRowToAttendance(row) : null
} }
@ -509,7 +554,7 @@ export class SQLiteDatabase implements IDatabase {
private mapRowToAttendance(row: any): Attendance { private mapRowToAttendance(row: any): Attendance {
return { return {
id: row.id, id: row.id,
clientId: row.clientId, userId: row.userId,
checkInTime: new Date(row.checkInTime), checkInTime: new Date(row.checkInTime),
checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined, checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined,
type: row.type, type: row.type,

View File

@ -39,7 +39,7 @@ export interface FitnessProfile {
export interface Attendance { export interface Attendance {
id: string; id: string;
clientId: string; userId: string;
checkInTime: Date; checkInTime: Date;
checkOutTime?: Date; checkOutTime?: Date;
type: "gym" | "class" | "personal_training"; type: "gym" | "class" | "personal_training";
@ -121,14 +121,14 @@ export interface IDatabase {
// Attendance operations // Attendance operations
checkIn( checkIn(
clientId: string, userId: string,
type: "gym" | "class" | "personal_training", type: "gym" | "class" | "personal_training",
notes?: string, notes?: string,
): Promise<Attendance>; ): Promise<Attendance>;
checkOut(attendanceId: string): Promise<Attendance | null>; checkOut(attendanceId: string): Promise<Attendance | null>;
getAttendanceHistory(clientId: string): Promise<Attendance[]>; getAttendanceHistory(userId: string): Promise<Attendance[]>;
getAllAttendance(): Promise<Attendance[]>; getAllAttendance(): Promise<Attendance[]>;
getActiveCheckIn(clientId: string): Promise<Attendance | null>; getActiveCheckIn(userId: string): Promise<Attendance | null>;
// Recommendation operations // Recommendation operations
createRecommendation( createRecommendation(