399 lines
13 KiB
TypeScript
399 lines
13 KiB
TypeScript
import {
|
|
sqliteTable,
|
|
text,
|
|
integer,
|
|
real,
|
|
index,
|
|
unique,
|
|
} 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,
|
|
),
|
|
}),
|
|
);
|
|
|
|
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;
|