import { z } from "zod"; // ============================================================================ // Common validation rules // ============================================================================ export const emailSchema = z.string().email("Invalid email address"); export const passwordSchema = z .string() .min(8, "Password must be at least 8 characters") .regex(/[A-Z]/, "Password must contain at least one uppercase letter") .regex(/[a-z]/, "Password must contain at least one lowercase letter") .regex(/[0-9]/, "Password must contain at least one number"); export const phoneSchema = z .string() .optional() .refine( (val) => !val || val.trim() === "" || /^\+?[1-9]\d{1,14}$/.test(val), { message: "Invalid phone number format", }, ); export const dateTimeSchema = z.string().datetime("Invalid datetime format"); // ============================================================================ // User schemas // ============================================================================ export const userRoleSchema = z.enum([ "client", "trainer", "admin", "superAdmin", ]); export const membershipTypeSchema = z.enum(["basic", "premium", "vip"]); export const membershipStatusSchema = z.enum([ "active", "inactive", "suspended", ]); export const userSchema = z.object({ email: emailSchema, password: passwordSchema, firstName: z.string().min(1, "First name is required").max(50), lastName: z.string().min(1, "Last name is required").max(50), phone: phoneSchema, role: userRoleSchema.default("client"), }); export const userUpdateSchema = userSchema.partial(); export const userLoginSchema = z.object({ email: emailSchema, password: z.string().min(1, "Password is required"), }); export const userInviteSchema = z.object({ email: emailSchema, firstName: z.string().min(1, "First name is required").max(50), lastName: z.string().min(1, "Last name is required").max(50), role: userRoleSchema, phone: phoneSchema, }); export const userUpdateWithIdSchema = z.object({ id: z.string().min(1, "User ID is required"), email: emailSchema.optional(), firstName: z.string().min(1, "First name is required").max(50).optional(), lastName: z.string().min(1, "Last name is required").max(50).optional(), role: userRoleSchema.optional(), phone: phoneSchema, gymId: z.string().nullable().optional(), membershipType: membershipTypeSchema.optional(), membershipStatus: membershipStatusSchema.optional(), }); // ============================================================================ // Fitness Goal schemas // ============================================================================ export const goalTypeSchema = z.enum([ "weight_target", "strength_milestone", "endurance_target", "flexibility_goal", "habit_building", "custom", ]); export const prioritySchema = z.enum(["low", "medium", "high"]); export const goalStatusSchema = z.enum(["active", "completed", "abandoned"]); export const fitnessGoalSchema = z.object({ userId: z.string().min(1, "User ID is required").optional(), // Optional for authenticated requests goalType: goalTypeSchema, title: z.string().min(1, "Title is required").max(100), description: z.string().max(500).optional(), targetValue: z.number().positive("Target value must be positive").optional(), currentValue: z .number() .nonnegative("Current value cannot be negative") .optional(), unit: z.string().max(20).optional(), targetDate: dateTimeSchema.optional(), priority: prioritySchema.default("medium"), status: goalStatusSchema.default("active"), notes: z.string().max(1000).optional(), }); export const fitnessGoalUpdateSchema = fitnessGoalSchema .omit({ userId: true }) .partial(); // ============================================================================ // Fitness Profile schemas // ============================================================================ export const genderSchema = z.enum([ "male", "female", "other", "prefer_not_to_say", ]); export const activityLevelSchema = z.enum([ "sedentary", "lightly_active", "moderately_active", "very_active", "extremely_active", ]); export const fitnessProfileSchema = z.object({ userId: z.string().min(1, "User ID is required"), height: z .number() .positive("Height must be positive") .max(300, "Invalid height value"), weight: z .number() .positive("Weight must be positive") .max(500, "Invalid weight value"), age: z .number() .int("Age must be an integer") .positive("Age must be positive") .min(13, "Must be at least 13 years old") .max(120, "Invalid age value"), gender: genderSchema.optional(), activityLevel: activityLevelSchema.optional(), fitnessGoals: z .array(z.string()) .max(10, "Maximum 10 goals allowed") .optional(), medicalConditions: z.string().max(1000).optional(), allergies: z.string().max(500).optional(), injuries: z.string().max(500).optional(), preferences: z.string().max(1000).optional(), }); export const fitnessProfileUpdateSchema = fitnessProfileSchema .omit({ userId: true }) .partial(); // ============================================================================ // Recommendation schemas // ============================================================================ export const recommendationTypeSchema = z.enum([ "workout", "nutrition", "lifestyle", "recovery", ]); export const recommendationStatusSchema = z.enum([ "pending", "accepted", "rejected", "completed", ]); export const recommendationSchema = z.object({ userId: z.string().min(1, "User ID is required"), type: recommendationTypeSchema, title: z.string().min(1, "Title is required").max(200), description: z.string().min(1, "Description is required").max(2000), reasoning: z.string().max(1000).optional(), priority: prioritySchema.default("medium"), status: recommendationStatusSchema.default("pending"), metadata: z.record(z.string(), z.unknown()).optional(), aiGenerated: z.boolean().default(false), }); export const recommendationUpdateSchema = recommendationSchema.partial().omit({ userId: true, }); export const generateRecommendationSchema = z.object({ userId: z.string().min(1, "User ID is required"), type: recommendationTypeSchema.optional(), count: z .number() .int() .positive() .min(1) .max(10, "Maximum 10 recommendations per request") .default(3), includeContext: z.boolean().default(true), }); // Legacy recommendation schema for backward compatibility export const legacyRecommendationSchema = z .object({ userId: z.string().min(1, "User ID is required"), fitnessProfileId: z.string().optional(), recommendationText: z.string().min(1).max(5000).optional(), activityPlan: z.string().max(5000).optional(), dietPlan: z.string().max(5000).optional(), type: z.string().optional(), content: z.string().max(5000).optional(), status: recommendationStatusSchema.optional(), }) .refine( (data) => (data.recommendationText && data.activityPlan && data.dietPlan) || (data.type && data.content), { message: "Either provide (recommendationText, activityPlan, dietPlan) or (type, content)", }, ); export const recommendationUpdateRequestSchema = z.object({ id: z.string().min(1, "Recommendation ID is required"), status: recommendationStatusSchema.optional(), recommendationText: z.string().max(5000).optional(), activityPlan: z.string().max(5000).optional(), dietPlan: z.string().max(5000).optional(), content: z.string().max(5000).optional(), }); // ============================================================================ // Attendance schemas // ============================================================================ export const attendanceSchema = z.object({ userId: z.string().min(1, "User ID is required"), gymId: z.string().min(1, "Gym ID is required"), checkInTime: dateTimeSchema, checkOutTime: dateTimeSchema.optional(), }); export const checkInSchema = z.object({ userId: z.string().min(1, "User ID is required"), gymId: z.string().min(1, "Gym ID is required"), }); export const checkOutSchema = z.object({ userId: z.string().min(1, "User ID is required"), attendanceId: z.string().min(1, "Attendance ID is required"), }); // ============================================================================ // Gym schemas // ============================================================================ export const gymSchema = z.object({ name: z.string().min(1, "Gym name is required").max(100), address: z.string().min(1, "Address is required").max(200), city: z.string().min(1, "City is required").max(100), state: z.string().min(2, "State is required").max(50), zipCode: z .string() .regex(/^\d{5}(-\d{4})?$/, "Invalid ZIP code format") .optional(), phone: phoneSchema, email: emailSchema.optional(), capacity: z.number().int().positive("Capacity must be positive").optional(), amenities: z.array(z.string()).max(50).optional(), operatingHours: z.string().max(500).optional(), }); export const gymUpdateSchema = gymSchema.partial(); export const assignUserToGymSchema = z.object({ userId: z.string().min(1, "User ID is required"), gymId: z.string().min(1, "Gym ID is required"), }); // ============================================================================ // Invitation schemas // ============================================================================ export const invitationSchema = z.object({ email: emailSchema, role: z.enum(["client", "trainer"]).default("client"), gymId: z.string().min(1, "Gym ID is required").optional(), expiresAt: dateTimeSchema.optional(), message: z.string().max(500).optional(), }); export const invitationUpdateSchema = z.object({ status: z.enum(["pending", "accepted", "rejected", "expired"]), }); // ============================================================================ // Query parameter schemas // ============================================================================ export const paginationSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), sortBy: z.string().optional(), sortOrder: z.enum(["asc", "desc"]).default("asc"), }); export const statusFilterSchema = z .enum(["active", "completed", "all"]) .default("all"); export const roleFilterSchema = z .enum(["client", "trainer", "admin", "superAdmin", "all"]) .default("all"); export const dateRangeSchema = z.object({ startDate: dateTimeSchema.optional(), endDate: dateTimeSchema.optional(), }); export const searchSchema = z.object({ query: z.string().min(1).max(100), fields: z.array(z.string()).max(10).optional(), }); // ============================================================================ // Type exports (inferred from schemas) // ============================================================================ export type User = z.infer; export type UserUpdate = z.infer; export type UserLogin = z.infer; export type UserRole = z.infer; export type MembershipType = z.infer; export type MembershipStatus = z.infer; export type FitnessGoal = z.infer; export type FitnessGoalUpdate = z.infer; export type GoalType = z.infer; export type GoalStatus = z.infer; export type FitnessProfile = z.infer; export type FitnessProfileUpdate = z.infer; export type Gender = z.infer; export type ActivityLevel = z.infer; export type Recommendation = z.infer; export type RecommendationUpdate = z.infer; export type RecommendationType = z.infer; export type RecommendationStatus = z.infer; export type GenerateRecommendation = z.infer< typeof generateRecommendationSchema >; export type Attendance = z.infer; export type CheckIn = z.infer; export type CheckOut = z.infer; export type Gym = z.infer; export type GymUpdate = z.infer; export type AssignUserToGym = z.infer; export type Invitation = z.infer; export type InvitationUpdate = z.infer; export type Pagination = z.infer; export type StatusFilter = z.infer; export type RoleFilter = z.infer; export type DateRange = z.infer; export type Search = z.infer; export type Priority = z.infer;