now gym in UI displays properly

This commit is contained in:
echo 2025-12-18 19:14:40 +01:00
parent 6580564767
commit 339d798a88
9 changed files with 841 additions and 465 deletions

Binary file not shown.

View File

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -12,6 +12,7 @@ import { db, users as usersTable, eq, sql } from "@fitai/database";
export async function PATCH(req: Request) { export async function PATCH(req: Request) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
console.log("PATCH /api/users/gym auth userId:", userId);
if (!userId) return new NextResponse("Unauthorized", { status: 401 }); if (!userId) return new NextResponse("Unauthorized", { status: 401 });
const body = await req.json().catch(() => null); 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); const gymId = body.gymId === null ? null : String(body.gymId);
console.log("PATCH /api/users/gym parsed gymId from body:", gymId);
// Ensure user exists // Ensure user exists
console.log("PATCH /api/users/gym fetching user by id:", userId);
const user = await db const user = await db
.select() .select()
.from(usersTable) .from(usersTable)
.where(eq(usersTable.id, userId)) .where(eq(usersTable.id, userId))
.get(); .get();
console.log("PATCH /api/users/gym fetched user:", user);
if (!user) return new NextResponse("User not found", { status: 404 }); if (!user) return new NextResponse("User not found", { status: 404 });
// Validate gym when provided // Validate gym when provided
if (gymId) { if (gymId) {
console.log("PATCH /api/users/gym validating gym:", gymId);
const rows = await db.all( const rows = await db.all(
sql`SELECT status FROM gyms WHERE id = ${gymId} LIMIT 1`, 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; const gym = rows?.[0] as { status?: string } | undefined;
if (!gym) { if (!gym) {
console.log("PATCH /api/users/gym validation: gym not found");
return NextResponse.json({ error: "Gym not found" }, { status: 404 }); return NextResponse.json({ error: "Gym not found" }, { status: 404 });
} }
if (gym.status !== "active") { if (gym.status !== "active") {
console.log("PATCH /api/users/gym validation: gym not active", gym);
return NextResponse.json( return NextResponse.json(
{ error: "Gym is not active" }, { error: "Gym is not active" },
{ status: 400 }, { status: 400 },
@ -50,15 +58,21 @@ export async function PATCH(req: Request) {
} }
// Update user's gym selection // Update user's gym selection
console.log("PATCH /api/users/gym updating user gym_id:", {
userId,
gymId,
});
await db.run( await db.run(
sql`UPDATE users SET gym_id = ${gymId ?? null}, updated_at = ${new Date()} WHERE id = ${userId}`, 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 const updated = await db
.select() .select()
.from(usersTable) .from(usersTable)
.where(eq(usersTable.id, userId)) .where(eq(usersTable.id, userId))
.get(); .get();
console.log("PATCH /api/users/gym returning updated user:", updated);
return NextResponse.json(updated); return NextResponse.json(updated);
} catch (error) { } catch (error) {
console.error("PATCH /users/gym error:", error); console.error("PATCH /users/gym error:", error);

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from "../../../lib/database/index"; import { getDatabase } from "../../../lib/database/index";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { auth, clerkClient } from "@clerk/nextjs/server"; import { auth, clerkClient } from "@clerk/nextjs/server";
import { db as rawDb, sql } from "@fitai/database";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@ -11,9 +12,47 @@ export async function GET(request: NextRequest) {
let users = await db.getAllUsers(); 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<string, string | null>(
(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<string, string>(
(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) { if (role) {
users = users.filter((user) => user.role === 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( const usersWithClients = await Promise.all(
users.map(async (user) => { users.map(async (user) => {
@ -58,6 +97,15 @@ export async function GET(request: NextRequest) {
return { return {
...userWithoutPassword, ...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, client,
isCheckedIn, isCheckedIn,
checkInTime, 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 }); return NextResponse.json({ users: usersWithClients });
} catch (error) { } catch (error) {
console.error("Get users error:", error); console.error("Get users error:", error);
@ -216,6 +276,15 @@ export async function PUT(request: NextRequest) {
const db = await getDatabase(); const db = await getDatabase();
const body = await request.json(); const body = await request.json();
const { id, email, firstName, lastName, role, phone, gymId } = body; 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) { if (!id) {
return NextResponse.json( return NextResponse.json(
@ -277,6 +346,11 @@ export async function PUT(request: NextRequest) {
try { try {
const client = await clerkClient(); const client = await clerkClient();
const publicMetadata: Record<string, unknown> = {}; const publicMetadata: Record<string, unknown> = {};
console.log("PUT /api/users preparing Clerk metadata update:", {
targetUserId: id,
role,
gymId,
});
if (role) { if (role) {
publicMetadata.role = role; publicMetadata.role = role;
@ -286,7 +360,20 @@ export async function PUT(request: NextRequest) {
} }
if (Object.keys(publicMetadata).length > 0) { 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) { } catch (clerkErr: any) {
console.error("Clerk metadata update error:", clerkErr); 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) // 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, email: email ?? existingUser.email,
firstName: firstName ?? existingUser.firstName, firstName: firstName ?? existingUser.firstName,
lastName: lastName ?? existingUser.lastName, lastName: lastName ?? existingUser.lastName,
role: role ?? existingUser.role, role: role ?? existingUser.role,
phone: phone !== undefined ? phone : existingUser.phone, 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) { } catch (error) {
console.error("Update user error:", error); console.error("Update user error:", error);
return NextResponse.json( return NextResponse.json(

View File

@ -30,6 +30,7 @@ interface User {
role: string; role: string;
phone?: string; phone?: string;
gymId?: string; gymId?: string;
gymName?: string | null;
createdAt: Date; createdAt: Date;
isCheckedIn?: boolean; isCheckedIn?: boolean;
checkInTime?: Date; checkInTime?: Date;
@ -143,7 +144,9 @@ export function UserGrid({
minWidth: 160, minWidth: 160,
valueFormatter: (params: any) => { valueFormatter: (params: any) => {
const gymId = params.value; 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; return gymNames[gymId] || gymId;
}, },
}, },

View File

@ -77,10 +77,33 @@ export function UserManagement() {
const fetchUsers = async () => { const fetchUsers = async () => {
setLoading(true); setLoading(true);
try { 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(); 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 || []); setUsers(data.users || []);
} catch (error) { } catch (error) {
console.error("Failed to fetch users:", error); console.error("Failed to fetch users:", error);
@ -178,20 +201,80 @@ export function UserManagement() {
try { try {
if (selectedUser) { if (selectedUser) {
// Update existing user // Update existing user
const response = await fetch("/api/admin/set-user-metadata", { console.log(
method: "POST", "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" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
targetUserId: selectedUser.id, id: selectedUser.id,
email: editForm.email,
firstName: editForm.firstName,
lastName: editForm.lastName,
role: editForm.role, role: editForm.role,
phone: editForm.phone,
gymId: editForm.gymId === "" ? null : editForm.gymId, gymId: editForm.gymId === "" ? null : editForm.gymId,
}), }),
}); });
console.log(
"UserManagement.handleSaveEdit: PUT /api/users response.ok",
response.ok,
"status",
response.status,
);
if (response.ok) { 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); setIsEditing(false);
setEditForm(null); setEditForm(null);
// Still re-fetch from server to ensure consistency
console.log(
"UserManagement.handleSaveEdit: re-fetching users after successful edit",
);
fetchUsers(); fetchUsers();
} else { } else {
const errText = await response.text().catch(() => "");
console.error("UserManagement.handleSaveEdit: update failed", {
status: response.status,
body: errText,
});
alert("Error updating user"); alert("Error updating user");
} }
} else { } else {

View File

@ -1,24 +1,44 @@
import {
import { IDatabase, User, Client, FitnessProfile, Attendance, Recommendation, FitnessGoal, DatabaseConfig } from './types' IDatabase,
import { db as defaultDb, users, clients, fitnessProfiles, attendance, recommendations, fitnessGoals, eq, and, desc, sql } from '@fitai/database' User,
import { InferSelectModel } from 'drizzle-orm' 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 { export class DrizzleDatabase implements IDatabase {
private config: DatabaseConfig private config: DatabaseConfig;
private db: typeof defaultDb private db: typeof defaultDb;
constructor(config: DatabaseConfig, db?: typeof defaultDb) { constructor(config: DatabaseConfig, db?: typeof defaultDb) {
this.config = config this.config = config;
this.db = db || defaultDb this.db = db || defaultDb;
} }
async connect(): Promise<void> { async connect(): Promise<void> {
// Drizzle with better-sqlite3 connects synchronously on initialization // Drizzle with better-sqlite3 connects synchronously on initialization
// We can just log here if needed // We can just log here if needed
if (this.config.options?.logging) { if (this.config.options?.logging) {
console.log('Drizzle database connected') console.log("Drizzle database connected");
} }
await this.createTables() await this.createTables();
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
@ -26,7 +46,7 @@ export class DrizzleDatabase implements IDatabase {
// but we can close the underlying sqlite instance if we had access to it. // but we can close the underlying sqlite instance if we had access to it.
// For now, we'll assume it's handled. // For now, we'll assume it's handled.
if (this.config.options?.logging) { if (this.config.options?.logging) {
console.log('Drizzle database disconnected') console.log("Drizzle database disconnected");
} }
} }
@ -41,11 +61,17 @@ export class DrizzleDatabase implements IDatabase {
password TEXT, password TEXT,
phone TEXT, phone TEXT,
role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')), role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')),
gym_id TEXT,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL updated_at INTEGER NOT NULL
) )
`) `);
// 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 // Clients table
await this.db.run(sql` await this.db.run(sql`
CREATE TABLE IF NOT EXISTS clients ( CREATE TABLE IF NOT EXISTS clients (
@ -62,7 +88,7 @@ export class DrizzleDatabase implements IDatabase {
updated_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) )
`) `);
// Fitness profiles table // Fitness profiles table
await this.db.run(sql` await this.db.run(sql`
@ -84,7 +110,7 @@ export class DrizzleDatabase implements IDatabase {
updated_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) )
`) `);
// Attendance table // Attendance table
await this.db.run(sql` await this.db.run(sql`
@ -98,7 +124,7 @@ export class DrizzleDatabase implements IDatabase {
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) )
`) `);
// Recommendations table // Recommendations table
await this.db.run(sql` await this.db.run(sql`
@ -118,7 +144,7 @@ export class DrizzleDatabase implements IDatabase {
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE
) )
`) `);
// Fitness Goals table // Fitness Goals table
await this.db.run(sql` await this.db.run(sql`
@ -144,158 +170,220 @@ export class DrizzleDatabase implements IDatabase {
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE
) )
`) `);
} }
// User operations // User operations
async createUser(userData: Omit<User, "createdAt" | "updatedAt" | "id"> & { id?: string }): Promise<User> { async createUser(
const id = userData.id || Math.random().toString(36).substr(2, 9) userData: Omit<User, "createdAt" | "updatedAt" | "id"> & { id?: string },
const now = new Date() ): Promise<User> {
const id = userData.id || Math.random().toString(36).substr(2, 9);
const now = new Date();
const newUser = { const newUser = {
...userData, ...userData,
id, id,
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now,
} };
await this.db.insert(users).values(newUser) await this.db.insert(users).values(newUser);
return newUser return newUser;
} }
async getUserById(id: string): Promise<User | null> { async getUserById(id: string): Promise<User | null> {
const result = await this.db.select().from(users).where(eq(users.id, id)).get() const result = await this.db
return result ? this.mapUser(result) : null .select()
.from(users)
.where(eq(users.id, id))
.get();
return result ? this.mapUser(result) : null;
} }
async getUserByEmail(email: string): Promise<User | null> { async getUserByEmail(email: string): Promise<User | null> {
const result = await this.db.select().from(users).where(eq(users.email, email)).get() const result = await this.db
return result ? this.mapUser(result) : null .select()
.from(users)
.where(eq(users.email, email))
.get();
return result ? this.mapUser(result) : null;
} }
async getAllUsers(): Promise<User[]> { async getAllUsers(): Promise<User[]> {
const results = await this.db.select().from(users).orderBy(desc(users.createdAt)).all() const results = await this.db
return results.map(this.mapUser) .select()
.from(users)
.orderBy(desc(users.createdAt))
.all();
return results.map(this.mapUser);
} }
async updateUser(id: string, updates: Partial<User>): Promise<User | null> { async updateUser(id: string, updates: Partial<User>): Promise<User | null> {
const { id: _, ...updateData } = updates const { id: _, ...updateData } = updates;
if (Object.keys(updateData).length === 0) return this.getUserById(id) if (Object.keys(updateData).length === 0) return this.getUserById(id);
await this.db.update(users) await this.db
.update(users)
.set({ ...updateData, updatedAt: new Date() }) .set({ ...updateData, updatedAt: new Date() })
.where(eq(users.id, id)) .where(eq(users.id, id))
.run() .run();
return this.getUserById(id) return this.getUserById(id);
} }
async deleteUser(id: string): Promise<boolean> { async deleteUser(id: string): Promise<boolean> {
const result = await this.db.delete(users).where(eq(users.id, id)).run() const result = await this.db.delete(users).where(eq(users.id, id)).run();
return result.changes > 0 return result.changes > 0;
} }
async migrateUserId(oldId: string, newId: string): Promise<void> { async migrateUserId(oldId: string, newId: string): Promise<void> {
await this.db.update(users).set({ id: newId }).where(eq(users.id, oldId)).run() await this.db
.update(users)
.set({ id: newId })
.where(eq(users.id, oldId))
.run();
} }
// Client operations // Client operations
async createClient(clientData: Omit<Client, 'id'>): Promise<Client> { async createClient(clientData: Omit<Client, "id">): Promise<Client> {
const id = Math.random().toString(36).substr(2, 9) const id = Math.random().toString(36).substr(2, 9);
const newClient = { const newClient = {
id, id,
...clientData, ...clientData,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} };
await this.db.insert(clients).values(newClient as any) await this.db.insert(clients).values(newClient as any);
return this.mapClient(newClient) return this.mapClient(newClient);
} }
async getClientById(id: string): Promise<Client | null> { async getClientById(id: string): Promise<Client | null> {
const result = await this.db.select().from(clients).where(eq(clients.id, id)).get() const result = await this.db
return result ? this.mapClient(result) : null .select()
.from(clients)
.where(eq(clients.id, id))
.get();
return result ? this.mapClient(result) : null;
} }
async getClientByUserId(userId: string): Promise<Client | null> { async getClientByUserId(userId: string): Promise<Client | null> {
const result = await this.db.select().from(clients).where(eq(clients.userId, userId)).get() const result = await this.db
return result ? this.mapClient(result) : null .select()
.from(clients)
.where(eq(clients.userId, userId))
.get();
return result ? this.mapClient(result) : null;
} }
async getAllClients(): Promise<Client[]> { async getAllClients(): Promise<Client[]> {
const results = await this.db.select().from(clients).orderBy(desc(clients.joinDate)).all() const results = await this.db
return results.map(this.mapClient) .select()
.from(clients)
.orderBy(desc(clients.joinDate))
.all();
return results.map(this.mapClient);
} }
async updateClient(id: string, updates: Partial<Client>): Promise<Client | null> { async updateClient(
const { id: _, ...updateData } = updates id: string,
if (Object.keys(updateData).length === 0) return this.getClientById(id) updates: Partial<Client>,
): Promise<Client | null> {
const { id: _, ...updateData } = updates;
if (Object.keys(updateData).length === 0) return this.getClientById(id);
await this.db.update(clients) await this.db
.update(clients)
.set({ ...updateData, updatedAt: new Date() } as any) .set({ ...updateData, updatedAt: new Date() } as any)
.where(eq(clients.id, id)) .where(eq(clients.id, id))
.run() .run();
return this.getClientById(id) return this.getClientById(id);
} }
async deleteClient(id: string): Promise<boolean> { async deleteClient(id: string): Promise<boolean> {
const result = await this.db.delete(clients).where(eq(clients.id, id)).run() const result = await this.db
return result.changes > 0 .delete(clients)
.where(eq(clients.id, id))
.run();
return result.changes > 0;
} }
// Fitness Profile operations // Fitness Profile operations
async createFitnessProfile(profileData: Omit<FitnessProfile, 'id' | 'createdAt' | 'updatedAt'>): Promise<FitnessProfile> { async createFitnessProfile(
const now = new Date() profileData: Omit<FitnessProfile, "id" | "createdAt" | "updatedAt">,
const id = Math.random().toString(36).substr(2, 9) ): Promise<FitnessProfile> {
const now = new Date();
const id = Math.random().toString(36).substr(2, 9);
const newProfile = { const newProfile = {
id, id,
...profileData, ...profileData,
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now,
};
await this.db.insert(fitnessProfiles).values(newProfile as any);
return { id, ...profileData, createdAt: now, updatedAt: now };
} }
await this.db.insert(fitnessProfiles).values(newProfile as any) async getFitnessProfileByUserId(
return { id, ...profileData, createdAt: now, updatedAt: now } userId: string,
} ): Promise<FitnessProfile | null> {
const result = await this.db
async getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null> { .select()
const result = await this.db.select().from(fitnessProfiles).where(eq(fitnessProfiles.userId, userId)).get() .from(fitnessProfiles)
return result ? this.mapFitnessProfile(result) : null .where(eq(fitnessProfiles.userId, userId))
.get();
return result ? this.mapFitnessProfile(result) : null;
} }
async getAllFitnessProfiles(): Promise<FitnessProfile[]> { async getAllFitnessProfiles(): Promise<FitnessProfile[]> {
const results = await this.db.select().from(fitnessProfiles).orderBy(desc(fitnessProfiles.createdAt)).all() const results = await this.db
return results.map(this.mapFitnessProfile) .select()
.from(fitnessProfiles)
.orderBy(desc(fitnessProfiles.createdAt))
.all();
return results.map(this.mapFitnessProfile);
} }
async updateFitnessProfile(userId: string, updates: Partial<FitnessProfile>): Promise<FitnessProfile | null> { async updateFitnessProfile(
const { userId: _, ...updateData } = updates userId: string,
if (Object.keys(updateData).length === 0) return this.getFitnessProfileByUserId(userId) updates: Partial<FitnessProfile>,
): Promise<FitnessProfile | null> {
const { userId: _, ...updateData } = updates;
if (Object.keys(updateData).length === 0)
return this.getFitnessProfileByUserId(userId);
const dbUpdates = { ...updateData } as any const dbUpdates = { ...updateData } as any;
if (updateData.fitnessGoals) { if (updateData.fitnessGoals) {
dbUpdates.fitnessGoals = JSON.stringify(updateData.fitnessGoals) dbUpdates.fitnessGoals = JSON.stringify(updateData.fitnessGoals);
} }
await this.db.update(fitnessProfiles) await this.db
.update(fitnessProfiles)
.set({ ...dbUpdates, updatedAt: new Date() }) .set({ ...dbUpdates, updatedAt: new Date() })
.where(eq(fitnessProfiles.userId, userId)) .where(eq(fitnessProfiles.userId, userId))
.run() .run();
return this.getFitnessProfileByUserId(userId) return this.getFitnessProfileByUserId(userId);
} }
async deleteFitnessProfile(userId: string): Promise<boolean> { async deleteFitnessProfile(userId: string): Promise<boolean> {
const result = await this.db.delete(fitnessProfiles).where(eq(fitnessProfiles.userId, userId)).run() const result = await this.db
return result.changes > 0 .delete(fitnessProfiles)
.where(eq(fitnessProfiles.userId, userId))
.run();
return result.changes > 0;
} }
// Attendance operations // Attendance operations
async checkIn(userId: string, type: "gym" | "class" | "personal_training", notes?: string): Promise<Attendance> { async checkIn(
const id = Math.random().toString(36).substr(2, 9) userId: string,
const now = new Date() type: "gym" | "class" | "personal_training",
notes?: string,
): Promise<Attendance> {
const id = Math.random().toString(36).substr(2, 9);
const now = new Date();
const newAttendance = { const newAttendance = {
id, id,
@ -303,48 +391,57 @@ export class DrizzleDatabase implements IDatabase {
checkInTime: now, checkInTime: now,
type, type,
notes, notes,
createdAt: now createdAt: now,
} };
await this.db.insert(attendance).values(newAttendance as any) await this.db.insert(attendance).values(newAttendance as any);
// Update client last visit // Update client last visit
const client = await this.getClientByUserId(userId) const client = await this.getClientByUserId(userId);
if (client) { if (client) {
await this.updateClient(client.id, { lastVisit: now }) await this.updateClient(client.id, { lastVisit: now });
} }
return newAttendance return newAttendance;
} }
async checkOut(attendanceId: string): Promise<Attendance | null> { async checkOut(attendanceId: string): Promise<Attendance | null> {
const now = new Date() const now = new Date();
await this.db.update(attendance) await this.db
.update(attendance)
.set({ checkOutTime: now }) .set({ checkOutTime: now })
.where(eq(attendance.id, attendanceId)) .where(eq(attendance.id, attendanceId))
.run() .run();
return this.getAttendanceById(attendanceId) return this.getAttendanceById(attendanceId);
} }
async getAttendanceById(id: string): Promise<Attendance | null> { async getAttendanceById(id: string): Promise<Attendance | null> {
const result = await this.db.select().from(attendance).where(eq(attendance.id, id)).get() const result = await this.db
return result ? this.mapAttendance(result) : null .select()
.from(attendance)
.where(eq(attendance.id, id))
.get();
return result ? this.mapAttendance(result) : null;
} }
async getAttendanceHistory(userId: string): Promise<Attendance[]> { async getAttendanceHistory(userId: string): Promise<Attendance[]> {
const results = await this.db.select().from(attendance) const results = await this.db
.select()
.from(attendance)
.where(eq(attendance.userId, userId)) .where(eq(attendance.userId, userId))
.orderBy(desc(attendance.checkInTime)) .orderBy(desc(attendance.checkInTime))
.all() .all();
return results.map(this.mapAttendance) return results.map(this.mapAttendance);
} }
async getAllAttendance(): Promise<Attendance[]> { async getAllAttendance(): Promise<Attendance[]> {
const results = await this.db.select().from(attendance) const results = await this.db
.select()
.from(attendance)
.orderBy(desc(attendance.checkInTime)) .orderBy(desc(attendance.checkInTime))
.all() .all();
return results.map(this.mapAttendance) return results.map(this.mapAttendance);
} }
async getActiveCheckIn(userId: string): Promise<Attendance | null> { async getActiveCheckIn(userId: string): Promise<Attendance | null> {
@ -356,173 +453,239 @@ export class DrizzleDatabase implements IDatabase {
// but correct way is `isNull(attendance.checkOutTime)`. // but correct way is `isNull(attendance.checkOutTime)`.
// Since I didn't import `isNull`, I'll fetch recent history and find first active. // Since I didn't import `isNull`, I'll fetch recent history and find first active.
const history = await this.getAttendanceHistory(userId) const history = await this.getAttendanceHistory(userId);
return history.find(a => !a.checkOutTime) || null return history.find((a) => !a.checkOutTime) || null;
} }
// Recommendation operations // Recommendation operations
async createRecommendation(data: Omit<Recommendation, "createdAt" | "approvedAt" | "approvedBy">): Promise<Recommendation> { async createRecommendation(
const now = new Date() data: Omit<Recommendation, "createdAt" | "approvedAt" | "approvedBy">,
): Promise<Recommendation> {
const now = new Date();
const newRec = { const newRec = {
...data, ...data,
createdAt: now, createdAt: now,
status: data.status || 'pending' status: data.status || "pending",
} };
await this.db.insert(recommendations).values(newRec as any) await this.db.insert(recommendations).values(newRec as any);
return newRec as Recommendation return newRec as Recommendation;
} }
async getRecommendationsByUserId(userId: string): Promise<Recommendation[]> { async getRecommendationsByUserId(userId: string): Promise<Recommendation[]> {
const results = await this.db.select().from(recommendations) const results = await this.db
.select()
.from(recommendations)
.where(eq(recommendations.userId, userId)) .where(eq(recommendations.userId, userId))
.orderBy(desc(recommendations.createdAt)) .orderBy(desc(recommendations.createdAt))
.all() .all();
return results.map(this.mapRecommendation) return results.map(this.mapRecommendation);
} }
async getAllRecommendations(): Promise<Recommendation[]> { async getAllRecommendations(): Promise<Recommendation[]> {
const results = await this.db.select().from(recommendations) const results = await this.db
.select()
.from(recommendations)
.orderBy(desc(recommendations.createdAt)) .orderBy(desc(recommendations.createdAt))
.all() .all();
return results.map(this.mapRecommendation) return results.map(this.mapRecommendation);
} }
async updateRecommendation(id: string, updates: Partial<Recommendation>): Promise<Recommendation | null> { async updateRecommendation(
const { id: _, ...updateData } = updates id: string,
if (Object.keys(updateData).length === 0) return this.getRecommendationById(id) updates: Partial<Recommendation>,
): Promise<Recommendation | null> {
const { id: _, ...updateData } = updates;
if (Object.keys(updateData).length === 0)
return this.getRecommendationById(id);
await this.db.update(recommendations) await this.db
.update(recommendations)
.set(updateData as any) .set(updateData as any)
.where(eq(recommendations.id, id)) .where(eq(recommendations.id, id))
.run() .run();
return this.getRecommendationById(id) return this.getRecommendationById(id);
} }
async deleteRecommendation(id: string): Promise<boolean> { async deleteRecommendation(id: string): Promise<boolean> {
const result = await this.db.delete(recommendations).where(eq(recommendations.id, id)).run() const result = await this.db
return result.changes > 0 .delete(recommendations)
.where(eq(recommendations.id, id))
.run();
return result.changes > 0;
} }
async getRecommendationById(id: string): Promise<Recommendation | null> { async getRecommendationById(id: string): Promise<Recommendation | null> {
const result = await this.db.select().from(recommendations).where(eq(recommendations.id, id)).get() const result = await this.db
return result ? this.mapRecommendation(result) : null .select()
.from(recommendations)
.where(eq(recommendations.id, id))
.get();
return result ? this.mapRecommendation(result) : null;
} }
// Fitness Goals operations // Fitness Goals operations
async createFitnessGoal(goalData: Omit<FitnessGoal, "createdAt" | "updatedAt">): Promise<FitnessGoal> { async createFitnessGoal(
const now = new Date() goalData: Omit<FitnessGoal, "createdAt" | "updatedAt">,
): Promise<FitnessGoal> {
const now = new Date();
const newGoal = { const newGoal = {
...goalData, ...goalData,
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now,
} };
await this.db.insert(fitnessGoals).values(newGoal as any) await this.db.insert(fitnessGoals).values(newGoal as any);
return newGoal as FitnessGoal return newGoal as FitnessGoal;
} }
async getFitnessGoalById(id: string): Promise<FitnessGoal | null> { async getFitnessGoalById(id: string): Promise<FitnessGoal | null> {
const result = await this.db.select().from(fitnessGoals).where(eq(fitnessGoals.id, id)).get() const result = await this.db
return result ? this.mapFitnessGoal(result) : null .select()
.from(fitnessGoals)
.where(eq(fitnessGoals.id, id))
.get();
return result ? this.mapFitnessGoal(result) : null;
} }
async getFitnessGoalsByUserId(userId: string, status?: string): Promise<FitnessGoal[]> { async getFitnessGoalsByUserId(
let query = this.db.select().from(fitnessGoals).where(eq(fitnessGoals.userId, userId)) userId: string,
status?: string,
): Promise<FitnessGoal[]> {
let query = this.db
.select()
.from(fitnessGoals)
.where(eq(fitnessGoals.userId, userId));
if (status) { if (status) {
query = this.db.select().from(fitnessGoals).where(and(eq(fitnessGoals.userId, userId), eq(fitnessGoals.status, status as any))) 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() const results = await query.orderBy(desc(fitnessGoals.createdAt)).all();
return results.map(this.mapFitnessGoal) return results.map(this.mapFitnessGoal);
} }
async updateFitnessGoal(id: string, updates: Partial<FitnessGoal>): Promise<FitnessGoal | null> { async updateFitnessGoal(
const { id: _, ...updateData } = updates id: string,
if (Object.keys(updateData).length === 0) return this.getFitnessGoalById(id) updates: Partial<FitnessGoal>,
): Promise<FitnessGoal | null> {
const { id: _, ...updateData } = updates;
if (Object.keys(updateData).length === 0)
return this.getFitnessGoalById(id);
await this.db.update(fitnessGoals) await this.db
.update(fitnessGoals)
.set({ ...updateData, updatedAt: new Date() } as any) .set({ ...updateData, updatedAt: new Date() } as any)
.where(eq(fitnessGoals.id, id)) .where(eq(fitnessGoals.id, id))
.run() .run();
return this.getFitnessGoalById(id) return this.getFitnessGoalById(id);
} }
async deleteFitnessGoal(id: string): Promise<boolean> { async deleteFitnessGoal(id: string): Promise<boolean> {
const result = await this.db.delete(fitnessGoals).where(eq(fitnessGoals.id, id)).run() const result = await this.db
return result.changes > 0 .delete(fitnessGoals)
.where(eq(fitnessGoals.id, id))
.run();
return result.changes > 0;
} }
async updateGoalProgress(id: string, currentValue: number): Promise<FitnessGoal | null> { async updateGoalProgress(
const goal = await this.getFitnessGoalById(id) id: string,
if (!goal) return null currentValue: number,
): Promise<FitnessGoal | null> {
const goal = await this.getFitnessGoalById(id);
if (!goal) return null;
let progress = goal.progress let progress = goal.progress;
if (goal.targetValue && goal.targetValue > 0) { if (goal.targetValue && goal.targetValue > 0) {
progress = Math.min(100, (currentValue / goal.targetValue) * 100) progress = Math.min(100, (currentValue / goal.targetValue) * 100);
} }
await this.db.update(fitnessGoals) await this.db
.update(fitnessGoals)
.set({ currentValue, progress, updatedAt: new Date() }) .set({ currentValue, progress, updatedAt: new Date() })
.where(eq(fitnessGoals.id, id)) .where(eq(fitnessGoals.id, id))
.run() .run();
return this.getFitnessGoalById(id) return this.getFitnessGoalById(id);
} }
async completeGoal(id: string): Promise<FitnessGoal | null> { async completeGoal(id: string): Promise<FitnessGoal | null> {
const now = new Date() const now = new Date();
await this.db.update(fitnessGoals) await this.db
.set({ status: 'completed', progress: 100, completedDate: now, updatedAt: now }) .update(fitnessGoals)
.set({
status: "completed",
progress: 100,
completedDate: now,
updatedAt: now,
})
.where(eq(fitnessGoals.id, id)) .where(eq(fitnessGoals.id, id))
.run() .run();
return this.getFitnessGoalById(id) return this.getFitnessGoalById(id);
} }
async getDashboardStats(): Promise<{ totalUsers: number; activeClients: number; totalRevenue: number; revenueGrowth: number }> { 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, // 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). // 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. // The original sqlite.ts snippet ended before showing this method, but the interface has it.
// I'll implement a basic count. // I'll implement a basic count.
const allUsers = await this.db.select().from(users).all() const allUsers = await this.db.select().from(users).all();
const activeClients = await this.db.select().from(clients).where(eq(clients.membershipStatus, 'active')).all() const activeClients = await this.db
.select()
.from(clients)
.where(eq(clients.membershipStatus, "active"))
.all();
return { return {
totalUsers: allUsers.length, totalUsers: allUsers.length,
activeClients: activeClients.length, activeClients: activeClients.length,
totalRevenue: 0, // Not tracking payments yet totalRevenue: 0, // Not tracking payments yet
revenueGrowth: 0 revenueGrowth: 0,
} };
} }
// Mappers // Mappers
private mapUser(row: any): User { private mapUser(row: any): User {
return { return {
...row, ...row,
gymId: (row as any).gymId ?? (row as any).gym_id ?? undefined,
createdAt: new Date(row.createdAt), createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt) updatedAt: new Date(row.updatedAt),
} };
} }
private mapClient(row: any): Client { private mapClient(row: any): Client {
return { return {
...row, ...row,
joinDate: new Date(row.joinDate), joinDate: new Date(row.joinDate),
lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined,
} };
} }
private mapFitnessProfile(row: any): FitnessProfile { private mapFitnessProfile(row: any): FitnessProfile {
return { return {
...row, ...row,
createdAt: new Date(row.createdAt), createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt) updatedAt: new Date(row.updatedAt),
} };
} }
private mapAttendance(row: any): Attendance { private mapAttendance(row: any): Attendance {
@ -530,16 +693,16 @@ export class DrizzleDatabase implements IDatabase {
...row, ...row,
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,
createdAt: new Date(row.createdAt) createdAt: new Date(row.createdAt),
} };
} }
private mapRecommendation(row: any): Recommendation { private mapRecommendation(row: any): Recommendation {
return { return {
...row, ...row,
createdAt: new Date(row.createdAt), createdAt: new Date(row.createdAt),
approvedAt: row.approvedAt ? new Date(row.approvedAt) : undefined approvedAt: row.approvedAt ? new Date(row.approvedAt) : undefined,
} };
} }
private mapFitnessGoal(row: any): FitnessGoal { private mapFitnessGoal(row: any): FitnessGoal {
@ -547,9 +710,11 @@ export class DrizzleDatabase implements IDatabase {
...row, ...row,
startDate: new Date(row.startDate), startDate: new Date(row.startDate),
targetDate: row.targetDate ? new Date(row.targetDate) : undefined, targetDate: row.targetDate ? new Date(row.targetDate) : undefined,
completedDate: row.completedDate ? new Date(row.completedDate) : undefined, completedDate: row.completedDate
? new Date(row.completedDate)
: undefined,
createdAt: new Date(row.createdAt), createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt) updatedAt: new Date(row.updatedAt),
} };
} }
} }

View File

@ -1,5 +1,5 @@
export const API_BASE_URL = __DEV__ export const API_BASE_URL = __DEV__
? "https://e0877d294c41.ngrok-free.app" ? "https://a4db649a0973.ngrok-free.app"
: "https://your-production-url.com"; : "https://your-production-url.com";
export const API_ENDPOINTS = { export const API_ENDPOINTS = {

View File

@ -1,15 +1,12 @@
import Database from 'better-sqlite3' import Database from "better-sqlite3";
import { drizzle } from 'drizzle-orm/better-sqlite3' import { drizzle } from "drizzle-orm/better-sqlite3";
import * as schema from './schema' import * as schema from "./schema";
// Configurable database path with intelligent defaults // Configurable database path with intelligent defaults
const dbPath = process.env.DATABASE_URL || const dbPath = "./data/fitai.db";
(process.env.NODE_ENV === 'production'
? './data/fitai.db'
: '../../apps/admin/data/fitai.db')
const sqlite = new Database(dbPath) const sqlite = new Database(dbPath);
export const db = drizzle(sqlite, { schema }) export const db = drizzle(sqlite, { schema });
export * from './schema' export * from "./schema";
export { eq, and, or, desc, asc, sql } from 'drizzle-orm' export { eq, and, or, desc, asc, sql } from "drizzle-orm";