Skip to main content

Overview

All form inputs in Quality Hub GINEZ are validated using Zod schemas with custom error messages in Spanish.
Validation rules are defined in lib/validations.ts

Authentication Forms

Login Form

const LoginSchema = z.object({
  email: z
    .string()
    .min(1, "El correo es obligatorio")
    .email("Formato de correo inválido")
    .max(255, "El correo no puede exceder 255 caracteres"),
  password: z
    .string()
    .min(6, "La contraseña debe tener al menos 6 caracteres")
    .max(128, "La contraseña no puede exceder 128 caracteres"),
})
Validation Rules:
email
string
required
  • Must not be empty
  • Must be valid email format
  • Maximum 255 characters
password
string
required
  • Minimum 6 characters
  • Maximum 128 characters

Registration Form

const RegisterSchema = LoginSchema.extend({
  full_name: z
    .string()
    .min(2, "El nombre debe tener al menos 2 caracteres")
    .max(100, "El nombre no puede exceder 100 caracteres")
    .regex(
      /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\s]+$/,
      "El nombre solo puede contener letras y espacios"
    ),
  role: z.enum(
    [
      "preparador",
      "gerente_sucursal",
      "director_operaciones",
      "gerente_calidad",
      "mostrador",
      "cajera",
      "director_compras",
    ],
    { errorMap: () => ({ message: "Selecciona un rol válido" }) }
  ),
  sucursal: z
    .string()
    .min(1, "La sucursal es obligatoria")
    .refine(
      (val) => SUCURSALES.includes(val),
      "Selecciona una sucursal válida"
    ),
})
Additional Rules:
full_name
string
required
  • Minimum 2 characters
  • Maximum 100 characters
  • Only letters (including Spanish accents), spaces allowed
  • Pattern: /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\s]+$/
role
enum
required
Must be one of:
  • preparador
  • gerente_sucursal
  • director_operaciones
  • gerente_calidad
  • mostrador
  • cajera
  • director_compras
sucursal
string
required
  • Must not be empty
  • Must exist in SUCURSALES constant (40 valid locations)

Bitácora Form

Required Fields

sucursal: z
  .string()
  .min(1, "La sucursal es obligatoria")
  .refine(
    (val) => SUCURSALES.includes(val),
    "Selecciona una sucursal válida"
  )
sucursal
string
required
  • Must be one of 40 valid branch locations
  • Validated against SUCURSALES constant
nombre_preparador: z
  .string()
  .min(1, "El nombre del preparador es obligatorio")
  .max(200, "El nombre no puede exceder 200 caracteres")
nombre_preparador
string
required
  • Minimum 1 character
  • Maximum 200 characters
fecha_fabricacion: z
  .string()
  .min(1, "La fecha de fabricación es obligatoria")
  .regex(/^\d{4}-\d{2}-\d{2}$/, "Formato de fecha inválido (YYYY-MM-DD)")
fecha_fabricacion
string
required
  • Format: YYYY-MM-DD
  • Pattern: /^\d{4}-\d{2}-\d{2}$/
  • Example: “2024-03-15”
codigo_producto: z
  .string()
  .min(1, "El código del producto es obligatorio")
  .max(20, "Código de producto inválido")
  .regex(
    /^[A-Z0-9-]+$/,
    "El código solo puede contener letras mayúsculas, números y guiones"
  )
codigo_producto
string
required
  • Minimum 1 character
  • Maximum 20 characters
  • Only uppercase letters, numbers, and hyphens
  • Pattern: /^[A-Z0-9-]+$/
  • Examples: “LIMLIM”, “TRALIM”, “REFARO-SUE”
tamano_lote: z
  .string()
  .min(1, "El tamaño de lote es obligatorio")
  .refine(
    (val) => !isNaN(parseFloat(val)) && parseFloat(val) > 0,
    "El tamaño de lote debe ser un número positivo"
  )
tamano_lote
string
required
  • Must be parseable as a number
  • Must be greater than 0
  • Represents batch size in liters or kilograms

Optional Numeric Fields

const optionalNumericString = z
  .string()
  .transform((val) => (val === "" ? null : val))
  .nullable()
Fields using this helper:
  • temp_med1 - Temperature for first measurement
  • temp_med2 - Temperature for second measurement
  • viscosidad_seg - Viscosity in seconds
  • temperatura - General temperature

