import { z } from 'zod'; const trimmed = (max, min = 0) => z.string().trim().min(min).max(max); const siret = z.string().trim().max(20).refine( (value) => !value || /^\d{14}$/.test(value.replace(/\s/g, '')), 'doit contenir exactement 14 chiffres' ); export const idParam = z.object({ id: z.coerce.number().int().positive() }); export const loginSchema = z.object({ email: trimmed(254, 3).toLowerCase(), password: z.string().min(1).max(200), }); export const changePasswordSchema = z.object({ currentPassword: z.string().min(1).max(200), newPassword: z .string() .min(10, 'au moins 10 caractères') .max(200) .regex(/[a-zA-Z]/, 'doit contenir une lettre') .regex(/[0-9]/, 'doit contenir un chiffre'), }); export const demandeSchema = z.object({ nom: trimmed(120, 1), fonction: trimmed(120).optional().default(''), entreprise: trimmed(120, 1), email: trimmed(254, 3).email(), tel: trimmed(30, 6), }); export const inscriptionPublicSchema = z.object({ participants: z.array(trimmed(120, 1)).min(1).max(100), }); export const memberProfileSchema = z.object({ nom: trimmed(120, 1), secteur: trimmed(120, 1), categorie_id: z.coerce.number().int().positive().nullable().optional(), dirigeant: trimmed(120, 1), email: trimmed(254).email().or(z.literal('')).optional().default(''), tel: trimmed(30).optional().default(''), site: trimmed(200).optional().default(''), adresse: trimmed(300).optional().default(''), presentation: trimmed(2000).optional().default(''), }); export const adminMemberSchema = memberProfileSchema.extend({ valide: z.boolean().optional(), }); export const rencontreSchema = z.object({ titre: trimmed(200, 1), date_renc: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'format AAAA-MM-JJ'), heure: trimmed(20).optional().default(''), lieu: trimmed(200).optional().default(''), description: trimmed(2000).optional().default(''), places: z.coerce.number().int().min(0).max(100000), participants_par_compte: z.coerce.number().int().min(1).max(100).optional().default(1), }); export const inscriptionAdminSchema = z.object({ nom: trimmed(120, 1), entreprise: trimmed(120, 1), email: trimmed(254).email().or(z.literal('')).optional().default(''), tel: trimmed(30).optional().default(''), rencontre_id: z.coerce.number().int().positive(), statut: z.enum(['confirmee', 'en_attente']), }); export const rencontrePasseeSchema = z.object({ date_label: trimmed(60, 1), lieu: trimmed(200, 1), titre: trimmed(200, 1), texte: trimmed(2000, 1), // Manual fallback participant count; ignored when rencontre_id is set. participants: z.coerce.number().int().min(0).max(100000).optional().default(0), rencontre_id: z.coerce.number().int().positive().nullable().optional(), }); export const pastPhotoParam = z.object({ id: z.coerce.number().int().positive(), photoId: z.coerce.number().int().positive(), }); export const categorySchema = z.object({ name: trimmed(80, 1) }); export const staffRoleEnum = z.enum(['admin', 'moderator', 'treasurer']); export const staffUserSchema = z.object({ fullName: trimmed(120, 1), email: trimmed(254, 3).email(), role: staffRoleEnum, }); export const roleChangeSchema = z.object({ role: staffRoleEnum }); export const billingSeasonParam = z.object({ season: z.string().regex(/^\d{4}-\d{4}$/, 'format AAAA-AAAA'), }); const iban = z.string().trim().max(42).refine( (value) => !value || /^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(value.replace(/\s/g, '').toUpperCase()), 'IBAN invalide' ); export const billingSettingsSchema = z.object({ sluc_partner_amount_ht: z.coerce.number().min(0).max(1000000), non_partner_amount_ht: z.coerce.number().min(0).max(1000000), payment_due_days: z.coerce.number().int().min(0).max(365), iban, legal_mentions: trimmed(3000).optional().default(''), }); export const billingTypeSchema = z.object({ billing_type: z.enum(['sluc_partner', 'non_partner']), }); export const invoiceGenerationSchema = z.object({ member_ids: z.array(z.coerce.number().int().positive()).min(1).max(10000).optional(), }); export const paymentSchema = z.object({ payment_method: z.enum(['carte', 'virement', 'cheque']), paid_at: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'format AAAA-MM-JJ'), }); export const contentSchema = z.object({ hero_quote_text: trimmed(300).optional(), hero_quote_author: trimmed(120).optional(), association_name: trimmed(160, 1).optional(), association_address: trimmed(500).optional(), association_email: trimmed(254).email().or(z.literal('')).optional(), association_phone: trimmed(40).optional(), association_siret: siret.optional(), association_contact: trimmed(160).optional(), association_website: z.string().trim().url().max(300).or(z.literal('')).optional(), association_facebook_url: z.string().trim().url().max(500).or(z.literal('')).optional(), association_linkedin_url: z.string().trim().url().max(500).or(z.literal('')).optional(), association_president: trimmed(160).optional(), association_vice_president: trimmed(160).optional(), association_treasurer: trimmed(160).optional(), association_secretary: trimmed(160).optional(), association_board_members: trimmed(3000).optional(), // Legal pages (mentions légales & politique de confidentialité) association_rna: trimmed(30).optional(), association_host_name: trimmed(200).optional(), association_host_address: trimmed(300).optional(), association_host_tel: trimmed(30).optional(), legal_updated: trimmed(60).optional(), }); // Invitation email composer: every text zone is optional — absent fields // fall back to the default wording, an empty string hides the block. export const emailComposeSchema = z.object({ base: z.string().trim().url().max(300).optional(), greeting: z.string().trim().max(300).optional(), intro: z.string().trim().max(2000).optional(), outro: z.string().trim().max(2000).optional(), signature: z.string().trim().max(500).optional(), }); export const customEmailSchema = z.object({ base: z.string().trim().url().max(300).optional(), subject: trimmed(200, 1), kicker: trimmed(120).optional().default(''), title: trimmed(200, 1), body: trimmed(6000, 1), buttonLabel: trimmed(100).optional().default(''), buttonUrl: z.string().trim().url().max(500).or(z.literal('')).optional().default(''), signature: trimmed(500).optional().default(''), }).refine((data) => Boolean(data.buttonLabel) === Boolean(data.buttonUrl), { message: 'le texte et le lien du bouton doivent être renseignés ensemble', path: ['buttonUrl'], });