All files / src/viewmodels/ticket useEventActionsViewModel.ts

74.19% Statements 46/62
57.89% Branches 11/19
70% Functions 7/10
75.47% Lines 40/53

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 1141x 1x 1x 1x 1x     1x                                                   1x 5x 5x 5x 5x 5x   5x 2x               2x 2x   2x 2x 2x   4x   2x 2x         2x 1x 2x 2x 2x 2x       2x           2x       5x 2x     5x 5x 5x           5x 3x 3x 2x     5x                        
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { ApiError } from '@/services/api';
import { getEventDetail } from '@/services/eventService';
import { listMyTickets } from '@/services/ticketService';
import type { EventDetail } from '@/models/event';
import type { TicketListItem } from '@/models/ticket';
import i18n from '@/i18n';
 
export interface EventActionsViewModel {
  event: EventDetail | null;
  ticket: TicketListItem | null;
  isLoading: boolean;
  errorMessage: string | null;
  primaryActionLabel: string | null;
  canOpenTicket: boolean;
  canScanTicket: boolean;
  canEditEvent: boolean;
  reload: () => Promise<void>;
}
 
function getLoadErrorMessage(error: unknown): string {
  Iif (error instanceof ApiError && error.status === 401) {
    return i18n.t('tickets.actions.errors.loginRequired');
  }
 
  Iif (error instanceof ApiError) {
    return error.message;
  }
 
  return i18n.t('tickets.actions.errors.loadFailed');
}
 
export function useEventActionsViewModel(eventId: string): EventActionsViewModel {
  const { token } = useAuth();
  const [event, setEvent] = useState<EventDetail | null>(null);
  const [ticket, setTicket] = useState<TicketListItem | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
 
  const reload = useCallback(async () => {
    Iif (!token) {
      setEvent(null);
      setTicket(null);
      setErrorMessage(i18n.t('tickets.actions.errors.loginRequired'));
      setIsLoading(false);
      return;
    }
 
    setIsLoading(true);
    setErrorMessage(null);
 
    try {
      const eventDetail = await getEventDetail(eventId, token);
      setEvent(eventDetail);
 
      const findTicket = (items: TicketListItem[]) => items.find((item) => item.event.id === eventId) ?? null;
 
      let ticketsResponse = await listMyTickets(token).catch(() => ({ items: [] }));
      let foundTicket = findTicket(ticketsResponse.items);
 
      // RETRY LOGIC: If joined a PUBLIC event but ticket is not yet in the list,
      // the server might still be processing the async ticket creation.
      // We retry up to 2 times with a 1s delay (3 attempts total).
      if (!foundTicket && eventDetail.viewer_context.participation_status === 'JOINED' && eventDetail.privacy_level === 'PUBLIC') {
        for (let i = 0; i < 2; i++) {
          await new Promise(r => setTimeout(r, 1000));
          ticketsResponse = await listMyTickets(token).catch(() => ({ items: [] }));
          foundTicket = findTicket(ticketsResponse.items);
          if (foundTicket) break;
        }
      }
 
      setTicket(foundTicket);
    } catch (error) {
      setEvent(null);
      setTicket(null);
      setErrorMessage(getLoadErrorMessage(error));
    } finally {
      setIsLoading(false);
    }
  }, [eventId, token]);
 
  useEffect(() => {
    void reload();
  }, [reload]);
 
  const canScanTicket = Boolean(event?.viewer_context.is_host);
  const canOpenTicket = !canScanTicket && ticket != null;
  const canEditEvent = Boolean(
    event?.viewer_context.is_host &&
      event.status === 'ACTIVE' &&
      new Date(event.start_time).getTime() > Date.now(),
  );
 
  const primaryActionLabel = useMemo(() => {
    Iif (canScanTicket) return i18n.t('tickets.actions.scanTicket');
    if (canOpenTicket) return i18n.t('tickets.actions.viewTicket');
    return null;
  }, [canOpenTicket, canScanTicket]);
 
  return {
    event,
    ticket,
    isLoading,
    errorMessage,
    primaryActionLabel,
    canOpenTicket,
    canScanTicket,
    canEditEvent,
    reload,
  };
}