assigment and reports groundwork
This commit is contained in:
parent
74f0d0dbed
commit
06973ccfb2
Binary file not shown.
@ -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 },
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 || []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -269,6 +269,7 @@ export interface IDatabase {
|
||||
getTrainerClientAssignments(
|
||||
trainerId: string,
|
||||
): Promise<TrainerClientAssignment[]>;
|
||||
getAllTrainerClientAssignments(): Promise<TrainerClientAssignment[]>;
|
||||
getClientTrainerAssignment(
|
||||
clientId: string,
|
||||
): Promise<TrainerClientAssignment | null>;
|
||||
|
||||
232
apps/admin/src/lib/migrations/create-report-tables.js
Normal file
232
apps/admin/src/lib/migrations/create-report-tables.js
Normal 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 };
|
||||
Loading…
Reference in New Issue
Block a user