diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 3bba925..c1d83da 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/next-env.d.ts b/apps/admin/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/apps/admin/next-env.d.ts +++ b/apps/admin/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/admin/src/app/api/users/gym/route.ts b/apps/admin/src/app/api/users/gym/route.ts index 93efe00..478b81a 100644 --- a/apps/admin/src/app/api/users/gym/route.ts +++ b/apps/admin/src/app/api/users/gym/route.ts @@ -12,6 +12,7 @@ import { db, users as usersTable, eq, sql } from "@fitai/database"; export async function PATCH(req: Request) { try { const { userId } = await auth(); + console.log("PATCH /api/users/gym auth userId:", userId); if (!userId) return new NextResponse("Unauthorized", { status: 401 }); const body = await req.json().catch(() => null); @@ -23,25 +24,32 @@ export async function PATCH(req: Request) { } const gymId = body.gymId === null ? null : String(body.gymId); + console.log("PATCH /api/users/gym parsed gymId from body:", gymId); // Ensure user exists + console.log("PATCH /api/users/gym fetching user by id:", userId); const user = await db .select() .from(usersTable) .where(eq(usersTable.id, userId)) .get(); + console.log("PATCH /api/users/gym fetched user:", user); if (!user) return new NextResponse("User not found", { status: 404 }); // Validate gym when provided if (gymId) { + console.log("PATCH /api/users/gym validating gym:", gymId); const rows = await db.all( sql`SELECT status FROM gyms WHERE id = ${gymId} LIMIT 1`, ); + console.log("PATCH /api/users/gym validation query result rows:", rows); const gym = rows?.[0] as { status?: string } | undefined; if (!gym) { + console.log("PATCH /api/users/gym validation: gym not found"); return NextResponse.json({ error: "Gym not found" }, { status: 404 }); } if (gym.status !== "active") { + console.log("PATCH /api/users/gym validation: gym not active", gym); return NextResponse.json( { error: "Gym is not active" }, { status: 400 }, @@ -50,15 +58,21 @@ export async function PATCH(req: Request) { } // Update user's gym selection + console.log("PATCH /api/users/gym updating user gym_id:", { + userId, + gymId, + }); await db.run( sql`UPDATE users SET gym_id = ${gymId ?? null}, updated_at = ${new Date()} WHERE id = ${userId}`, ); + console.log("PATCH /api/users/gym update completed"); const updated = await db .select() .from(usersTable) .where(eq(usersTable.id, userId)) .get(); + console.log("PATCH /api/users/gym returning updated user:", updated); return NextResponse.json(updated); } catch (error) { console.error("PATCH /users/gym error:", error); diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts index 28aac92..74b59c9 100644 --- a/apps/admin/src/app/api/users/route.ts +++ b/apps/admin/src/app/api/users/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getDatabase } from "../../../lib/database/index"; import bcrypt from "bcryptjs"; import { auth, clerkClient } from "@clerk/nextjs/server"; +import { db as rawDb, sql } from "@fitai/database"; export async function GET(request: NextRequest) { try { @@ -11,9 +12,47 @@ export async function GET(request: NextRequest) { let users = await db.getAllUsers(); + // Hydrate gymId from raw DB to ensure consistency with writes + const rawUserRows = await rawDb.all(sql`SELECT id, gym_id FROM users`); + const gymById = new Map( + (rawUserRows || []).map((r: any) => [ + r.id as string, + (r.gym_id as string | null) ?? null, + ]), + ); + + // Load gym names for mapping gymId -> gymName + const gymRows = await rawDb.all(sql`SELECT id, name FROM gyms`); + const gymNames = new Map( + (gymRows || []) + .filter((g: any) => !!g && typeof g.id === "string") + .map((g: any) => [ + g.id as string, + (g.name as string) || (g.id as string), + ]), + ); + console.log( + "GET /api/users: total users fetched from DB:", + Array.isArray(users) ? users.length : 0, + ); + if (role) { users = users.filter((user) => user.role === role); } + console.log( + "GET /api/users: role filter:", + role, + "users after filter:", + Array.isArray(users) ? users.length : 0, + "sample:", + users && users[0] + ? { + id: users[0].id, + role: users[0].role, + gymId: (users as any)[0].gymId, + } + : null, + ); const usersWithClients = await Promise.all( users.map(async (user) => { @@ -58,6 +97,15 @@ export async function GET(request: NextRequest) { return { ...userWithoutPassword, + // Override gymId from raw DB hydration to avoid undefined from Drizzle mapping + gymId: gymById.get(user.id) ?? (user as any).gymId ?? undefined, + // Provide gymName mapped from gyms table + gymName: (() => { + const gid = + gymById.get(user.id) ?? (user as any).gymId ?? undefined; + if (!gid) return null; + return gymNames.get(gid) ?? null; + })(), client, isCheckedIn, checkInTime, @@ -68,6 +116,18 @@ export async function GET(request: NextRequest) { }), ); + console.log( + "GET /api/users: responding users count:", + Array.isArray(usersWithClients) ? usersWithClients.length : 0, + "sample:", + usersWithClients && usersWithClients[0] + ? { + id: usersWithClients[0].id, + role: usersWithClients[0].role, + gymId: (usersWithClients as any)[0].gymId, + } + : null, + ); return NextResponse.json({ users: usersWithClients }); } catch (error) { console.error("Get users error:", error); @@ -216,6 +276,15 @@ export async function PUT(request: NextRequest) { const db = await getDatabase(); const body = await request.json(); const { id, email, firstName, lastName, role, phone, gymId } = body; + console.log("PUT /api/users received body:", { + id, + email, + firstName, + lastName, + role, + phone, + gymId, + }); if (!id) { return NextResponse.json( @@ -277,6 +346,11 @@ export async function PUT(request: NextRequest) { try { const client = await clerkClient(); const publicMetadata: Record = {}; + console.log("PUT /api/users preparing Clerk metadata update:", { + targetUserId: id, + role, + gymId, + }); if (role) { publicMetadata.role = role; @@ -286,7 +360,20 @@ export async function PUT(request: NextRequest) { } if (Object.keys(publicMetadata).length > 0) { - await client.users.updateUser(id, { publicMetadata }); + console.log( + "PUT /api/users calling Clerk updateUser with metadata:", + publicMetadata, + ); + const clerkResult = await client.users.updateUser(id, { + publicMetadata, + }); + console.log("PUT /api/users Clerk updateUser result:", { + id: clerkResult.id, + role: clerkResult.publicMetadata?.role, + gymId: clerkResult.publicMetadata?.gymId, + }); + } else { + console.log("PUT /api/users no Clerk metadata changes requested"); } } catch (clerkErr: any) { console.error("Clerk metadata update error:", clerkErr); @@ -297,16 +384,43 @@ export async function PUT(request: NextRequest) { } // Update local DB for immediate UI feedback (webhook will also sync) - await db.updateUser(id, { + console.log( + "PUT /api/users raw SQL updating local DB user gym_id and fields", + ); + await rawDb.run( + sql`UPDATE users + SET email = ${email ?? existingUser.email}, + first_name = ${firstName ?? existingUser.firstName}, + last_name = ${lastName ?? existingUser.lastName}, + role = ${role ?? existingUser.role}, + phone = ${phone !== undefined && typeof phone === "string" ? phone : (existingUser.phone ?? null)}, + gym_id = ${gymId !== undefined ? gymId : (existingUser.gymId ?? null)}, + updated_at = ${Date.now()} + WHERE id = ${id}`, + ); + // Read back the updated row to surface gym_id and confirm write + const updatedRow = await rawDb.get( + sql`SELECT id, email, first_name, last_name, role, phone, gym_id, created_at, updated_at FROM users WHERE id = ${id}`, + ); + console.log("PUT /api/users raw DB row after update:", updatedRow); + + const updatedUser = { + ...existingUser, email: email ?? existingUser.email, firstName: firstName ?? existingUser.firstName, lastName: lastName ?? existingUser.lastName, role: role ?? existingUser.role, phone: phone !== undefined ? phone : existingUser.phone, - gymId: gymId !== undefined ? gymId : existingUser.gymId, - }); + gymId: + updatedRow?.gym_id !== undefined + ? updatedRow.gym_id + : gymId !== undefined + ? gymId + : existingUser.gymId, + }; + console.log("PUT /api/users responding with updated user:", updatedUser); - return NextResponse.json({ success: true }); + return NextResponse.json({ user: updatedUser }); } catch (error) { console.error("Update user error:", error); return NextResponse.json( diff --git a/apps/admin/src/components/users/UserGrid.tsx b/apps/admin/src/components/users/UserGrid.tsx index d3a23ca..d9bc6b3 100644 --- a/apps/admin/src/components/users/UserGrid.tsx +++ b/apps/admin/src/components/users/UserGrid.tsx @@ -30,6 +30,7 @@ interface User { role: string; phone?: string; gymId?: string; + gymName?: string | null; createdAt: Date; isCheckedIn?: boolean; checkInTime?: Date; @@ -143,7 +144,9 @@ export function UserGrid({ minWidth: 160, valueFormatter: (params: any) => { const gymId = params.value; - if (!gymId) return "None"; + const gymName = params.data?.gymName; + if (!gymId && !gymName) return "None"; + if (gymName) return gymName; return gymNames[gymId] || gymId; }, }, diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx index 6e2d519..bcab3ad 100644 --- a/apps/admin/src/components/users/UserManagement.tsx +++ b/apps/admin/src/components/users/UserManagement.tsx @@ -77,10 +77,33 @@ export function UserManagement() { const fetchUsers = async () => { setLoading(true); try { - const url = filter === "all" ? "/api/users" : `/api/users?role=${filter}`; + const ts = Date.now(); + const url = + filter === "all" + ? `/api/users?ts=${ts}` + : `/api/users?role=${filter}&ts=${ts}`; - const response = await fetch(url); + console.log("UserManagement.fetchUsers: fetching URL", url); + const response = await fetch(url, { cache: "no-store" }); + console.log( + "UserManagement.fetchUsers: response.ok", + response.ok, + "status", + response.status, + ); const data = await response.json(); + console.log( + "UserManagement.fetchUsers: received users count", + Array.isArray(data.users) ? data.users.length : 0, + "sample", + data.users && data.users[0] + ? { + id: data.users[0].id, + gymId: data.users[0].gymId, + role: data.users[0].role, + } + : null, + ); setUsers(data.users || []); } catch (error) { console.error("Failed to fetch users:", error); @@ -178,20 +201,80 @@ export function UserManagement() { try { if (selectedUser) { // Update existing user - const response = await fetch("/api/admin/set-user-metadata", { - method: "POST", + console.log( + "UserManagement.handleSaveEdit: sending PUT /api/users payload", + { + id: selectedUser.id, + email: editForm.email, + firstName: editForm.firstName, + lastName: editForm.lastName, + role: editForm.role, + phone: editForm.phone, + gymId: editForm.gymId === "" ? null : editForm.gymId, + }, + ); + const response = await fetch("/api/users", { + method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - targetUserId: selectedUser.id, + id: selectedUser.id, + email: editForm.email, + firstName: editForm.firstName, + lastName: editForm.lastName, role: editForm.role, + phone: editForm.phone, gymId: editForm.gymId === "" ? null : editForm.gymId, }), }); + console.log( + "UserManagement.handleSaveEdit: PUT /api/users response.ok", + response.ok, + "status", + response.status, + ); if (response.ok) { + // Optimistically update local state so grid reflects changes immediately + setUsers((prev) => + prev.map((u) => + u.id === selectedUser.id + ? { + ...u, + email: editForm.email, + firstName: editForm.firstName, + lastName: editForm.lastName, + role: editForm.role, + phone: editForm.phone || undefined, + gymId: editForm.gymId === "" ? undefined : editForm.gymId, + } + : u, + ), + ); + setSelectedUser((prev) => + prev + ? { + ...prev, + email: editForm.email, + firstName: editForm.firstName, + lastName: editForm.lastName, + role: editForm.role, + phone: editForm.phone || undefined, + gymId: editForm.gymId === "" ? undefined : editForm.gymId, + } + : prev, + ); setIsEditing(false); setEditForm(null); + // Still re-fetch from server to ensure consistency + console.log( + "UserManagement.handleSaveEdit: re-fetching users after successful edit", + ); fetchUsers(); } else { + const errText = await response.text().catch(() => ""); + console.error("UserManagement.handleSaveEdit: update failed", { + status: response.status, + body: errText, + }); alert("Error updating user"); } } else { diff --git a/apps/admin/src/lib/database/drizzle.ts b/apps/admin/src/lib/database/drizzle.ts index 40ca1eb..38cfe0e 100644 --- a/apps/admin/src/lib/database/drizzle.ts +++ b/apps/admin/src/lib/database/drizzle.ts @@ -1,38 +1,58 @@ - -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' +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 + private config: DatabaseConfig; + private db: typeof defaultDb; - constructor(config: DatabaseConfig, db?: typeof defaultDb) { - this.config = config - this.db = db || 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 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"); } + } - 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` + 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, @@ -41,13 +61,19 @@ export class DrizzleDatabase implements IDatabase { 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 ) - `) + `); - // Clients table - await this.db.run(sql` + // 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, @@ -62,10 +88,10 @@ export class DrizzleDatabase implements IDatabase { updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) - `) + `); - // Fitness profiles table - await this.db.run(sql` + // 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, @@ -84,10 +110,10 @@ export class DrizzleDatabase implements IDatabase { updated_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) - `) + `); - // Attendance table - await this.db.run(sql` + // Attendance table + await this.db.run(sql` CREATE TABLE IF NOT EXISTS attendance ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, @@ -98,10 +124,10 @@ export class DrizzleDatabase implements IDatabase { created_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) - `) + `); - // Recommendations table - await this.db.run(sql` + // Recommendations table + await this.db.run(sql` CREATE TABLE IF NOT EXISTS recommendations ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, @@ -118,10 +144,10 @@ export class DrizzleDatabase implements IDatabase { 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` + // Fitness Goals table + await this.db.run(sql` CREATE TABLE IF NOT EXISTS fitness_goals ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, @@ -144,412 +170,551 @@ export class DrizzleDatabase implements IDatabase { 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); } - // User operations - async createUser(userData: Omit & { id?: string }): Promise { - const id = userData.id || Math.random().toString(36).substr(2, 9) - const now = new Date() + await this.db + .update(fitnessProfiles) + .set({ ...dbUpdates, updatedAt: new Date() }) + .where(eq(fitnessProfiles.userId, userId)) + .run(); - const newUser = { - ...userData, - id, - createdAt: now, - updatedAt: now - } + return this.getFitnessProfileByUserId(userId); + } - await this.db.insert(users).values(newUser) - return newUser + 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 }); } - 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 + 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), + ), + ); } - 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 + 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); } - 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, - 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) - } - } + 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), + }; + } } diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index ca38de0..da96e2b 100644 --- a/apps/mobile/src/config/api.ts +++ b/apps/mobile/src/config/api.ts @@ -1,5 +1,5 @@ export const API_BASE_URL = __DEV__ - ? "https://e0877d294c41.ngrok-free.app" + ? "https://a4db649a0973.ngrok-free.app" : "https://your-production-url.com"; export const API_ENDPOINTS = { diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 81cb320..a78284e 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1,15 +1,12 @@ -import Database from 'better-sqlite3' -import { drizzle } from 'drizzle-orm/better-sqlite3' -import * as schema from './schema' +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import * as schema from "./schema"; // Configurable database path with intelligent defaults -const dbPath = process.env.DATABASE_URL || - (process.env.NODE_ENV === 'production' - ? './data/fitai.db' - : '../../apps/admin/data/fitai.db') +const dbPath = "./data/fitai.db"; -const sqlite = new Database(dbPath) -export const db = drizzle(sqlite, { schema }) +const sqlite = new Database(dbPath); +export const db = drizzle(sqlite, { schema }); -export * from './schema' -export { eq, and, or, desc, asc, sql } from 'drizzle-orm' \ No newline at end of file +export * from "./schema"; +export { eq, and, or, desc, asc, sql } from "drizzle-orm";