pH Validation

ph: z
  .string()
  .refine(
    (val) => {
      if (val === "") return true // Optional when empty
      const num = parseFloat(val)
      return !isNaN(num) && num >= 0 && num <= 14
    },
    "El pH debe estar entre 0 y 14"
  )
  .default("")
ph
string
  • Optional (empty string allowed)
  • If provided: must be between 0 and 14
  • Only required for products in PH_STANDARDS

Solids Validation

const optionalSolidsString = z
  .string()
  .refine(
    (val) => {
      if (val === "") return true
      const num = parseFloat(val)
      return !isNaN(num) && num >= 0 && num <= 55
    },
    "El % de sólidos debe estar entre 0 y 55"
  )
  .default("")
solidos_medicion_1
string
  • Optional (empty string allowed)
  • If provided: must be between 0 and 55
  • Percentage value
solidos_medicion_2
string
  • Same validation as solidos_medicion_1
  • Used for double-checking measurements

Categorical Fields

color: z.enum(["CONFORME", "NO CONFORME"], {
  errorMap: () => ({ message: "Selecciona conformidad del color" }),
})
color
enum
required
Must be one of:
  • CONFORME
  • NO CONFORME
apariencia: z.string().min(1, "La apariencia es obligatoria")
apariencia
string
required
Expected values (validated against APPEARANCE_STANDARDS):
  • CRISTALINO
  • OPACO
  • APERLADO
aroma: z.enum(["CONFORME", "NO CONFORME"], {
  errorMap: () => ({ message: "Selecciona conformidad del aroma" }),
})
aroma
enum
required
Must be one of:
  • CONFORME
  • NO CONFORME
contaminacion_microbiologica: z.enum(["SIN PRESENCIA", "CON PRESENCIA"], {
  errorMap: () => ({ message: "Selecciona estado de contaminación" }),
})
contaminacion_microbiologica
enum
required
Must be one of:
  • SIN PRESENCIA
  • CON PRESENCIA

Observations

observaciones: z
  .string()
  .max(1000, "Las observaciones no pueden exceder 1000 caracteres")
  .default("")
observaciones
string
  • Optional field
  • Maximum 1000 characters
  • Free-form text

Business Logic Validation

Product Code Validation

Beyond schema validation, product codes must exist in:
  • PRODUCT_STANDARDS (for solids)
  • PH_STANDARDS (if pH applicable)
  • APPEARANCE_STANDARDS (for appearance)
  • PARAMETER_APPLICABILITY (for determining required fields)

Dynamic Field Requirements

Fields become required based on PARAMETER_APPLICABILITY:
if (PARAMETER_APPLICABILITY[productCode].solidos) {
  // solidos_medicion_1 required
}

if (PARAMETER_APPLICABILITY[productCode].ph) {
  // ph required
}

Conformity Logic

After validation, measurements are classified:
// Solids conformity
if (solids >= min && solids <= max) {
  status = 'CONFORME'
} else if (solids >= min * 0.95 && solids <= max * 1.05) {
  status = 'SEMI-CONFORME'
} else {
  status = 'NO CONFORME'
}

// pH conformity
if (ph >= phMin && ph <= phMax) {
  status = 'CONFORME'
} else {
  status = 'NO CONFORME'
}

// Appearance conformity
if (appearance === APPEARANCE_STANDARDS[productCode]) {
  status = 'CONFORME'
} else {
  status = 'NO CONFORME'
}

Error Handling

validateForm Helper

const result = validateForm(BitacoraSchema, formData)

if (result.success) {
  // result.data is type-safe
} else {
  // result.errors is Record<string, string>
  // Format: { "field_name": "Error message" }
}

getFirstError Helper

const firstError = getFirstError(result.errors)
// Returns: "El correo es obligatorio"
Useful for displaying a single error in toast notifications.

Client-Side vs Server-Side

Client-Side Validation

  • Real-time feedback in forms
  • Uses Zod schemas
  • Prevents invalid submissions

Server-Side Validation

  • Supabase RLS policies
  • Database constraints
  • Type checking at runtime
Always validate on both client and server. Client-side validation improves UX, but server-side validation ensures data integrity.

Source Code Reference

File: lib/validations.ts All validation schemas and helpers are defined in this file.

Build docs developers (and LLMs) love