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 {

File diff suppressed because it is too large Load Diff

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";