fitaiProto/packages/database/src/schema.ts
2026-03-19 03:37:15 +01:00

600 lines
19 KiB
TypeScript

import {
sqliteTable,
text,
integer,
real,
index,
unique,
uniqueIndex,
} from "drizzle-orm/sqlite-core";
export const users = sqliteTable(
"users",
{
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
firstName: text("first_name").notNull(),
lastName: text("last_name").notNull(),
password: text("password"), // Optional - Clerk handles authentication
role: text("role", {
enum: ["superAdmin", "admin", "trainer", "client"],
})
.notNull()
.default("client"),
phone: text("phone"),
gymId: text("gym_id"), // FK reference added after gyms table
expoPushToken: text("expo_push_token"), // For push notifications
deviceType: text("device_type", { enum: ["ios", "android"] }), // Device platform
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
emailIdx: index("users_email_idx").on(table.email),
gymIdIdx: index("users_gym_id_idx").on(table.gymId),
roleIdx: index("users_role_idx").on(table.role),
expoPushTokenIdx: index("users_expo_push_token_idx").on(
table.expoPushToken,
),
}),
);
export const gyms = sqliteTable(
"gyms",
{
id: text("id").primaryKey(),
name: text("name").notNull(),
location: text("location"),
status: text("status", { enum: ["active", "inactive"] })
.notNull()
.default("active"),
adminUserId: text("admin_user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
adminUserIdIdx: index("gyms_admin_user_id_idx").on(table.adminUserId),
statusIdx: index("gyms_status_idx").on(table.status),
}),
);
export const clients = sqliteTable(
"clients",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.unique()
.references(() => users.id, { onDelete: "cascade" }),
membershipType: text("membership_type", {
enum: ["basic", "premium", "vip"],
})
.notNull()
.default("basic"),
membershipStatus: text("membership_status", {
enum: ["active", "inactive", "suspended"],
})
.notNull()
.default("active"),
joinDate: integer("join_date", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
lastVisit: integer("last_visit", { mode: "timestamp" }),
emergencyContactName: text("emergency_contact_name"),
emergencyContactPhone: text("emergency_contact_phone"),
emergencyContactRelationship: text("emergency_contact_relationship"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("clients_user_id_idx").on(table.userId),
membershipStatusIdx: index("clients_membership_status_idx").on(
table.membershipStatus,
),
}),
);
export const payments = sqliteTable(
"payments",
{
id: text("id").primaryKey(),
clientId: text("client_id")
.notNull()
.references(() => clients.id, { onDelete: "cascade" }),
amount: real("amount").notNull(),
currency: text("currency").notNull().default("USD"),
status: text("status", {
enum: ["pending", "completed", "failed", "refunded"],
})
.notNull()
.default("pending"),
paymentMethod: text("payment_method", {
enum: ["cash", "card", "bank_transfer"],
}).notNull(),
dueDate: integer("due_date", { mode: "timestamp" }).notNull(),
paidAt: integer("paid_at", { mode: "timestamp" }),
description: text("description").notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
clientIdIdx: index("payments_client_id_idx").on(table.clientId),
statusIdx: index("payments_status_idx").on(table.status),
dueDateIdx: index("payments_due_date_idx").on(table.dueDate),
}),
);
export const attendance = sqliteTable(
"attendance",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
checkInTime: integer("check_in_time", { mode: "timestamp" }).notNull(),
checkOutTime: integer("check_out_time", { mode: "timestamp" }),
type: text("type", { enum: ["gym", "class", "personal_training"] })
.notNull()
.default("gym"),
notes: text("notes"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("attendance_user_id_idx").on(table.userId),
checkInTimeIdx: index("attendance_check_in_time_idx").on(table.checkInTime),
// Composite index for common query: get user's attendance history
userCheckInIdx: index("attendance_user_check_in_idx").on(
table.userId,
table.checkInTime,
),
}),
);
export const notifications = sqliteTable(
"notifications",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
title: text("title").notNull(),
message: text("message").notNull(),
type: text("type", {
enum: ["payment_reminder", "attendance", "promotion", "system"],
}).notNull(),
read: integer("read", { mode: "boolean" }).notNull().default(false),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("notifications_user_id_idx").on(table.userId),
readIdx: index("notifications_read_idx").on(table.read),
// Composite index for common query: get user's unread notifications
userReadIdx: index("notifications_user_read_idx").on(
table.userId,
table.read,
),
}),
);
export const fitnessProfiles = sqliteTable(
"fitness_profiles",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.unique()
.references(() => users.id, { onDelete: "cascade" }),
height: real("height"), // in cm
weight: real("weight"), // in kg
age: integer("age"),
gender: text("gender", {
enum: ["male", "female", "other", "prefer_not_to_say"],
}),
fitnessGoals: text("fitness_goals", { mode: "json" }).$type<string[]>(),
activityLevel: text("activity_level", {
enum: [
"sedentary",
"lightly_active",
"moderately_active",
"very_active",
"extremely_active",
],
}),
medicalConditions: text("medical_conditions"),
allergies: text("allergies"),
injuries: text("injuries"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("fitness_profiles_user_id_idx").on(table.userId),
}),
);
export const fitnessGoals = sqliteTable(
"fitness_goals",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
fitnessProfileId: text("fitness_profile_id").references(
() => fitnessProfiles.id,
{ onDelete: "cascade" },
),
// Goal details
goalType: text("goal_type", {
enum: [
"weight_target",
"strength_milestone",
"endurance_target",
"flexibility_goal",
"habit_building",
"custom",
],
}).notNull(),
title: text("title").notNull(),
description: text("description"),
// Measurable targets
targetValue: real("target_value"), // e.g., 70 (kg), 100 (kg bench press)
currentValue: real("current_value"), // Current progress
unit: text("unit"), // kg, km, reps, etc.
// Timeline
startDate: integer("start_date", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
targetDate: integer("target_date", { mode: "timestamp" }),
completedDate: integer("completed_date", { mode: "timestamp" }),
// Status tracking
status: text("status", {
enum: ["active", "completed", "abandoned", "paused"],
})
.notNull()
.default("active"),
progress: real("progress").default(0), // 0-100 percentage
// Metadata
priority: text("priority", {
enum: ["low", "medium", "high"],
}).default("medium"),
notes: text("notes"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("fitness_goals_user_id_idx").on(table.userId),
statusIdx: index("fitness_goals_status_idx").on(table.status),
// Composite index for common query: get user's active goals
userStatusIdx: index("fitness_goals_user_status_idx").on(
table.userId,
table.status,
),
}),
);
// Removed local invitations table; Clerk invitations are the source of truth
export const trainerClients = sqliteTable(
"trainer_clients",
{
id: text("id").primaryKey(),
trainerUserId: text("trainer_user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
clientUserId: text("client_user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
gymId: text("gym_id")
.notNull()
.references(() => gyms.id, { onDelete: "cascade" }),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
trainerIdIdx: index("trainer_clients_trainer_id_idx").on(
table.trainerUserId,
),
clientIdIdx: index("trainer_clients_client_id_idx").on(table.clientUserId),
gymIdIdx: index("trainer_clients_gym_id_idx").on(table.gymId),
// Composite unique constraint: prevent duplicate trainer-client assignments
trainerClientUnique: unique("trainer_client_unique").on(
table.trainerUserId,
table.clientUserId,
),
}),
);
export const recommendations = sqliteTable(
"recommendations",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
fitnessProfileId: text("fitness_profile_id")
.notNull()
.references(() => fitnessProfiles.id, { onDelete: "cascade" }),
recommendationText: text("recommendation_text").notNull(),
activityPlan: text("activity_plan").notNull(),
dietPlan: text("diet_plan").notNull(),
status: text("status", {
enum: ["pending", "approved", "rejected"],
})
.notNull()
.default("pending"),
generatedAt: integer("generated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
approvedAt: integer("approved_at", { mode: "timestamp" }),
approvedBy: text("approved_by"), // User ID of admin/trainer
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("recommendations_user_id_idx").on(table.userId),
statusIdx: index("recommendations_status_idx").on(table.status),
fitnessProfileIdIdx: index("recommendations_fitness_profile_id_idx").on(
table.fitnessProfileId,
),
}),
);
// Daily nutrition tracking table
export const dailyNutrition = sqliteTable(
"daily_nutrition",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
date: text("date").notNull(), // YYYY-MM-DD format
totalCalories: real("total_calories").notNull().default(0),
calorieGoal: real("calorie_goal").notNull().default(2000),
meals: text("meals", { mode: "json" }).$type<
Array<{
type: "breakfast" | "lunch" | "dinner" | "snack";
name: string;
calories: number;
time?: string;
}>
>(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("daily_nutrition_user_id_idx").on(table.userId),
dateIdx: index("daily_nutrition_date_idx").on(table.date),
userDateIdx: uniqueIndex("daily_nutrition_user_date_idx").on(
table.userId,
table.date,
),
}),
);
// Individual meal entries table
export const mealEntries = sqliteTable(
"meal_entries",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
dailyNutritionId: text("daily_nutrition_id").references(
() => dailyNutrition.id,
{ onDelete: "cascade" },
),
mealType: text("meal_type", {
enum: ["breakfast", "lunch", "dinner", "snack"],
}).notNull(),
foodName: text("food_name").notNull(),
calories: real("calories").notNull(),
protein: real("protein"), // grams (optional)
carbs: real("carbs"), // grams (optional)
fats: real("fats"), // grams (optional)
timestamp: integer("timestamp", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("meal_entries_user_id_idx").on(table.userId),
timestampIdx: index("meal_entries_timestamp_idx").on(table.timestamp),
dailyNutritionIdIdx: index("meal_entries_daily_nutrition_id_idx").on(
table.dailyNutritionId,
),
}),
);
// Daily hydration tracking table
export const dailyHydration = sqliteTable(
"daily_hydration",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
date: text("date").notNull(), // YYYY-MM-DD format
totalWater: real("total_water").notNull().default(0), // ml
waterGoal: real("water_goal").notNull().default(2000), // ml
entries: text("entries", { mode: "json" }).$type<
Array<{ amount: number; time: string }>
>(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("daily_hydration_user_id_idx").on(table.userId),
dateIdx: index("daily_hydration_date_idx").on(table.date),
userDateIdx: uniqueIndex("daily_hydration_user_date_idx").on(
table.userId,
table.date,
),
}),
);
// Fitness profile history tracking table
export const fitnessProfileHistory = sqliteTable(
"fitness_profile_history",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
fitnessProfileId: text("fitness_profile_id")
.notNull()
.references(() => fitnessProfiles.id, { onDelete: "cascade" }),
changeType: text("change_type", {
enum: [
"weight",
"height",
"age",
"activity_level",
"goals",
"medical",
"other",
],
}).notNull(),
fieldName: text("field_name").notNull(), // "weight", "height", etc.
previousValue: text("previous_value"), // JSON string for flexibility
newValue: text("new_value"), // JSON string
changedAt: integer("changed_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
userIdIdx: index("fitness_profile_history_user_id_idx").on(table.userId),
changedAtIdx: index("fitness_profile_history_changed_at_idx").on(
table.changedAt,
),
userChangedIdx: index("fitness_profile_history_user_changed_idx").on(
table.userId,
table.changedAt,
),
changeTypeIdx: index("fitness_profile_history_change_type_idx").on(
table.changeType,
),
}),
);
// Trainer-client assignment table
export const trainerClientAssignments = sqliteTable(
"trainer_client_assignments",
{
id: text("id").primaryKey(),
trainerId: text("trainer_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
clientId: text("client_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
assignedAt: integer("assigned_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
assignedBy: text("assigned_by").references(() => users.id),
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
trainerIdx: index("trainer_client_assignments_trainer_idx").on(
table.trainerId,
),
clientIdx: index("trainer_client_assignments_client_idx").on(
table.clientId,
),
trainerClientIdx: uniqueIndex(
"trainer_client_assignments_trainer_client_idx",
).on(table.trainerId, table.clientId),
isActiveIdx: index("trainer_client_assignments_is_active_idx").on(
table.isActive,
),
}),
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Client = typeof clients.$inferSelect;
export type NewClient = typeof clients.$inferInsert;
export type Payment = typeof payments.$inferSelect;
export type NewPayment = typeof payments.$inferInsert;
export type Attendance = typeof attendance.$inferSelect;
export type NewAttendance = typeof attendance.$inferInsert;
export type Notification = typeof notifications.$inferSelect;
export type NewNotification = typeof notifications.$inferInsert;
export type FitnessProfile = typeof fitnessProfiles.$inferSelect;
export type NewFitnessProfile = typeof fitnessProfiles.$inferInsert;
export type FitnessGoal = typeof fitnessGoals.$inferSelect;
export type NewFitnessGoal = typeof fitnessGoals.$inferInsert;
export type Recommendation = typeof recommendations.$inferSelect;
export type NewRecommendation = typeof recommendations.$inferInsert;
export type DailyNutrition = typeof dailyNutrition.$inferSelect;
export type NewDailyNutrition = typeof dailyNutrition.$inferInsert;
export type MealEntry = typeof mealEntries.$inferSelect;
export type NewMealEntry = typeof mealEntries.$inferInsert;
export type DailyHydration = typeof dailyHydration.$inferSelect;
export type NewDailyHydration = typeof dailyHydration.$inferInsert;
export type FitnessProfileHistory = typeof fitnessProfileHistory.$inferSelect;
export type NewFitnessProfileHistory =
typeof fitnessProfileHistory.$inferInsert;
export type TrainerClientAssignment =
typeof trainerClientAssignments.$inferSelect;
export type NewTrainerClientAssignment =
typeof trainerClientAssignments.$inferInsert;