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 | 1x 24x 7x 5x 2x 4x 2x 8x 1x | /**
* Speaker Content Service (Story 5.5)
*
* HTTP client for Speaker Content Submission APIs
* Features:
* - Submit speaker presentation content (title, abstract)
* - Get speaker content details
* - JWT authentication via interceptors
* - Error handling with correlation IDs
*/
import apiClient from '@/services/api/apiClient';
import type { SpeakerPoolEntry } from '@/types/speakerPool.types';
// API base path
const EVENTS_API_PATH = '/events';
/**
* Submit Content Request DTO.
*
* Story 11.D.4 (AC8 + AC9) — `username` removed from the wire contract (the 11.C.2
* `ContentSubmissionService.submit()` resolves the speaker username from `speaker_pool`
* server-side; the Java DTO is `@JsonIgnoreProperties(ignoreUnknown = false)` and a
* legacy `username` field on the request body now returns 400).
*
* Optional `bio`, `profilePictureUrl`, `presentationUploadId` extend the on-behalf form
* with bio + portrait + presentation upload entry points. The backend patches `User.bio`
* and `User.profile_picture_url` via `UserApiClient.patchUserProfile` (Story 11.C.2 AR14).
*/
export interface SubmitContentRequest {
presentationTitle: string;
presentationAbstract: string;
bio?: string;
profilePictureUrl?: string;
presentationUploadId?: string;
}
/**
* Speaker Content Response DTO
*/
export interface SpeakerContentResponse {
poolId: string;
sessionId?: string;
status: string;
presentationTitle?: string;
presentationAbstract?: string;
username?: string;
warning?: string;
// Material fields (AC7)
hasMaterial?: boolean;
materialUrl?: string;
materialFileName?: string;
}
/**
* Quality Review Request DTO
*/
export interface ReviewRequest {
action: 'APPROVE' | 'REJECT';
feedback?: string;
}
/**
* Speaker Content Service
*
* Handles all HTTP requests for speaker content submission
*/
class SpeakerContentService {
/**
* Submit speaker content (AC6-10)
*
* Creates session + session_users junction and updates speaker_pool.status to CONTENT_SUBMITTED
*
* @param eventCode Event code (e.g., "BATbern56")
* @param poolId Speaker pool UUID
* @param request Content submission data (username, title, abstract)
* @returns Created content details with session ID and updated status
* @throws Error if validation fails (400), speaker not found (404), or invalid state (422)
*/
async submitContent(
eventCode: string,
poolId: string,
request: SubmitContentRequest
): Promise<SpeakerContentResponse> {
const response = await apiClient.post<SpeakerContentResponse>(
`${EVENTS_API_PATH}/${eventCode}/speakers/${poolId}/content`,
request
);
return response.data;
}
/**
* Get speaker content (AC34)
*
* Retrieves presentation details for a speaker
* Handles orphaned session FK (session deleted) gracefully
*
* @param eventCode Event code (e.g., "BATbern56")
* @param poolId Speaker pool UUID
* @returns Content details or warning if content lost
* @throws Error if speaker not found (404) or unauthorized (401, 403)
*/
async getSpeakerContent(eventCode: string, poolId: string): Promise<SpeakerContentResponse> {
const response = await apiClient.get<SpeakerContentResponse>(
`${EVENTS_API_PATH}/${eventCode}/speakers/${poolId}/content`
);
return response.data;
}
/**
* Get review queue (AC11)
*
* Retrieves all speakers pending quality review (status=CONTENT_SUBMITTED)
*
* @param eventCode Event code (e.g., "BATbern56")
* @returns List of speakers pending review
* @throws Error if unauthorized (401, 403)
*/
async getReviewQueue(eventCode: string): Promise<SpeakerPoolEntry[]> {
const response = await apiClient.get<SpeakerPoolEntry[]>(
`${EVENTS_API_PATH}/${eventCode}/speakers/review-queue`
);
return response.data;
}
/**
* Review content (approve or reject) (AC13-14)
*
* Approves or rejects speaker content quality review
*
* @param eventCode Event code (e.g., "BATbern56")
* @param poolId Speaker pool UUID
* @param request Review request (approve/reject with optional feedback)
* @returns void (204 No Content)
* @throws Error if validation fails (400), speaker not found (404), or unauthorized (401, 403)
*/
async reviewContent(eventCode: string, poolId: string, request: ReviewRequest): Promise<void> {
await apiClient.post(`${EVENTS_API_PATH}/${eventCode}/speakers/${poolId}/review`, request);
}
}
// Export singleton instance
export const speakerContentService = new SpeakerContentService();
// Export class for testing
export default SpeakerContentService;
|