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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | 13x 3x 12x 12x 2x 2x 12x 4x 3x 4x 2x 12x 2x 12x 9x 9x 3x 4x 4x 3x 3x 1x 3x 2x 3x 3x 3x 3x 3x 2x 3x | /**
* Event Registration Service
*
* API service for fetching event registrations (participants).
* Story 3.3: Event Participants Tab
*/
import apiClient from './apiClient';
import type {
EventParticipant,
EventRegistrationsResponse,
FetchEventRegistrationsOptions,
} from '@/types/eventParticipant.types';
/**
* Backend API response format (field names differ from frontend)
*/
interface BackendRegistrationItem {
registrationCode: string;
eventCode: string;
status: string;
registrationDate: string;
attendeeUsername: string;
attendeeFirstName: string; // Backend uses attendeeFirstName
attendeeLastName: string; // Backend uses attendeeLastName
attendeeEmail: string; // Backend uses attendeeEmail
attendeeCompany?: string; // Company ID (optional)
waitlistPosition?: number | null; // Story 10.11 — 1-based; null unless status === WAITLIST
}
interface BackendPaginatedResponse {
data: BackendRegistrationItem[];
pagination: {
page: number;
limit: number;
totalItems: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}
/**
* Transform backend registration item to frontend EventParticipant type
*/
const transformRegistration = (backendReg: BackendRegistrationItem): EventParticipant => ({
registrationCode: backendReg.registrationCode,
eventCode: backendReg.eventCode,
attendeeUsername: backendReg.attendeeUsername,
firstName: backendReg.attendeeFirstName, // Transform from attendeeFirstName
lastName: backendReg.attendeeLastName, // Transform from attendeeLastName
email: backendReg.attendeeEmail, // Transform from attendeeEmail
company: backendReg.attendeeCompany
? { id: backendReg.attendeeCompany, name: backendReg.attendeeCompany }
: undefined,
status: backendReg.status as EventParticipant['status'],
registrationDate: backendReg.registrationDate,
// Epic 14 FR28: carry the authoritative 1-based queue position so the
// Waitlisted-filter view numbers rows by the backend, not by page index.
waitlistPosition: backendReg.waitlistPosition ?? null,
});
/**
* Fetch event registrations (participants) for a specific event
*
* @param eventCode - Event code to fetch registrations for
* @param options - Optional filters, pagination, and search
* @returns Registrations list and total count
*/
export const getEventRegistrations = async (
eventCode: string,
options?: Omit<FetchEventRegistrationsOptions, 'eventCode'>
): Promise<EventRegistrationsResponse> => {
// Build query parameters
const params: Record<string, unknown> = {};
// Add pagination
if (options?.pagination) {
params.page = options.pagination.page;
params.limit = options.pagination.limit;
}
// Add filters
if (options?.filters) {
if (options.filters.status && options.filters.status.length > 0) {
params.status = options.filters.status;
}
if (options.filters.companyId) {
params.companyId = options.filters.companyId;
}
}
// Add search
if (options?.search) {
params.search = options.search;
}
// Backend returns paginated response: { data: [...], pagination: {...} }
const response = await apiClient.get<BackendPaginatedResponse>(
`/events/${eventCode}/registrations`,
{ params }
);
// Transform backend field names to frontend format
const data = response.data.data.map(transformRegistration);
return {
data,
pagination: response.data.pagination,
};
};
/**
* Update registration status
*
* @param eventCode - Event code
* @param registrationCode - Registration code (e.g., BAT-2025-000123)
* @param status - New registration status
* @returns Updated registration
*/
export const updateRegistrationStatus = async (
eventCode: string,
registrationCode: string,
status: EventParticipant['status']
): Promise<EventParticipant> => {
// Backend expects lowercase status values to match database constraint
const lowercaseStatus = status.toLowerCase();
const response = await apiClient.patch<BackendRegistrationItem>(
`/events/${eventCode}/registrations/${registrationCode}`,
{ status: lowercaseStatus }
);
return transformRegistration(response.data);
};
/**
* Cancel registration (set status to CANCELLED)
*
* @param eventCode - Event code
* @param registrationCode - Registration code (e.g., BAT-2025-000123)
* @returns Updated registration with CANCELLED status
*/
export const cancelRegistration = async (
eventCode: string,
registrationCode: string
): Promise<EventParticipant> => {
return updateRegistrationStatus(eventCode, registrationCode, 'CANCELLED');
};
/**
* Delete registration (permanent removal)
*
* @param eventCode - Event code
* @param registrationCode - Registration code (e.g., BAT-2025-000123)
*/
export const deleteRegistration = async (
eventCode: string,
registrationCode: string
): Promise<void> => {
await apiClient.delete(`/events/${eventCode}/registrations/${registrationCode}`);
};
/**
* Promote a waitlisted registration to registered (Story 10.11 — AC3, AC5)
*
* ORGANIZER only. Promotes the specified waitlisted registration to status=registered.
*
* @param eventCode - Event code
* @param registrationCode - Registration code of the waitlisted attendee
* @throws 404 if registration not found; 409 if not on waitlist; 403 if not ORGANIZER
*/
export const promoteFromWaitlist = async (
eventCode: string,
registrationCode: string
): Promise<void> => {
await apiClient.post(`/events/${eventCode}/registrations/${registrationCode}/promote`);
};
/**
* Resend registration confirmation email (organizer only)
*
* Only works for registrations in REGISTERED (pending) status.
*
* @param eventCode - Event code
* @param registrationCode - Registration code
*/
export const resendConfirmationEmail = async (
eventCode: string,
registrationCode: string
): Promise<void> => {
await apiClient.post(
`/events/${eventCode}/registrations/${registrationCode}/resend-confirmation`
);
};
/**
* Enroll all organizers and partners as confirmed participants for an existing event.
*
* ORGANIZER only. Idempotent — already-registered users are counted as skipped.
*
* @param eventCode - Event code
* @returns Summary of enrolled and skipped counts
*/
export const enrollStakeholders = async (
eventCode: string
): Promise<{ enrolled: number; skipped: number }> => {
const response = await apiClient.post<{ enrolled: number; skipped: number }>(
`/events/${eventCode}/enroll-stakeholders`
);
return response.data;
};
/**
* Add an existing user as a confirmed participant (organizer only).
*
* @param eventCode - Event code
* @param params - `username` of the chosen existing user; `force` to add over capacity;
* `notify` to email the attendee a "you have a confirmed spot" notice (default true)
* @returns the created registration's code + status
* @throws 404 unknown event/user; 409 already-registered (generic) or capacity-full
* (`error.response.data.details.code === 'capacity_exceeded'`); 403 if not ORGANIZER
*/
export const addParticipant = async (
eventCode: string,
params: { username: string; force?: boolean; notify?: boolean }
): Promise<{ registrationCode: string; status: string; attendeeUsername: string }> => {
const response = await apiClient.post<{
registrationCode: string;
status: string;
attendeeUsername: string;
}>(`/events/${eventCode}/participants`, {
username: params.username,
force: params.force ?? false,
notify: params.notify ?? true,
});
return response.data;
};
|