user report gtound work

This commit is contained in:
dimitar committed 2026-03-19 03:37:15 +01:00
1 parent 1be2de05fa
commit 74f0d0dbed
42 files changed
+8127 -5

No files matched your search

+657
View File
@@ -7,6 +7,11 @@ import {
Recommendation,
FitnessGoal,
Notification,
DailyNutrition,
DailyHydration,
MealEntry,
FitnessProfileHistory,
TrainerClientAssignment,
DatabaseConfig,
} from "./types";
import {
@@ -18,6 +23,11 @@ import {
recommendations,
fitnessGoals,
notifications,
dailyNutrition,
dailyHydration,
mealEntries,
fitnessProfileHistory,
trainerClientAssignments,
eq,
and,
desc,
@@ -1524,4 +1534,651 @@ export class DrizzleDatabase implements IDatabase {
createdAt,
};
}
// ==================== DAILY NUTRITION OPERATIONS ====================
async createDailyNutrition(
nutrition: Omit<DailyNutrition, "id" | "createdAt" | "updatedAt">,
): Promise<DailyNutrition> {
const id = `nutrition_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const now = new Date();
const newNutrition = {
id,
...nutrition,
createdAt: now,
updatedAt: now,
};
await this.db.insert(dailyNutrition).values(newNutrition as any);
return this.mapDailyNutrition(newNutrition);
}
async getDailyNutrition(
userId: string,
date: string,
): Promise<DailyNutrition | null> {
const results = await this.db
.select()
.from(dailyNutrition)
.where(
and(eq(dailyNutrition.userId, userId), eq(dailyNutrition.date, date)),
)
.limit(1)
.all();
return results.length > 0 ? this.mapDailyNutrition(results[0]) : null;
}
async getDailyNutritionById(id: string): Promise<DailyNutrition | null> {
const results = await this.db
.select()
.from(dailyNutrition)
.where(eq(dailyNutrition.id, id))
.limit(1)
.all();
return results.length > 0 ? this.mapDailyNutrition(results[0]) : null;
}
async getDailyNutritionRange(
userId: string,
startDate: string,
endDate: string,
): Promise<DailyNutrition[]> {
const results = await this.db
.select()
.from(dailyNutrition)
.where(
and(
eq(dailyNutrition.userId, userId),
gte(dailyNutrition.date, startDate),
lte(dailyNutrition.date, endDate),
),
)
.orderBy(dailyNutrition.date)
.all();
return results.map((row) => this.mapDailyNutrition(row));
}
async updateDailyNutrition(
id: string,
updates: Partial<DailyNutrition>,
): Promise<DailyNutrition | null> {
const { id: _, ...updateData } = updates as any;
if (Object.keys(updateData).length === 0) {
const existing = await this.db
.select()
.from(dailyNutrition)
.where(eq(dailyNutrition.id, id))
.limit(1)
.all();
return existing.length > 0 ? this.mapDailyNutrition(existing[0]) : null;
}
await this.db
.update(dailyNutrition)
.set({ ...updateData, updatedAt: new Date() })
.where(eq(dailyNutrition.id, id))
.run();
const results = await this.db
.select()
.from(dailyNutrition)
.where(eq(dailyNutrition.id, id))
.limit(1)
.all();
return results.length > 0 ? this.mapDailyNutrition(results[0]) : null;
}
async deleteDailyNutrition(id: string): Promise<boolean> {
const result = await this.db
.delete(dailyNutrition)
.where(eq(dailyNutrition.id, id))
.run();
return result.changes > 0;
}
private mapDailyNutrition(row: Record<string, unknown>): DailyNutrition {
const createdAtValue = row.createdAt as number | Date;
const updatedAtValue = row.updatedAt as number | Date;
return {
id: String(row.id),
userId: String(row.userId),
date: String(row.date),
totalCalories: Number(row.totalCalories || 0),
calorieGoal: Number(row.calorieGoal || 2000),
meals: row.meals ? (JSON.parse(String(row.meals)) as any) : undefined,
createdAt:
typeof createdAtValue === "number"
? new Date(createdAtValue * 1000)
: new Date(createdAtValue),
updatedAt:
typeof updatedAtValue === "number"
? new Date(updatedAtValue * 1000)
: new Date(updatedAtValue),
};
}
// ==================== MEAL ENTRY OPERATIONS ====================
async createMealEntry(
meal: Omit<MealEntry, "id" | "createdAt">,
): Promise<MealEntry> {
const id = `meal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const now = new Date();
const newMeal = {
id,
...meal,
createdAt: now,
};
await this.db.insert(mealEntries).values(newMeal as any);
return this.mapMealEntry(newMeal);
}
async getMealEntriesByDate(
userId: string,
date: string,
): Promise<MealEntry[]> {
// Parse date string to get start and end of day in seconds
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const startTimestamp = Math.floor(startOfDay.getTime() / 1000);
const endTimestamp = Math.floor(endOfDay.getTime() / 1000);
const results = await this.db
.select()
.from(mealEntries)
.where(
and(
eq(mealEntries.userId, userId),
sql`${mealEntries.timestamp} >= ${startTimestamp}`,
sql`${mealEntries.timestamp} <= ${endTimestamp}`,
),
)
.orderBy(mealEntries.timestamp)
.all();
return results.map((row) => this.mapMealEntry(row));
}
async getMealEntryById(id: string): Promise<MealEntry | null> {
const results = await this.db
.select()
.from(mealEntries)
.where(eq(mealEntries.id, id))
.limit(1)
.all();
return results.length > 0 ? this.mapMealEntry(results[0]) : null;
}
async deleteMealEntry(id: string): Promise<boolean> {
const result = await this.db
.delete(mealEntries)
.where(eq(mealEntries.id, id))
.run();
return result.changes > 0;
}
private mapMealEntry(row: Record<string, unknown>): MealEntry {
const timestampValue = row.timestamp as number | Date;
const createdAtValue = row.createdAt as number | Date;
return {
id: String(row.id),
userId: String(row.userId),
dailyNutritionId: row.dailyNutritionId
? String(row.dailyNutritionId)
: undefined,
mealType: String(row.mealType) as MealEntry["mealType"],
foodName: String(row.foodName),
calories: Number(row.calories),
protein: row.protein ? Number(row.protein) : undefined,
carbs: row.carbs ? Number(row.carbs) : undefined,
fats: row.fats ? Number(row.fats) : undefined,
timestamp:
typeof timestampValue === "number"
? new Date(timestampValue * 1000)
: new Date(timestampValue),
createdAt:
typeof createdAtValue === "number"
? new Date(createdAtValue * 1000)
: new Date(createdAtValue),
};
}
// ==================== DAILY HYDRATION OPERATIONS ====================
async createDailyHydration(
hydration: Omit<DailyHydration, "id" | "createdAt" | "updatedAt">,
): Promise<DailyHydration> {
const id = `hydration_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const now = new Date();
const newHydration = {
id,
...hydration,
createdAt: now,
updatedAt: now,
};
await this.db.insert(dailyHydration).values(newHydration as any);
return this.mapDailyHydration(newHydration);
}
async getDailyHydration(
userId: string,
date: string,
): Promise<DailyHydration | null> {
const results = await this.db
.select()
.from(dailyHydration)
.where(
and(eq(dailyHydration.userId, userId), eq(dailyHydration.date, date)),
)
.limit(1)
.all();
return results.length > 0 ? this.mapDailyHydration(results[0]) : null;
}
async getDailyHydrationById(id: string): Promise<DailyHydration | null> {
const results = await this.db
.select()
.from(dailyHydration)
.where(eq(dailyHydration.id, id))
.limit(1)
.all();
return results.length > 0 ? this.mapDailyHydration(results[0]) : null;
}
async getDailyHydrationRange(
userId: string,
startDate: string,
endDate: string,
): Promise<DailyHydration[]> {
const results = await this.db
.select()
.from(dailyHydration)
.where(
and(
eq(dailyHydration.userId, userId),
gte(dailyHydration.date, startDate),
lte(dailyHydration.date, endDate),
),
)
.orderBy(dailyHydration.date)
.all();
return results.map((row) => this.mapDailyHydration(row));
}
async updateDailyHydration(
id: string,
updates: Partial<DailyHydration>,
): Promise<DailyHydration | null> {
const { id: _, ...updateData } = updates as any;
if (Object.keys(updateData).length === 0) {
const existing = await this.db
.select()
.from(dailyHydration)
.where(eq(dailyHydration.id, id))
.limit(1)
.all();
return existing.length > 0 ? this.mapDailyHydration(existing[0]) : null;
}
await this.db
.update(dailyHydration)
.set({ ...updateData, updatedAt: new Date() })
.where(eq(dailyHydration.id, id))
.run();
const results = await this.db
.select()
.from(dailyHydration)
.where(eq(dailyHydration.id, id))
.limit(1)
.all();
return results.length > 0 ? this.mapDailyHydration(results[0]) : null;
}
async deleteDailyHydration(id: string): Promise<boolean> {
const result = await this.db
.delete(dailyHydration)
.where(eq(dailyHydration.id, id))
.run();
return result.changes > 0;
}
private mapDailyHydration(row: Record<string, unknown>): DailyHydration {
const createdAtValue = row.createdAt as number | Date;
const updatedAtValue = row.updatedAt as number | Date;
return {
id: String(row.id),
userId: String(row.userId),
date: String(row.date),
totalWater: Number(row.totalWater || 0),
waterGoal: Number(row.waterGoal || 2000),
entries: row.entries
? (JSON.parse(String(row.entries)) as any)
: undefined,
createdAt:
typeof createdAtValue === "number"
? new Date(createdAtValue * 1000)
: new Date(createdAtValue),
updatedAt:
typeof updatedAtValue === "number"
? new Date(updatedAtValue * 1000)
: new Date(updatedAtValue),
};
}
// ==================== FITNESS PROFILE HISTORY OPERATIONS ====================
async createFitnessProfileHistory(
history: Omit<FitnessProfileHistory, "id" | "createdAt">,
): Promise<FitnessProfileHistory> {
const id = `profile_history_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const now = new Date();
const newHistory = {
id,
...history,
createdAt: now,
};
await this.db.insert(fitnessProfileHistory).values(newHistory as any);
return this.mapFitnessProfileHistory(newHistory);
}
async getFitnessProfileHistory(
userId: string,
startDate?: Date,
endDate?: Date,
): Promise<FitnessProfileHistory[]> {
const conditions = [eq(fitnessProfileHistory.userId, userId)];
if (startDate) {
const startTimestamp = Math.floor(startDate.getTime() / 1000);
conditions.push(
sql`${fitnessProfileHistory.changedAt} >= ${startTimestamp}`,
);
}
if (endDate) {
const endTimestamp = Math.floor(endDate.getTime() / 1000);
conditions.push(
sql`${fitnessProfileHistory.changedAt} <= ${endTimestamp}`,
);
}
const results = await this.db
.select()
.from(fitnessProfileHistory)
.where(and(...conditions))
.orderBy(desc(fitnessProfileHistory.changedAt))
.all();
return results.map((row) => this.mapFitnessProfileHistory(row));
}
async getWeightHistory(
userId: string,
startDate: Date,
endDate: Date,
): Promise<FitnessProfileHistory[]> {
const startTimestamp = Math.floor(startDate.getTime() / 1000);
const endTimestamp = Math.floor(endDate.getTime() / 1000);
const results = await this.db
.select()
.from(fitnessProfileHistory)
.where(
and(
eq(fitnessProfileHistory.userId, userId),
eq(fitnessProfileHistory.changeType, "weight"),
sql`${fitnessProfileHistory.changedAt} >= ${startTimestamp}`,
sql`${fitnessProfileHistory.changedAt} <= ${endTimestamp}`,
),
)
.orderBy(fitnessProfileHistory.changedAt)
.all();
return results.map((row) => this.mapFitnessProfileHistory(row));
}
private mapFitnessProfileHistory(
row: Record<string, unknown>,
): FitnessProfileHistory {
const changedAtValue = row.changedAt as number | Date;
const createdAtValue = row.createdAt as number | Date;
return {
id: String(row.id),
userId: String(row.userId),
fitnessProfileId: String(row.fitnessProfileId),
changeType: String(row.changeType) as FitnessProfileHistory["changeType"],
fieldName: String(row.fieldName),
previousValue: row.previousValue ? String(row.previousValue) : undefined,
newValue: row.newValue ? String(row.newValue) : undefined,
changedAt:
typeof changedAtValue === "number"
? new Date(changedAtValue * 1000)
: new Date(changedAtValue),
createdAt:
typeof createdAtValue === "number"
? new Date(createdAtValue * 1000)
: new Date(createdAtValue),
};
}
// ==================== TRAINER-CLIENT ASSIGNMENT OPERATIONS ====================
async createTrainerClientAssignment(
assignment: Omit<TrainerClientAssignment, "id" | "createdAt" | "updatedAt">,
): Promise<TrainerClientAssignment> {
const id = `assignment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const now = new Date();
const newAssignment = {
id,
...assignment,
createdAt: now,
updatedAt: now,
};
await this.db.insert(trainerClientAssignments).values(newAssignment as any);
return this.mapTrainerClientAssignment(newAssignment);
}
async getTrainerClientAssignments(
trainerId: string,
): Promise<TrainerClientAssignment[]> {
const results = await this.db
.select()
.from(trainerClientAssignments)
.where(
and(
eq(trainerClientAssignments.trainerId, trainerId),
eq(trainerClientAssignments.isActive, true),
),
)
.orderBy(desc(trainerClientAssignments.assignedAt))
.all();
return results.map((row) => this.mapTrainerClientAssignment(row));
}
async getClientTrainerAssignment(
clientId: string,
): Promise<TrainerClientAssignment | null> {
const results = await this.db
.select()
.from(trainerClientAssignments)
.where(
and(
eq(trainerClientAssignments.clientId, clientId),
eq(trainerClientAssignments.isActive, true),
),
)
.limit(1)
.all();
return results.length > 0
? this.mapTrainerClientAssignment(results[0])
: null;
}
async deleteTrainerClientAssignment(id: string): Promise<boolean> {
const result = await this.db
.delete(trainerClientAssignments)
.where(eq(trainerClientAssignments.id, id))
.run();
return result.changes > 0;
}
async deactivateTrainerClientAssignment(
id: string,
): Promise<TrainerClientAssignment | null> {
await this.db
.update(trainerClientAssignments)
.set({ isActive: false, updatedAt: new Date() })
.where(eq(trainerClientAssignments.id, id))
.run();
const results = await this.db
.select()
.from(trainerClientAssignments)
.where(eq(trainerClientAssignments.id, id))
.limit(1)
.all();
return results.length > 0
? this.mapTrainerClientAssignment(results[0])
: null;
}
private mapTrainerClientAssignment(
row: Record<string, unknown>,
): TrainerClientAssignment {
const assignedAtValue = row.assignedAt as number | Date;
const createdAtValue = row.createdAt as number | Date;
const updatedAtValue = row.updatedAt as number | Date;
return {
id: String(row.id),
trainerId: String(row.trainerId),
clientId: String(row.clientId),
assignedAt:
typeof assignedAtValue === "number"
? new Date(assignedAtValue * 1000)
: new Date(assignedAtValue),
assignedBy: row.assignedBy ? String(row.assignedBy) : undefined,
isActive: Boolean(row.isActive),
createdAt:
typeof createdAtValue === "number"
? new Date(createdAtValue * 1000)
: new Date(createdAtValue),
updatedAt:
typeof updatedAtValue === "number"
? new Date(updatedAtValue * 1000)
: new Date(updatedAtValue),
};
}
// ==================== ENHANCED ATTENDANCE OPERATIONS FOR REPORTS ====================
async getAttendanceByWeek(
userId: string,
weekStart: Date,
): Promise<Attendance[]> {
// Calculate week end (Sunday at 23:59:59)
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
const startTimestamp = Math.floor(weekStart.getTime() / 1000);
const endTimestamp = Math.floor(weekEnd.getTime() / 1000);
const results = await this.db
.select()
.from(attendance)
.where(
and(
eq(attendance.userId, userId),
sql`${attendance.checkInTime} >= ${startTimestamp}`,
sql`${attendance.checkInTime} <= ${endTimestamp}`,
),
)
.orderBy(attendance.checkInTime)
.all();
return results.map((row) => this.mapAttendance(row));
}
async calculateWeeklyCheckInStats(
userId: string,
weekStart: Date,
): Promise<{
totalCheckIns: number;
totalTimeSpent: number;
avgSessionDuration: number;
byType: { gym: number; class: number; personal_training: number };
}> {
const attendanceRecords = await this.getAttendanceByWeek(userId, weekStart);
const stats = {
totalCheckIns: attendanceRecords.length,
totalTimeSpent: 0,
avgSessionDuration: 0,
byType: {
gym: 0,
class: 0,
personal_training: 0,
},
};
let completedSessions = 0;
for (const record of attendanceRecords) {
// Count by type
if (record.type === "gym") stats.byType.gym++;
else if (record.type === "class") stats.byType.class++;
else if (record.type === "personal_training")
stats.byType.personal_training++;
// Calculate time spent (only for completed sessions with check-out)
if (record.checkOutTime) {
const duration =
(record.checkOutTime.getTime() - record.checkInTime.getTime()) /
(1000 * 60); // in minutes
stats.totalTimeSpent += duration;
completedSessions++;
}
}
// Calculate average session duration
if (completedSessions > 0) {
stats.avgSessionDuration = stats.totalTimeSpent / completedSessions;
}
return stats;
}
}