From 06973ccfb260b2584346b9c8527232ecb303039b Mon Sep 17 00:00:00 2001 From: echo Date: Thu, 19 Mar 2026 04:36:35 +0100 Subject: [PATCH] assigment and reports groundwork --- apps/admin/data/fitai.db | Bin 172032 -> 253952 bytes .../src/app/api/trainer-client/[id]/route.ts | 16 +- .../admin/src/app/api/trainer-client/route.ts | 3 +- apps/admin/src/app/api/users/route.ts | 22 +- apps/admin/src/app/trainer-clients/page.tsx | 60 ++--- .../src/components/reports/ReportFilters.tsx | 4 +- apps/admin/src/lib/database/drizzle.ts | 10 + apps/admin/src/lib/database/types.ts | 1 + .../lib/migrations/create-report-tables.js | 232 ++++++++++++++++++ 9 files changed, 299 insertions(+), 49 deletions(-) create mode 100644 apps/admin/src/lib/migrations/create-report-tables.js diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 4d6bc52298c953e82393ace7149d1a76729dce52..ccd543a8aed2cae07d2e22d803b651c8a75f3768 100644 GIT binary patch delta 4512 zcmcgvUu@Gx828ypoTN!E1%!c8aAEzomNsenccq}L4Gh*66CKD33HKa)wyM;0d zlMa|RF^MXIh&>>lkS1QnGLcCG0efJYrcIO3G-=udlaThXJ%Esgwmq=(C3X@!DU6Lp zvU9%gyYGJA-Tl7ryJI)($8NyWRh730g4l!}^;fsh>|<^bE$=Neqb>?QPy~JOFZerr zaC!DESdBhw8_}zB6?(ZVoX#GCb{w!c4#1OedD`WUxk%T&q`ZbVA$i-vLJN#i&Y=!w zy^~-}rVRv^|T)mBjSM$J|3M)pIm$HTW~g6*b z9%QaLfr(5pgXF*#OA$EdNE0B0XqZ1-JmZ-U!R^f05P69_ z=>}KaB=GobW&)B`gyUVu_KHkJz4bW#tEHR%#QYWYz4?@Rh^(T{yWY1ya!lGMfSovE z`@*&axw1u0INNAuYZFLx;CY1bVJ;jLB6Hm2TsR(-gjkfD5&1aklz5$>r+;U_KN?`S z4G#qNu#1jl?JWB37-n@Q8f0u-2sa~%#5W?fgxgCquhxP@p9W}(4@NjXD#Zm}%%YKm zDPELeq|&>U6zt z6ZJGxTMJI~OTqmSz7R=ayCvt?sQ=}`KoMTH!JZ60tVu4bsIJKxkItZF!rLZ<+0nqB z(Pq5#EE<;b+Ca5wJQfQl_qnR9pQ4OVWOj>jZkh@#CL4QvhUR7Zt^@%^H{ zb37;+`n2`J=2B@IR5|d>ja>~Ew4VxDCJUY7yoFz_^WqCM)6@h`RO6-4Iive*Dq0Yo zuwYFnbf{902F#=slh7GbT~-}p2LfCCV}qltC+{y_6mB9Gj2N`qJRVI^0WabO){NBe zeWN~gi3VFz-_XEXGd8?!WGs+BQR)F_*~Y#)^^D}sQ-0TS=Sl0>&{wPK8g@8$o;2O& zOe{dMWG1zhTm@u;AlGTkPPDbt%xiv-*sZZjiU)-#VsZC&IZ=6y zUIWz(y0XEX76c5snree862dqZRQqUVs23zQYpBNsDawl?htBJ`5aGE=L6l^#DvW)}+rXicjl;;TN?dtM%_jo!xyk3uwL(t!x^7+KMQh#iO3cp#nQXSI&;mh3xbvASjCm+3|Gy zx;@%Fg>oG=0-o$6RZ7k1 z0$EFq*!0$n2o)TJTX_+giVNV6&2|c&C*WOp9iB(1PD2w*1%Tp*nbKP1S}I^u`dnEz zN(?}okQ<=1vIEdUtaGA?oU}l|n{eI*oF6;<6~9%yZvWEWV>@TPXKkZ}bd7r}Ehn~g z(#&o@m#Vq@}bp7rHb3*u!no8!1rObKER5e`zV9oUn^ zuBY)NYekaFW`~Xc#FeZyEq*QHiH06SZj=A-e5K`}Z1$5;q{8(JDq0sot&0AAzRG7I zoUY`e!n#Zo!mpIjsXGw3MqLY<_bu1Q&JfJxo}mG;nNFgn3=(%dw@ zW|+qXK7wyoozroCR*21r+-xv1!xy<3R2%`aI^()9LXFS zZ2Q^2vx>2`u*_mnXP(bk!<@&+$FP#Yj(0ErL0+w?I*ggqg;^LyrZ@30Uf$lu%lL_r zjYEN9r?c$j4gV#k7xOX-Pp{x-?AWf(!uX4sxs-up;z9fE$sCNAnJ0&_*t0M&Fn3Sp z`>eQK-J407ar+KNMiwR}Tbb$e9hqdNbDd%4n_lL_B-|#zxJ`hG(Lj~GpMjq>pMk%E zw~aTH=Q&Rkk2sew=M_#zj*T1!?9*BAut%`wZ&noWWZiz+l}Uk#1L#)}X4wAPok@xj eWJ? a.id === id); + let assignment = trainerAssignments.find((a) => a.id === id); if (!assignment) { - // Check if any trainer has this assignment - const allAssignments = await db.getTrainerClientAssignments(""); - const foundAssignment = allAssignments.find((a) => a.id === id); + // Check all assignments to find the one with this ID + const allAssignments = await db.getAllTrainerClientAssignments(); + assignment = allAssignments.find((a) => a.id === id); - if (!foundAssignment) { + if (!assignment) { return NextResponse.json( { error: "Assignment not found" }, { status: 404 }, diff --git a/apps/admin/src/app/api/trainer-client/route.ts b/apps/admin/src/app/api/trainer-client/route.ts index 0a81dbe..7a1a688 100644 --- a/apps/admin/src/app/api/trainer-client/route.ts +++ b/apps/admin/src/app/api/trainer-client/route.ts @@ -48,8 +48,7 @@ export async function GET(request: NextRequest) { } } else { // Get all assignments (for admins, filtered by gym) - assignments = await db.getTrainerClientAssignments(""); - // TODO: Filter by gym once gymId is properly set + assignments = await db.getAllTrainerClientAssignments(); } return NextResponse.json({ assignments }); diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts index 298189f..4630514 100644 --- a/apps/admin/src/app/api/users/route.ts +++ b/apps/admin/src/app/api/users/route.ts @@ -45,12 +45,24 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const role = searchParams.get("role"); + log.debug("User API called", { + currentUserId: currentUser.id, + currentUserRole: currentUser.role, + currentUserGymId: currentUser.gymId, + }); + // Get target gym based on role const targetGymId = currentUser.role === "superAdmin" ? (searchParams.get("gymId") ?? undefined) : (currentUser.gymId ?? undefined); + log.debug("Target gym calculation", { + targetGymId, + currentUserRole: currentUser.role, + currentUserGymId: currentUser.gymId, + }); + // Validate gym access for non-superAdmins if (currentUser.role !== "superAdmin" && !targetGymId) { return forbiddenResponse("No gym assigned"); @@ -61,6 +73,12 @@ export async function GET(request: NextRequest) { ? await getUsersByGym(targetGymId) : await db.getAllUsers(); + log.debug("Users fetched from database", { + targetGymId, + totalUsers: Array.isArray(users) ? users.length : 0, + sampleGymId: users && users[0] ? (users[0] as any).gymId : null, + }); + // 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( @@ -90,12 +108,12 @@ export async function GET(request: NextRequest) { log.debug("Applied role filter", { role, usersAfterFilter: Array.isArray(users) ? users.length : 0, - sample: + sampleUser: users && users[0] ? { id: users[0].id, role: users[0].role, - gymId: (users as any)[0].gymId, + gymId: (users[0] as any).gymId, } : null, }); diff --git a/apps/admin/src/app/trainer-clients/page.tsx b/apps/admin/src/app/trainer-clients/page.tsx index 1782171..5ae9028 100644 --- a/apps/admin/src/app/trainer-clients/page.tsx +++ b/apps/admin/src/app/trainer-clients/page.tsx @@ -12,7 +12,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; -import { Plus, Trash2, UserCheck, UserX } from "lucide-react"; +import { Plus, UserCheck, UserX } from "lucide-react"; export default function TrainerClientsPage() { const [trainers, setTrainers] = useState([]); @@ -32,37 +32,34 @@ export default function TrainerClientsPage() { try { setLoading(true); - // Fetch trainers - const trainersRes = await fetch("/api/users?role=trainer"); - if (trainersRes.ok) { - const trainersData = await trainersRes.json(); - setTrainers(trainersData.users || []); - } + const [trainersRes, clientsRes, assignmentsRes] = await Promise.all([ + fetch("/api/users?role=trainer"), + fetch("/api/users?role=client"), + fetch("/api/trainer-client"), + ]); - // Fetch clients - const clientsRes = await fetch("/api/users?role=client"); - if (clientsRes.ok) { - const clientsData = await clientsRes.json(); - setClients(clientsData.users || []); - } + const fetchedTrainers: User[] = trainersRes.ok + ? (await trainersRes.json()).data?.users || [] + : []; + const fetchedClients: User[] = clientsRes.ok + ? (await clientsRes.json()).data?.users || [] + : []; + + setTrainers(fetchedTrainers); + setClients(fetchedClients); - // Fetch all assignments - const assignmentsRes = await fetch("/api/trainer-client"); if (assignmentsRes.ok) { const assignmentsData = await assignmentsRes.json(); - // Enrich assignments with user data - const enrichedAssignments = await Promise.all( - (assignmentsData.assignments || []).map( - async (assignment: TrainerClientAssignment) => { - const trainer = trainers.find( - (t: User) => t.id === assignment.trainerId, - ); - const client = clients.find( - (c: User) => c.id === assignment.clientId, - ); - return { ...assignment, trainer, client }; - }, - ), + const enrichedAssignments = (assignmentsData.assignments || []).map( + (assignment: TrainerClientAssignment) => { + return { + ...assignment, + trainer: fetchedTrainers.find( + (t) => t.id === assignment.trainerId, + ), + client: fetchedClients.find((c) => c.id === assignment.clientId), + }; + }, ); setAssignments(enrichedAssignments); } @@ -93,7 +90,7 @@ export default function TrainerClientsPage() { alert("Assignment created successfully!"); setSelectedTrainer(""); setSelectedClient(""); - fetchData(); // Refresh + fetchData(); } else { const error = await response.json(); alert(error.error || "Failed to create assignment"); @@ -116,7 +113,7 @@ export default function TrainerClientsPage() { if (response.ok) { alert("Assignment removed successfully!"); - fetchData(); // Refresh + fetchData(); } else { const error = await response.json(); alert(error.error || "Failed to remove assignment"); @@ -151,7 +148,6 @@ export default function TrainerClientsPage() { - {/* Create Assignment Form */} Assign Trainer to Client @@ -206,7 +202,6 @@ export default function TrainerClientsPage() { - {/* Active Assignments */}
@@ -260,7 +255,6 @@ export default function TrainerClientsPage() { - {/* Inactive Assignments */} {getInactiveAssignments().length > 0 && ( diff --git a/apps/admin/src/components/reports/ReportFilters.tsx b/apps/admin/src/components/reports/ReportFilters.tsx index 96f1c37..441877c 100644 --- a/apps/admin/src/components/reports/ReportFilters.tsx +++ b/apps/admin/src/components/reports/ReportFilters.tsx @@ -71,7 +71,7 @@ export function ReportFilters({ ); if (clientsRes.ok) { const clientsData = await clientsRes.json(); - setUsers(clientsData.users || []); + setUsers(clientsData.data?.users || []); } } else { setUsers([]); @@ -82,7 +82,7 @@ export function ReportFilters({ const clientsRes = await fetch("/api/users?role=client"); if (clientsRes.ok) { const clientsData = await clientsRes.json(); - setUsers(clientsData.users || []); + setUsers(clientsData.data?.users || []); } } } diff --git a/apps/admin/src/lib/database/drizzle.ts b/apps/admin/src/lib/database/drizzle.ts index 301a75d..dcd0ae1 100644 --- a/apps/admin/src/lib/database/drizzle.ts +++ b/apps/admin/src/lib/database/drizzle.ts @@ -2025,6 +2025,16 @@ export class DrizzleDatabase implements IDatabase { return results.map((row) => this.mapTrainerClientAssignment(row)); } + async getAllTrainerClientAssignments(): Promise { + const results = await this.db + .select() + .from(trainerClientAssignments) + .orderBy(desc(trainerClientAssignments.assignedAt)) + .all(); + + return results.map((row) => this.mapTrainerClientAssignment(row)); + } + async getClientTrainerAssignment( clientId: string, ): Promise { diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts index 1e15c4f..e37519a 100644 --- a/apps/admin/src/lib/database/types.ts +++ b/apps/admin/src/lib/database/types.ts @@ -269,6 +269,7 @@ export interface IDatabase { getTrainerClientAssignments( trainerId: string, ): Promise; + getAllTrainerClientAssignments(): Promise; getClientTrainerAssignment( clientId: string, ): Promise; diff --git a/apps/admin/src/lib/migrations/create-report-tables.js b/apps/admin/src/lib/migrations/create-report-tables.js new file mode 100644 index 0000000..b0bdcb6 --- /dev/null +++ b/apps/admin/src/lib/migrations/create-report-tables.js @@ -0,0 +1,232 @@ +/** + * Migration Script: Create new tables for report generation + * + * This script: + * 1. Creates the following tables: + * - daily_nutrition - Daily calorie tracking + * - meal_entries - Individual meal details + * - daily_hydration - Daily water intake tracking + * - fitness_profile_history - Profile change history + * - trainer_client_assignments - Trainer-client relationships + * + * 2. Fixes gym assignments for users without gymId: + * - Assigns superAdmin to their first gym + * - Assigns other users to gym of their trainer + * + * Run with: node apps/admin/src/lib/migrations/create-report-tables.js + * + * Note: Run this AFTER setting up the base database + */ + +const Database = require("better-sqlite3"); + +// Use absolute path to the database +const dbPath = "/home/echo/dev/prototype/apps/admin/data/fitai.db"; + +function createReportTables() { + console.log("Starting report tables migration...\n"); + console.log(`Database path: ${dbPath}\n`); + + const db = new Database(dbPath); + + // 1. Create daily_nutrition table + console.log("Creating daily_nutrition table..."); + db.exec(` + CREATE TABLE IF NOT EXISTS daily_nutrition ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + date TEXT NOT NULL, + total_calories INTEGER DEFAULT 0, + calorie_goal INTEGER DEFAULT 2000, + meals TEXT DEFAULT '[]', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(user_id, date) + ) + `); + console.log(" ✓ daily_nutrition table created"); + + // 2. Create meal_entries table + console.log("Creating meal_entries table..."); + db.exec(` + CREATE TABLE IF NOT EXISTS meal_entries ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + daily_nutrition_id TEXT, + meal_type TEXT NOT NULL, + food_name TEXT NOT NULL, + calories INTEGER NOT NULL, + protein INTEGER, + carbs INTEGER, + fats INTEGER, + timestamp INTEGER NOT NULL, + created_at INTEGER NOT NULL + ) + `); + console.log(" ✓ meal_entries table created"); + + // 3. Create daily_hydration table + console.log("Creating daily_hydration table..."); + db.exec(` + CREATE TABLE IF NOT EXISTS daily_hydration ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + date TEXT NOT NULL, + total_water INTEGER DEFAULT 0, + water_goal INTEGER DEFAULT 2000, + entries TEXT DEFAULT '[]', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(user_id, date) + ) + `); + console.log(" ✓ daily_hydration table created"); + + // 4. Create fitness_profile_history table + console.log("Creating fitness_profile_history table..."); + db.exec(` + CREATE TABLE IF NOT EXISTS fitness_profile_history ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + fitness_profile_id TEXT NOT NULL, + change_type TEXT NOT NULL, + field_name TEXT NOT NULL, + previous_value TEXT, + new_value TEXT, + changed_at INTEGER NOT NULL, + created_at INTEGER NOT NULL + ) + `); + console.log(" ✓ fitness_profile_history table created"); + + // 5. Create trainer_client_assignments table + console.log("Creating trainer_client_assignments table..."); + db.exec(` + CREATE TABLE IF NOT EXISTS trainer_client_assignments ( + id TEXT PRIMARY KEY, + trainer_id TEXT NOT NULL, + client_id TEXT NOT NULL, + assigned_at INTEGER NOT NULL, + assigned_by TEXT, + is_active INTEGER DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `); + console.log(" ✓ trainer_client_assignments table created"); + + // Create indexes for better query performance + console.log("\nCreating indexes..."); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_daily_nutrition_user_date + ON daily_nutrition(user_id, date) + `); + console.log(" ✓ Index: daily_nutrition.user_id + date"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_meal_entries_user_timestamp + ON meal_entries(user_id, timestamp) + `); + console.log(" ✓ Index: meal_entries.user_id + timestamp"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_daily_hydration_user_date + ON daily_hydration(user_id, date) + `); + console.log(" ✓ Index: daily_hydration.user_id + date"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_fitness_profile_history_user + ON fitness_profile_history(user_id) + `); + console.log(" ✓ Index: fitness_profile_history.user_id"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_trainer_client_assignments_trainer + ON trainer_client_assignments(trainer_id) + `); + console.log(" ✓ Index: trainer_client_assignments.trainer_id"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_trainer_client_assignments_client + ON trainer_client_assignments(client_id) + `); + console.log(" ✓ Index: trainer_client_assignments.client_id"); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_trainer_client_assignments_active + ON trainer_client_assignments(trainer_id, client_id, is_active) + `); + console.log(" ✓ Index: trainer_client_assignments (composite)"); + + db.close(); + + console.log("\n=== Migration Complete ==="); + console.log("All report generation tables created successfully!"); + console.log("\nTables created:"); + console.log(" - daily_nutrition"); + console.log(" - meal_entries"); + console.log(" - daily_hydration"); + console.log(" - fitness_profile_history"); + console.log(" - trainer_client_assignments"); + console.log("\nIndexes created: 7"); + + // Fix gym assignments for users without gymId + console.log("\n=== Fixing Gym Assignments ==="); + + const usersWithoutGym = db + .prepare("SELECT id, email, role FROM users WHERE gym_id IS NULL") + .all(); + + console.log(`Found ${usersWithoutGym.length} users without gymId`); + + let fixedCount = 0; + + for (const user of usersWithoutGym) { + if (user.role === "superAdmin") { + // Get first gym + const gym = db.prepare("SELECT id FROM gyms LIMIT 1").get(); + if (gym) { + db.prepare("UPDATE users SET gym_id = ? WHERE id = ?").run( + gym.id, + user.id, + ); + console.log(` ✓ Fixed ${user.email} (superAdmin) -> gym ${gym.id}`); + fixedCount++; + } + } else { + // Try to find gym from trainer_clients table + const trainerClient = db + .prepare( + "SELECT gym_id FROM trainer_clients WHERE trainer_user_id = ? OR client_user_id = ? LIMIT 1", + ) + .get(user.id, user.id); + + if (trainerClient) { + db.prepare("UPDATE users SET gym_id = ? WHERE id = ?").run( + trainerClient.gym_id, + user.id, + ); + console.log( + ` ✓ Fixed ${user.email} (${user.role}) -> gym ${trainerClient.gym_id}`, + ); + fixedCount++; + } else { + console.log( + ` ⚠ Could not fix ${user.email} (${user.role}) - no trainer_clients record`, + ); + } + } + } + + console.log(`\nFixed ${fixedCount} users without gymId`); + console.log("\nGym assignments update complete!"); +} + +// Run if called directly +if (require.main === module) { + createReportTables(); +} + +module.exports = { createReportTables };