now gym in UI displays properly
This commit is contained in:
parent
6580564767
commit
339d798a88
Binary file not shown.
2
apps/admin/next-env.d.ts
vendored
2
apps/admin/next-env.d.ts
vendored
@ -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.
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user