All files / src/components/organizer/EventManagement eventFormSchema.ts

86.36% Statements 19/22
62.5% Branches 10/16
100% Functions 6/6
90% Lines 18/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                      46x 46x               46x       3x 93x                                                             12x 2x 2x 2x                                                               1x 1x 10x 1x     1x               1x 1x     1x     1x    
/**
 * Shared event-identity form schema + helpers (Epic 14, Story 14.F.2).
 *
 * Extracted verbatim from EventForm so the modal AND the in-tab Details · Info
 * editor validate identically with zero drift. Pure module — no React, no
 * component state.
 */
 
import { z } from 'zod';
import type { PatchEventRequest } from '@/types/event.types';
import type { components } from '@/types/generated/events-core-api.types';
 
/**
 * Converts legacy lowercase snake_case event types to UPPER_CASE enum values
 * (migration from old `full_day` to new `FULL_DAY`).
 */
export function normalizeEventType(
  eventType: string | undefined
): components['schemas']['EventType'] {
  Iif (!eventType) return 'FULL_DAY';
  const typeMap: Record<string, components['schemas']['EventType']> = {
    full_day: 'FULL_DAY',
    afternoon: 'AFTERNOON',
    evening: 'EVENING',
    FULL_DAY: 'FULL_DAY',
    AFTERNOON: 'AFTERNOON',
    EVENING: 'EVENING',
  };
  return typeMap[eventType] || 'FULL_DAY';
}
 
/** Validation schema factory (needs `t` for translated messages). */
export const createEventSchema = (t: (key: string) => string) =>
  z
    .object({
      eventNumber: z.coerce
        .number({ message: t('validation.eventNumberRequired') })
        .positive(t('validation.eventNumberPositive')),
      title: z
        .string()
        .min(1, t('validation.titleRequired'))
        .min(10, t('validation.titleMinLength')),
      description: z.string().min(1, t('validation.descriptionRequired')),
      date: z.string().min(1, t('validation.eventDateRequired')),
      registrationDeadline: z.string().optional().or(z.literal('')),
      venueName: z.string().min(1, t('validation.venueNameRequired')),
      venueAddress: z.string().min(1, t('validation.venueAddressRequired')),
      venueCapacity: z.coerce
        .number({ message: t('validation.capacityRequired') })
        .positive(t('validation.capacityPositive')),
      workflowState: z.enum([
        'CREATED',
        'TOPIC_SELECTION',
        'SPEAKER_IDENTIFICATION',
        'SLOT_ASSIGNMENT',
        'AGENDA_PUBLISHED',
        'EVENT_LIVE',
        'EVENT_COMPLETED',
        'ARCHIVED',
      ]),
      eventType: z.enum(['FULL_DAY', 'AFTERNOON', 'EVENING']).optional(),
    })
    .refine(
      (data) => {
        if (!data.registrationDeadline) return true;
        const eventDate = new Date(data.date);
        const deadline = new Date(data.registrationDeadline);
        return deadline <= eventDate;
      },
      {
        message: t('validation.registrationDeadline'),
        path: ['registrationDeadline'],
      }
    );
 
/** Explicit form-data type (z.infer doesn't handle the transforms cleanly). */
export interface EventFormData {
  eventNumber: number;
  title: string;
  description: string;
  date: string;
  venueName: string;
  venueAddress: string;
  venueCapacity: number;
  registrationDeadline?: string;
  workflowState?: components['schemas']['EventWorkflowState'];
  eventType?: components['schemas']['EventType'];
}
 
/** Partial form data (all fields optional) — used for PATCH. */
export type PartialEventFormData = {
  [K in keyof EventFormData]?: EventFormData[K];
};
 
/** Return only the fields that differ from the initial snapshot (for PATCH). */
export function getChangedFields(
  currentData: PartialEventFormData,
  initialFormData: PartialEventFormData
): PartialEventFormData {
  const changedFields: Record<string, unknown> = {};
  (Object.keys(currentData) as Array<keyof EventFormData>).forEach((key) => {
    if (currentData[key] !== initialFormData[key]) {
      changedFields[key] = currentData[key];
    }
  });
  return changedFields as PartialEventFormData;
}
 
/**
 * Transform date-only fields (YYYY-MM-DD) to ISO 8601 (date-time) for PATCH,
 * per the OpenAPI spec.
 */
export function transformDatesForApi(data: PartialEventFormData): PatchEventRequest {
  const transformed = { ...data } as Record<string, unknown>;
  Iif (transformed.date && typeof transformed.date === 'string') {
    transformed.date = new Date(transformed.date).toISOString();
  }
  Iif (transformed.registrationDeadline && typeof transformed.registrationDeadline === 'string') {
    transformed.registrationDeadline = new Date(transformed.registrationDeadline).toISOString();
  }
  return transformed as PatchEventRequest;
}