phase 1-6 implemented
This commit is contained in:
1 parent
22c274bb83
commit
2c4927478a
73 files changed
+9702
-2354
No files matched your search
@@ -19,9 +19,114 @@ import {
|
||||
eq,
|
||||
and,
|
||||
desc,
|
||||
asc,
|
||||
sql,
|
||||
} from "@fitai/database";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { SQL, or, like, inArray, gte, lte } from "drizzle-orm";
|
||||
import type { SortConfig, FilterCondition } from "../filtering";
|
||||
import { buildWhereConditions, buildSearchCondition } from "../filtering";
|
||||
import log from "../logger";
|
||||
|
||||
// Database row types (before mapping to domain types)
|
||||
interface UserRow {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string | null;
|
||||
phone: string | null;
|
||||
role: string;
|
||||
gymId?: string | null;
|
||||
gym_id?: string | null; // Alternative column name
|
||||
createdAt: number | Date;
|
||||
updatedAt: number | Date;
|
||||
}
|
||||
|
||||
interface ClientRow {
|
||||
id: string;
|
||||
userId: string;
|
||||
membershipType: string;
|
||||
membershipStatus: string;
|
||||
joinDate: number | Date;
|
||||
lastVisit?: number | Date | null;
|
||||
emergencyContactName?: string | null;
|
||||
emergencyContactPhone?: string | null;
|
||||
emergencyContactRelationship?: string | null;
|
||||
createdAt: number | Date;
|
||||
updatedAt: number | Date;
|
||||
}
|
||||
|
||||
interface FitnessProfileRow {
|
||||
id: string;
|
||||
userId: string;
|
||||
height?: number | null;
|
||||
weight?: number | null;
|
||||
age?: number | null;
|
||||
goals?: string | null;
|
||||
medicalConditions?: string | null;
|
||||
dietaryRestrictions?: string | null;
|
||||
activityLevel?: string | null;
|
||||
createdAt: number | Date;
|
||||
updatedAt: number | Date;
|
||||
}
|
||||
|
||||
interface AttendanceRow {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
checkInTime: number | Date;
|
||||
checkOutTime?: number | Date | null;
|
||||
notes?: string | null;
|
||||
createdAt: number | Date;
|
||||
}
|
||||
|
||||
interface RecommendationRow {
|
||||
id: string;
|
||||
userId: string;
|
||||
fitnessProfileId: string;
|
||||
recommendationText: string;
|
||||
activityPlan: string;
|
||||
dietPlan: string;
|
||||
status: string;
|
||||
generatedAt: number | Date;
|
||||
approvedBy?: string | null;
|
||||
approvedAt?: number | Date | null;
|
||||
createdAt: number | Date;
|
||||
updatedAt: number | Date;
|
||||
}
|
||||
|
||||
interface FitnessGoalRow {
|
||||
id: string;
|
||||
userId: string;
|
||||
fitnessProfileId?: string | null;
|
||||
goalType: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
targetValue?: number | null;
|
||||
currentValue?: number | null;
|
||||
unit?: string | null;
|
||||
status: string;
|
||||
progress: number | null;
|
||||
priority: string;
|
||||
startDate: number | Date;
|
||||
targetDate?: number | Date | null;
|
||||
completedDate?: number | Date | null;
|
||||
notes?: string | null;
|
||||
createdAt: number | Date;
|
||||
updatedAt: number | Date;
|
||||
}
|
||||
|
||||
// Type for PRAGMA table_info result
|
||||
interface TableInfoRow {
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: number;
|
||||
dflt_value: string | null;
|
||||
pk: number;
|
||||
}
|
||||
|
||||
// Type for raw SQL results with unknown structure
|
||||
type RawSQLRow = Record<string, unknown>;
|
||||
|
||||
export class DrizzleDatabase implements IDatabase {
|
||||
private config: DatabaseConfig;
|
||||
@@ -36,7 +141,7 @@ export class DrizzleDatabase implements IDatabase {
|
||||
// Drizzle with better-sqlite3 connects synchronously on initialization
|
||||
// We can just log here if needed
|
||||
if (this.config.options?.logging) {
|
||||
console.log("Drizzle database connected");
|
||||
log.info("Drizzle database connected");
|
||||
}
|
||||
await this.createTables();
|
||||
}
|
||||
@@ -46,7 +151,7 @@ export class DrizzleDatabase implements IDatabase {
|
||||
// but we can close the underlying sqlite instance if we had access to it.
|
||||
// For now, we'll assume it's handled.
|
||||
if (this.config.options?.logging) {
|
||||
console.log("Drizzle database disconnected");
|
||||
log.info("Drizzle database disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +349,268 @@ export class DrizzleDatabase implements IDatabase {
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users with pagination, sorting, and filtering
|
||||
* Uses SQL LIMIT/OFFSET for efficient pagination
|
||||
*/
|
||||
async getUsersWithPagination(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
role?: string;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{ users: User[]; total: number }> {
|
||||
const { page, limit, role, sort, filters, search } = params;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Build WHERE conditions
|
||||
const whereConditions: any[] = [];
|
||||
|
||||
// Add role filter if provided
|
||||
if (role) {
|
||||
whereConditions.push(eq(users.role, role as User["role"]));
|
||||
}
|
||||
|
||||
// Add filter conditions from query params
|
||||
if (filters && filters.length > 0) {
|
||||
const columnMap = {
|
||||
role: users.role,
|
||||
email: users.email,
|
||||
firstName: users.firstName,
|
||||
lastName: users.lastName,
|
||||
phone: users.phone,
|
||||
gymId: users.gymId,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
};
|
||||
const filterCondition = buildWhereConditions(filters, columnMap);
|
||||
if (filterCondition) {
|
||||
whereConditions.push(filterCondition);
|
||||
}
|
||||
}
|
||||
|
||||
// Add search condition
|
||||
if (search) {
|
||||
const searchFields = ["email", "firstName", "lastName", "phone"];
|
||||
const searchCondition = buildSearchCondition(search, searchFields, {
|
||||
email: users.email,
|
||||
firstName: users.firstName,
|
||||
lastName: users.lastName,
|
||||
phone: users.phone,
|
||||
});
|
||||
if (searchCondition) {
|
||||
whereConditions.push(searchCondition);
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
whereConditions.length > 0 ? and(...whereConditions) : undefined;
|
||||
|
||||
// Build ORDER BY clause
|
||||
const sortColumn = sort?.field
|
||||
? {
|
||||
email: users.email,
|
||||
firstName: users.firstName,
|
||||
lastName: users.lastName,
|
||||
role: users.role,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
}[sort.field]
|
||||
: users.createdAt;
|
||||
|
||||
const orderBy =
|
||||
sort?.direction === "asc"
|
||||
? asc(sortColumn || users.createdAt)
|
||||
: desc(sortColumn || users.createdAt);
|
||||
|
||||
// Get total count (without pagination)
|
||||
const countQuery = whereClause
|
||||
? this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users)
|
||||
.where(whereClause)
|
||||
: this.db.select({ count: sql<number>`count(*)` }).from(users);
|
||||
|
||||
const countResult = await countQuery.get();
|
||||
const total = countResult?.count ?? 0;
|
||||
|
||||
// Get paginated results
|
||||
const query = this.db
|
||||
.select()
|
||||
.from(users)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const results = whereClause
|
||||
? await query.where(whereClause).all()
|
||||
: await query.all();
|
||||
|
||||
return {
|
||||
users: results.map(this.mapUser),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users with their client and attendance data in optimized batches
|
||||
* Avoids N+1 query problem by batching related data queries
|
||||
* Now supports sorting, filtering, and search
|
||||
*/
|
||||
async getUsersWithRelatedData(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
role?: string;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{
|
||||
users: Array<
|
||||
User & {
|
||||
client?: Client | null;
|
||||
isCheckedIn?: boolean;
|
||||
checkInTime?: Date | null;
|
||||
lastCheckInTime?: Date | null;
|
||||
checkInsThisWeek?: number;
|
||||
checkInsThisMonth?: number;
|
||||
}
|
||||
>;
|
||||
total?: number;
|
||||
}> {
|
||||
const page = params?.page ?? 1;
|
||||
const limit = params?.limit ?? 1000;
|
||||
const role = params?.role;
|
||||
const sort = params?.sort;
|
||||
const filters = params?.filters;
|
||||
const search = params?.search;
|
||||
|
||||
// Step 1: Get paginated users with sorting and filtering
|
||||
const { users: paginatedUsers, total } = await this.getUsersWithPagination({
|
||||
page,
|
||||
limit,
|
||||
role,
|
||||
sort,
|
||||
filters,
|
||||
search,
|
||||
});
|
||||
|
||||
if (paginatedUsers.length === 0) {
|
||||
return { users: [], total: 0 };
|
||||
}
|
||||
|
||||
const userIds = paginatedUsers.map((u) => u.id);
|
||||
|
||||
// Step 2: Batch fetch clients for all users (1 query instead of N)
|
||||
const allClients = await this.db.select().from(clients).all();
|
||||
const clientsByUserId = new Map(
|
||||
allClients.map((c) => [c.userId, this.mapClient(c)]),
|
||||
);
|
||||
|
||||
// Step 3: Batch fetch attendance stats (2 queries instead of 2N)
|
||||
const attendanceStats = await this.getAttendanceStatsBatch(userIds);
|
||||
|
||||
// Step 4: Combine all data
|
||||
const usersWithData = paginatedUsers.map((user) => {
|
||||
const client = clientsByUserId.get(user.id) || null;
|
||||
const stats = attendanceStats.get(user.id) || {
|
||||
isCheckedIn: false,
|
||||
checkInTime: null,
|
||||
lastCheckInTime: null,
|
||||
checkInsThisWeek: 0,
|
||||
checkInsThisMonth: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
...user,
|
||||
client,
|
||||
...stats,
|
||||
};
|
||||
});
|
||||
|
||||
return { users: usersWithData, total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attendance statistics for multiple users efficiently
|
||||
* Uses aggregation queries to avoid N+1 problem
|
||||
*/
|
||||
async getAttendanceStatsBatch(userIds: string[]): Promise<
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
isCheckedIn: boolean;
|
||||
checkInTime: Date | null;
|
||||
lastCheckInTime: Date | null;
|
||||
checkInsThisWeek: number;
|
||||
checkInsThisMonth: number;
|
||||
}
|
||||
>
|
||||
> {
|
||||
if (userIds.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
// Fetch ALL attendance records for these users (2 queries total instead of 2N)
|
||||
const allAttendance = await this.db.select().from(attendance).all();
|
||||
|
||||
// Filter to relevant users
|
||||
const relevantAttendance = allAttendance.filter((a) =>
|
||||
userIds.includes(a.userId),
|
||||
);
|
||||
|
||||
const statsMap = new Map();
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
for (const userId of userIds) {
|
||||
const userAttendance = relevantAttendance.filter(
|
||||
(a) => a.userId === userId,
|
||||
);
|
||||
|
||||
// Find active check-in
|
||||
const activeCheckIn = userAttendance.find((a) => !a.checkOutTime);
|
||||
|
||||
// Get last check-in time
|
||||
const sortedAttendance = userAttendance.sort(
|
||||
(a, b) =>
|
||||
new Date(b.checkInTime).getTime() - new Date(a.checkInTime).getTime(),
|
||||
);
|
||||
const lastCheckInTime = sortedAttendance[0]?.checkInTime || null;
|
||||
|
||||
// Count check-ins
|
||||
const checkInsThisWeek = userAttendance.filter(
|
||||
(a) => new Date(a.checkInTime) >= weekAgo,
|
||||
).length;
|
||||
const checkInsThisMonth = userAttendance.filter(
|
||||
(a) => new Date(a.checkInTime) >= monthAgo,
|
||||
).length;
|
||||
|
||||
statsMap.set(userId, {
|
||||
isCheckedIn: !!activeCheckIn,
|
||||
checkInTime: activeCheckIn?.checkInTime || null,
|
||||
lastCheckInTime,
|
||||
checkInsThisWeek,
|
||||
checkInsThisMonth,
|
||||
});
|
||||
}
|
||||
|
||||
// Fill in missing users with default values
|
||||
for (const userId of userIds) {
|
||||
if (!statsMap.has(userId)) {
|
||||
statsMap.set(userId, {
|
||||
isCheckedIn: false,
|
||||
checkInTime: null,
|
||||
lastCheckInTime: null,
|
||||
checkInsThisWeek: 0,
|
||||
checkInsThisMonth: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return statsMap;
|
||||
}
|
||||
|
||||
// Client operations
|
||||
async createClient(clientData: Omit<Client, "id">): Promise<Client> {
|
||||
const id = Math.random().toString(36).substr(2, 9);
|
||||
@@ -285,6 +652,101 @@ export class DrizzleDatabase implements IDatabase {
|
||||
return results.map(this.mapClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clients with pagination, sorting, and filtering
|
||||
*/
|
||||
async getClientsWithPagination(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{ clients: Client[]; total: number }> {
|
||||
const { page, limit, sort, filters, search } = params;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Build WHERE conditions
|
||||
const whereConditions: any[] = [];
|
||||
|
||||
// Add filter conditions
|
||||
if (filters && filters.length > 0) {
|
||||
const columnMap = {
|
||||
userId: clients.userId,
|
||||
membershipType: clients.membershipType,
|
||||
membershipStatus: clients.membershipStatus,
|
||||
joinDate: clients.joinDate,
|
||||
lastVisit: clients.lastVisit,
|
||||
};
|
||||
const filterCondition = buildWhereConditions(filters, columnMap);
|
||||
if (filterCondition) {
|
||||
whereConditions.push(filterCondition);
|
||||
}
|
||||
}
|
||||
|
||||
// Add search condition (search by user ID or emergency contact)
|
||||
if (search) {
|
||||
const searchCondition = buildSearchCondition(
|
||||
search,
|
||||
["userId", "emergencyContactName", "emergencyContactPhone"],
|
||||
{
|
||||
userId: clients.userId,
|
||||
emergencyContactName: clients.emergencyContactName,
|
||||
emergencyContactPhone: clients.emergencyContactPhone,
|
||||
},
|
||||
);
|
||||
if (searchCondition) {
|
||||
whereConditions.push(searchCondition);
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
whereConditions.length > 0 ? and(...whereConditions) : undefined;
|
||||
|
||||
// Build ORDER BY clause
|
||||
const sortColumn = sort?.field
|
||||
? {
|
||||
joinDate: clients.joinDate,
|
||||
lastVisit: clients.lastVisit,
|
||||
membershipType: clients.membershipType,
|
||||
membershipStatus: clients.membershipStatus,
|
||||
userId: clients.userId,
|
||||
}[sort.field]
|
||||
: clients.joinDate;
|
||||
|
||||
const orderBy =
|
||||
sort?.direction === "asc"
|
||||
? asc(sortColumn || clients.joinDate)
|
||||
: desc(sortColumn || clients.joinDate);
|
||||
|
||||
// Get total count
|
||||
const countQuery = whereClause
|
||||
? this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(clients)
|
||||
.where(whereClause)
|
||||
: this.db.select({ count: sql<number>`count(*)` }).from(clients);
|
||||
|
||||
const countResult = await countQuery.get();
|
||||
const total = countResult?.count ?? 0;
|
||||
|
||||
// Get paginated results
|
||||
const query = this.db
|
||||
.select()
|
||||
.from(clients)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const results = whereClause
|
||||
? await query.where(whereClause).all()
|
||||
: await query.all();
|
||||
|
||||
return {
|
||||
clients: results.map(this.mapClient),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
async updateClient(
|
||||
id: string,
|
||||
updates: Partial<Client>,
|
||||
@@ -444,6 +906,98 @@ export class DrizzleDatabase implements IDatabase {
|
||||
return results.map(this.mapAttendance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attendance records with pagination, sorting, and filtering
|
||||
*/
|
||||
async getAttendanceWithPagination(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{ attendance: Attendance[]; total: number }> {
|
||||
const { page, limit, sort, filters, search } = params;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Build WHERE conditions
|
||||
const whereConditions: any[] = [];
|
||||
|
||||
// Add filter conditions
|
||||
if (filters && filters.length > 0) {
|
||||
const columnMap = {
|
||||
userId: attendance.userId,
|
||||
type: attendance.type,
|
||||
checkInTime: attendance.checkInTime,
|
||||
checkOutTime: attendance.checkOutTime,
|
||||
};
|
||||
const filterCondition = buildWhereConditions(filters, columnMap);
|
||||
if (filterCondition) {
|
||||
whereConditions.push(filterCondition);
|
||||
}
|
||||
}
|
||||
|
||||
// Add search condition (search by user ID or notes)
|
||||
if (search) {
|
||||
const searchCondition = buildSearchCondition(
|
||||
search,
|
||||
["userId", "notes"],
|
||||
{
|
||||
userId: attendance.userId,
|
||||
notes: attendance.notes,
|
||||
},
|
||||
);
|
||||
if (searchCondition) {
|
||||
whereConditions.push(searchCondition);
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
whereConditions.length > 0 ? and(...whereConditions) : undefined;
|
||||
|
||||
// Build ORDER BY clause
|
||||
const sortColumn = sort?.field
|
||||
? {
|
||||
checkInTime: attendance.checkInTime,
|
||||
checkOutTime: attendance.checkOutTime,
|
||||
type: attendance.type,
|
||||
userId: attendance.userId,
|
||||
}[sort.field]
|
||||
: attendance.checkInTime;
|
||||
|
||||
const orderBy =
|
||||
sort?.direction === "asc"
|
||||
? asc(sortColumn || attendance.checkInTime)
|
||||
: desc(sortColumn || attendance.checkInTime);
|
||||
|
||||
// Get total count
|
||||
const countQuery = whereClause
|
||||
? this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(attendance)
|
||||
.where(whereClause)
|
||||
: this.db.select({ count: sql<number>`count(*)` }).from(attendance);
|
||||
|
||||
const countResult = await countQuery.get();
|
||||
const total = countResult?.count ?? 0;
|
||||
|
||||
// Get paginated results
|
||||
const query = this.db
|
||||
.select()
|
||||
.from(attendance)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const results = whereClause
|
||||
? await query.where(whereClause).all()
|
||||
: await query.all();
|
||||
|
||||
return {
|
||||
attendance: results.map(this.mapAttendance),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
async getActiveCheckIn(userId: string): Promise<Attendance | null> {
|
||||
// Drizzle doesn't support IS NULL in where directly with simple syntax sometimes, but eq(col, null) works or isNull(col)
|
||||
// We need to check how to filter for null checkOutTime.
|
||||
@@ -662,59 +1216,135 @@ export class DrizzleDatabase implements IDatabase {
|
||||
};
|
||||
}
|
||||
|
||||
// Mappers
|
||||
private mapUser(row: any): User {
|
||||
// Mappers - using Record<string, unknown> for database rows since Drizzle types vary
|
||||
private mapUser(row: Record<string, unknown>): User {
|
||||
return {
|
||||
...row,
|
||||
gymId: (row as any).gymId ?? (row as any).gym_id ?? undefined,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt),
|
||||
id: String(row.id),
|
||||
email: String(row.email),
|
||||
firstName: String(row.firstName),
|
||||
lastName: String(row.lastName),
|
||||
password: String(row.password ?? ""),
|
||||
phone: row.phone ? String(row.phone) : undefined,
|
||||
role: String(row.role) as User["role"],
|
||||
gymId: row.gymId
|
||||
? String(row.gymId)
|
||||
: row.gym_id
|
||||
? String(row.gym_id)
|
||||
: undefined,
|
||||
createdAt: new Date(row.createdAt as number | Date),
|
||||
updatedAt: new Date(row.updatedAt as number | Date),
|
||||
};
|
||||
}
|
||||
|
||||
private mapClient(row: any): Client {
|
||||
private mapClient(row: Record<string, unknown>): Client {
|
||||
return {
|
||||
...row,
|
||||
joinDate: new Date(row.joinDate),
|
||||
lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private mapFitnessProfile(row: any): FitnessProfile {
|
||||
return {
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
private mapAttendance(row: any): Attendance {
|
||||
return {
|
||||
...row,
|
||||
checkInTime: new Date(row.checkInTime),
|
||||
checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined,
|
||||
createdAt: new Date(row.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
private mapRecommendation(row: any): Recommendation {
|
||||
return {
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
approvedAt: row.approvedAt ? new Date(row.approvedAt) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private mapFitnessGoal(row: any): FitnessGoal {
|
||||
return {
|
||||
...row,
|
||||
startDate: new Date(row.startDate),
|
||||
targetDate: row.targetDate ? new Date(row.targetDate) : undefined,
|
||||
completedDate: row.completedDate
|
||||
? new Date(row.completedDate)
|
||||
id: String(row.id),
|
||||
userId: String(row.userId),
|
||||
membershipType: String(row.membershipType) as Client["membershipType"],
|
||||
membershipStatus: String(
|
||||
row.membershipStatus,
|
||||
) as Client["membershipStatus"],
|
||||
joinDate: new Date(row.joinDate as number | Date),
|
||||
lastVisit: row.lastVisit
|
||||
? new Date(row.lastVisit as number | Date)
|
||||
: undefined,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt),
|
||||
emergencyContact: row.emergencyContactName
|
||||
? {
|
||||
name: String(row.emergencyContactName),
|
||||
phone: String(row.emergencyContactPhone ?? ""),
|
||||
relationship: String(row.emergencyContactRelationship ?? ""),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private mapFitnessProfile(row: Record<string, unknown>): FitnessProfile {
|
||||
return {
|
||||
id: String(row.id),
|
||||
userId: String(row.userId),
|
||||
height: typeof row.height === "number" ? row.height : undefined,
|
||||
weight: typeof row.weight === "number" ? row.weight : undefined,
|
||||
age: typeof row.age === "number" ? row.age : undefined,
|
||||
gender: row.gender
|
||||
? (String(row.gender) as FitnessProfile["gender"])
|
||||
: undefined,
|
||||
fitnessGoals: row.goals ? [String(row.goals)] : undefined,
|
||||
medicalConditions: row.medicalConditions
|
||||
? String(row.medicalConditions)
|
||||
: undefined,
|
||||
allergies: row.dietaryRestrictions
|
||||
? String(row.dietaryRestrictions)
|
||||
: undefined,
|
||||
injuries: undefined,
|
||||
activityLevel: row.activityLevel
|
||||
? (String(row.activityLevel) as FitnessProfile["activityLevel"])
|
||||
: undefined,
|
||||
createdAt: new Date(row.createdAt as number | Date),
|
||||
updatedAt: new Date(row.updatedAt as number | Date),
|
||||
};
|
||||
}
|
||||
|
||||
private mapAttendance(row: Record<string, unknown>): Attendance {
|
||||
return {
|
||||
id: String(row.id),
|
||||
userId: String(row.userId),
|
||||
type: String(row.type) as Attendance["type"],
|
||||
checkInTime: new Date(row.checkInTime as number | Date),
|
||||
checkOutTime: row.checkOutTime
|
||||
? new Date(row.checkOutTime as number | Date)
|
||||
: undefined,
|
||||
notes: row.notes ? String(row.notes) : undefined,
|
||||
createdAt: new Date(row.createdAt as number | Date),
|
||||
};
|
||||
}
|
||||
|
||||
private mapRecommendation(row: Record<string, unknown>): Recommendation {
|
||||
return {
|
||||
id: String(row.id),
|
||||
userId: String(row.userId),
|
||||
fitnessProfileId: String(row.fitnessProfileId),
|
||||
recommendationText: String(row.recommendationText),
|
||||
activityPlan: String(row.activityPlan),
|
||||
dietPlan: String(row.dietPlan),
|
||||
status: String(row.status) as Recommendation["status"],
|
||||
generatedAt: new Date(row.generatedAt as number | Date),
|
||||
approvedBy: row.approvedBy ? String(row.approvedBy) : undefined,
|
||||
approvedAt: row.approvedAt
|
||||
? new Date(row.approvedAt as number | Date)
|
||||
: undefined,
|
||||
createdAt: new Date(row.createdAt as number | Date),
|
||||
updatedAt: new Date(row.updatedAt as number | Date),
|
||||
};
|
||||
}
|
||||
|
||||
private mapFitnessGoal(row: Record<string, unknown>): FitnessGoal {
|
||||
return {
|
||||
id: String(row.id),
|
||||
userId: String(row.userId),
|
||||
fitnessProfileId: row.fitnessProfileId
|
||||
? String(row.fitnessProfileId)
|
||||
: undefined,
|
||||
goalType: String(row.goalType) as FitnessGoal["goalType"],
|
||||
title: String(row.title),
|
||||
description: row.description ? String(row.description) : undefined,
|
||||
targetValue:
|
||||
typeof row.targetValue === "number" ? row.targetValue : undefined,
|
||||
currentValue:
|
||||
typeof row.currentValue === "number" ? row.currentValue : undefined,
|
||||
unit: row.unit ? String(row.unit) : undefined,
|
||||
status: String(row.status) as FitnessGoal["status"],
|
||||
progress: typeof row.progress === "number" ? row.progress : 0,
|
||||
priority: String(row.priority) as FitnessGoal["priority"],
|
||||
startDate: new Date(row.startDate as number | Date),
|
||||
targetDate: row.targetDate
|
||||
? new Date(row.targetDate as number | Date)
|
||||
: undefined,
|
||||
completedDate: row.completedDate
|
||||
? new Date(row.completedDate as number | Date)
|
||||
: undefined,
|
||||
notes: row.notes ? String(row.notes) : undefined,
|
||||
createdAt: new Date(row.createdAt as number | Date),
|
||||
updatedAt: new Date(row.updatedAt as number | Date),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Recommendation,
|
||||
FitnessGoal,
|
||||
} from "@fitai/shared";
|
||||
import type { SortConfig, FilterCondition } from "../filtering";
|
||||
|
||||
// Database Entity Types
|
||||
export interface User extends SharedUser {
|
||||
@@ -33,11 +34,62 @@ export interface IDatabase {
|
||||
deleteUser(id: string): Promise<boolean>;
|
||||
migrateUserId(oldId: string, newId: string): Promise<void>;
|
||||
|
||||
// Optimized query methods (Phase 3 & 4 additions)
|
||||
getUsersWithPagination(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
role?: string;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{ users: User[]; total: number }>;
|
||||
|
||||
getUsersWithRelatedData(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
role?: string;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{
|
||||
users: Array<
|
||||
User & {
|
||||
client?: Client | null;
|
||||
isCheckedIn?: boolean;
|
||||
checkInTime?: Date | null;
|
||||
lastCheckInTime?: Date | null;
|
||||
checkInsThisWeek?: number;
|
||||
checkInsThisMonth?: number;
|
||||
}
|
||||
>;
|
||||
total?: number;
|
||||
}>;
|
||||
|
||||
getAttendanceStatsBatch(userIds: string[]): Promise<
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
isCheckedIn: boolean;
|
||||
checkInTime: Date | null;
|
||||
lastCheckInTime: Date | null;
|
||||
checkInsThisWeek: number;
|
||||
checkInsThisMonth: number;
|
||||
}
|
||||
>
|
||||
>;
|
||||
|
||||
// Client operations
|
||||
createClient(client: Omit<Client, "id">): Promise<Client>;
|
||||
getClientById(id: string): Promise<Client | null>;
|
||||
getClientByUserId(userId: string): Promise<Client | null>;
|
||||
getAllClients(): Promise<Client[]>;
|
||||
getClientsWithPagination(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{ clients: Client[]; total: number }>;
|
||||
updateClient(id: string, updates: Partial<Client>): Promise<Client | null>;
|
||||
deleteClient(id: string): Promise<boolean>;
|
||||
|
||||
@@ -62,6 +114,13 @@ export interface IDatabase {
|
||||
checkOut(attendanceId: string): Promise<Attendance | null>;
|
||||
getAttendanceHistory(userId: string): Promise<Attendance[]>;
|
||||
getAllAttendance(): Promise<Attendance[]>;
|
||||
getAttendanceWithPagination(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
sort?: SortConfig;
|
||||
filters?: FilterCondition[];
|
||||
search?: string;
|
||||
}): Promise<{ attendance: Attendance[]; total: number }>;
|
||||
getActiveCheckIn(userId: string): Promise<Attendance | null>;
|
||||
|
||||
// Recommendation operations
|
||||
|
||||
Reference in new issue
Block a user