fitaiProto/apps/admin/src/lib/validation/schemas.ts
echo b1f01208fa admin role enhancment
basic invitation flow
2026-03-18 13:56:51 +01:00

389 lines
13 KiB
TypeScript

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<typeof userSchema>;
export type UserUpdate = z.infer<typeof userUpdateSchema>;
export type UserLogin = z.infer<typeof userLoginSchema>;
export type UserRole = z.infer<typeof userRoleSchema>;
export type MembershipType = z.infer<typeof membershipTypeSchema>;
export type MembershipStatus = z.infer<typeof membershipStatusSchema>;
export type FitnessGoal = z.infer<typeof fitnessGoalSchema>;
export type FitnessGoalUpdate = z.infer<typeof fitnessGoalUpdateSchema>;
export type GoalType = z.infer<typeof goalTypeSchema>;
export type GoalStatus = z.infer<typeof goalStatusSchema>;
export type FitnessProfile = z.infer<typeof fitnessProfileSchema>;
export type FitnessProfileUpdate = z.infer<typeof fitnessProfileUpdateSchema>;
export type Gender = z.infer<typeof genderSchema>;
export type ActivityLevel = z.infer<typeof activityLevelSchema>;
export type Recommendation = z.infer<typeof recommendationSchema>;
export type RecommendationUpdate = z.infer<typeof recommendationUpdateSchema>;
export type RecommendationType = z.infer<typeof recommendationTypeSchema>;
export type RecommendationStatus = z.infer<typeof recommendationStatusSchema>;
export type GenerateRecommendation = z.infer<
typeof generateRecommendationSchema
>;
export type Attendance = z.infer<typeof attendanceSchema>;
export type CheckIn = z.infer<typeof checkInSchema>;
export type CheckOut = z.infer<typeof checkOutSchema>;
export type Gym = z.infer<typeof gymSchema>;
export type GymUpdate = z.infer<typeof gymUpdateSchema>;
export type AssignUserToGym = z.infer<typeof assignUserToGymSchema>;
export type Invitation = z.infer<typeof invitationSchema>;
export type InvitationUpdate = z.infer<typeof invitationUpdateSchema>;
export type Pagination = z.infer<typeof paginationSchema>;
export type StatusFilter = z.infer<typeof statusFilterSchema>;
export type RoleFilter = z.infer<typeof roleFilterSchema>;
export type DateRange = z.infer<typeof dateRangeSchema>;
export type Search = z.infer<typeof searchSchema>;
export type Priority = z.infer<typeof prioritySchema>;