assigment and reports groundwork

This commit is contained in:
echo 2026-03-19 04:36:35 +01:00
parent 74f0d0dbed
commit 06973ccfb2
9 changed files with 299 additions and 49 deletions

Binary file not shown.

View File

@ -35,22 +35,18 @@ export async function DELETE(
const { id } = await params;
// Get all assignments for the trainer to find the one with this ID
// Note: We need to find the assignment ID, not the trainer or client ID
// For now, we'll deactivate by finding the assignment through the trainer
// This is a simplified approach - in production you'd want a direct getById method
// Try to find the assignment by checking trainer's assignments first
const trainerAssignments = await db.getTrainerClientAssignments(
currentUser.id,
);
const assignment = trainerAssignments.find((a) => 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 },

View File

@ -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 });

View File

@ -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<string, string | null>(
@ -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,
});

View File

@ -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<User[]>([]);
@ -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() {
</h1>
</div>
{/* Create Assignment Form */}
<Card>
<CardHeader>
<CardTitle>Assign Trainer to Client</CardTitle>
@ -206,7 +202,6 @@ export default function TrainerClientsPage() {
</CardContent>
</Card>
{/* Active Assignments */}
<Card>
<CardHeader>
<div className="flex justify-between items-center">
@ -260,7 +255,6 @@ export default function TrainerClientsPage() {
</CardContent>
</Card>
{/* Inactive Assignments */}
{getInactiveAssignments().length > 0 && (
<Card>
<CardHeader>

View File

@ -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 || []);
}
}
}

View File

@ -2025,6 +2025,16 @@ export class DrizzleDatabase implements IDatabase {
return results.map((row) => this.mapTrainerClientAssignment(row));
}
async getAllTrainerClientAssignments(): Promise<TrainerClientAssignment[]> {
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<TrainerClientAssignment | null> {

View File

@ -269,6 +269,7 @@ export interface IDatabase {
getTrainerClientAssignments(
trainerId: string,
): Promise<TrainerClientAssignment[]>;
getAllTrainerClientAssignments(): Promise<TrainerClientAssignment[]>;
getClientTrainerAssignment(
clientId: string,
): Promise<TrainerClientAssignment | null>;

View File

@ -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 };