export type AOrBProps = (Partial & A) | (Partial & B); export type Remote = { origin_logo_url: string; origin_name: string; src_timezone: string; } & RemoteSlugProp; export type Sport = { duration: number; sport_logo_url: string; sport_name: string; sport_slug: string; }; export type RemoteMemberIdProp = { remote_member_id: number; } export type BaseRemoteMemberData = { glicko_level: number | null; self_reported_level: number | null; } & RemoteMemberIdProp; export type RemoteSlugProp = { origin_slug: string; } export type FullAccountStrProp = { full_account_str: string; } export type RemoteMember = BaseRemoteMemberData & FullAccountStrProp; // API response types (with TranslatedField wrappers) export interface ApiUserSettingsResponse { onboarding: TranslatedField<'onboarding', { dismissed: boolean}>; default_remote_sport: TranslatedField<'default_remote_sport', DefaultRemoteSport>; default_remote_member_id: TranslatedField<'default_remote_member_id', DefaultRemoteMemberId>; } export type ApiRemoteMember = RemoteMember & RemoteSlugProp & { position: number|null; }; export type ApiAppUser = { app_user_id: number; } export type ApiUserContextPayload = { settings: ApiUserSettingsResponse; remote_members: ApiRemoteMember[]; app_user: ApiAppUser; }; // Legacy token-based auth types (deprecated, kept for backward compatibility) /** @deprecated Use ApiUserSettingsResponse instead */ export type ApiTokenTradUserSettings = ApiUserSettingsResponse; /** @deprecated Use ApiRemoteMember instead */ export type ApiTokenTradeRemoteMember = ApiRemoteMember; /** @deprecated Use ApiAppUser instead */ export type ApiTokenTradeAppUser = ApiAppUser; /** @deprecated Token-based auth is deprecated, type kept for backward compatibility */ export type ApiTokenTradePayload = { token: string; settings: ApiUserSettingsResponse; remote_members: ApiRemoteMember[]; app_user: ApiAppUser; }; export type RemoteApiTradeTokenResponse = { code: 'INTERNAL_TOKEN_GENERATED', message: 'Token traded successfully.', payload: ApiTokenTradePayload, status: 'success' } | { code: 'INVALID_TOKEN' | 'DATABASE_ERROR' | 'SERVER_ERROR' | 'USER_NOT_FOUND', message: 'Token traded successfully.', payload?: undefined, status: 'fail' } export type ApiTokenResponseSuccess = { settings: ApiUserSettingsResponse; remote_members: ApiRemoteMember[]; app_user: ApiAppUser; } export type ApiRefreshTokenResponseSuccess = ApiTokenResponseSuccess & { tokenExpiry: number; } export type ApiRefreshTokenReponse = ApiRefreshTokenResponseSuccess | ({ message: string } & Partial) export type ApiTradeTokenResponseSuccess = ApiTokenResponseSuccess & { registered: boolean; } export type ApiTradeTokenReponse = ApiTradeTokenResponseSuccess | ({ message: string } & Partial) export type NameProp = { name: string; } export type PositionProp = { position: number; /** Position index coming from the backend (0-3). */ } // ---------------- remote responses export type MaybeAppUserIdProp = { app_user_id: number | null; } export type BookedByProp = RemoteMemberIdProp & MaybeAppUserIdProp; export interface PendingPositionRequest { request_id: number; requesting_remote_member_id: number; approving_remote_member_id: number; requested_position: number; } export interface MatchTypeApproval { app_user_id: number; status: 'pending' | 'approved' | 'rejected' | 'forced'; created_at?: string; updated_at?: string | null; email?: string; } export interface PendingMatchTypeRequest { request_id: number; court_slot_id: number; initiator_app_user_id: number; target_match_type_id: number; is_forced: boolean; created_at?: string; approvals: MatchTypeApproval[]; } export interface MatchType { match_type_id: number; name: string; importance_level: string; requires_score: boolean; } export type GlickoLevel = { rating: number; confidence: number; games_played: number; }; export interface Player

{ remote_member_id: number; full_account_str: string; app_user_id: number | null; self_reported_level: number | null; glicko_level: number | null; position: P; } export interface PendingJoinRequest { request_id: number; created_at: string; player: Player; } export type CourtSlot = { remote: Remote; sport: Sport; booked_by: BookedByProp | null; court: string; end: string; i: number | null; players: Player[]; slot_id: number; start: string; status: SlotStatus; match_type_id?: number; updated_at?: string; data_source?: 'cache' | 'remote'; fetched_at?: string; fetch_blocked_reason?: 'slot_started' | 'slot_free' | null; outcome?: { winning_team: number; team_0_score: number; team_1_score: number; }; score_submissions?: ScoreSubmission[]; pending_position_requests?: PendingPositionRequest[]; pending_position_swap_groups?: PendingPositionSwapGroup[]; pending_join_requests?: PendingJoinRequest[]; pending_match_type_requests?: PendingMatchTypeRequest[]; punctuality_reports?: { reporter_app_user_id: number; reports: { app_user_id?: number; remote_member_id?: number; minutes_late: number; }[]; update_count: number; }[]; punctuality_consensus?: { app_user_id?: number; remote_member_id?: number; consensus_minutes_late: number; consensus_weight: number; status: 'consensus' | 'disputed' | 'pending'; reporter_count: number; }[] } & E export interface PendingRequest { request_id: number; requesting_remote_member_id: number; approving_remote_member_id: number; requested_position: number; } export interface SwapApproval { approval_id: number; remote_member_id: number; app_user_id: number | null; old_position: number; new_position: number; approved: boolean; approved_at: string | null; } export interface PendingPositionSwapGroup { swap_group_id: string; initiator_remote_member_id: number; swap_chain: [number, number][]; initial_positions: number[]; final_positions: number[]; created_at: string; approvals: SwapApproval[]; } export type JoinRequestList = { slot_id: string; requests: PendingJoinRequest[] }[]; export type BookingResponseOk = { booking: CourtSlot; match_types?: MatchType[]; session_token: string; } export type BookingResponseError = { message: string; } // ------- inferred locally export type PlayerMeta = { /** The {x, y} coordinates for the avatar on the court. */ team?: string; isBooker: boolean; isCurrentUser: boolean; isCoach: boolean; } export type CourtDisplayMeta = { currentPosition: number; // This is always a logical position (0-3) } // For internal use where we need explicit logical position export type LogicalPositionProp = { logicalPosition: number; } export type PlayerWithMeta = Player & PlayerMeta; export type PlayerWithDisplayMeta = PlayerWithMeta & CourtDisplayMeta; export enum SlotStatus { available='available', booked='booked', pending='pending', coach='coach', club='club', } export type DurationProp = { duration: number; // minutes } export type StatusProp = { status: SlotStatus; } export type RemoteProp = { remote: Remote; } export type SportProp = { sport: Sport; } export type CourtSlotWithDurationAndStatus = CourtSlot< DurationProp & StatusProp & RemoteProp & SportProp >; export type RemoteMemberWithNameAndRemoteSlug = RemoteMember & NameProp & { origin_slug: string | undefined; } export type RemoteMemberWithRemoteSlugAndMaybeName = RemoteMember & Partial & { origin_slug: string | undefined; } export type BookingResponse = AOrBProps; // ---------- user settings export interface DefaultRemoteSport { facility_slug: string | null; sport_slug: string | null; } export interface DefaultRemoteMemberId { remotes?: { origin_slug: string; remote_member_id: number }[]; } export interface UserSettings { onboarding: { dismissed: boolean }; default_remote_sport: DefaultRemoteSport; default_remote_member_id: DefaultRemoteMemberId; } /** * Transforms API settings response (with TranslatedField wrappers) into UserSettings format. * Extracts the .value property from each TranslatedField. */ export function transformApiSettings(apiSettings: ApiUserSettingsResponse): UserSettings { // Validate that all required fields with TranslatedField wrappers are present if (!apiSettings.onboarding?.value) { throw new Error('API settings missing required field: onboarding.value. Received: ' + JSON.stringify(apiSettings)); } if (!apiSettings.default_remote_sport) { throw new Error('API settings missing required field: default_remote_sport. Received: ' + JSON.stringify(apiSettings)); } if (!apiSettings.default_remote_member_id) { throw new Error('API settings missing required field: default_remote_member_id. Received: ' + JSON.stringify(apiSettings)); } return { onboarding: apiSettings.onboarding.value, default_remote_sport: apiSettings.default_remote_sport.value, default_remote_member_id: apiSettings.default_remote_member_id.value, }; } export type TranslatedField

= { description: string; name: P; value: T; } // ---------- token refresh export type RefreshTokenResponse = | { code: 'INTERNAL_TOKEN_GENERATED'; message: string; payload: ApiTokenTradePayload; status: 'success'; } | { code: string; message: string; status: 'fail'; payload?: undefined; }; export interface CourtColumn { courtName: string; slots: T[]; } export type SlotStatusFilter = "all" | "available" | "booked" | "pending"; export type CourtSlotAugmentation = { duration: number; status: SlotStatus; opacity: number; isVisible: boolean; } export type FilterAndProcessSlotsFunc = (slots: CourtSlot[], overrideFilter?: SlotStatusFilter) => (CourtSlot & CourtSlotAugmentation)[]; // New types for the migration export interface TeamMember { app_user_id?: number; remote_member_id: number; } export interface ScoreSet { games?: [number, number]; // Regular games score tiebreak?: [number, number]; // Tiebreak score if applicable } // Legacy alias for migration period export type ScoreSetV2 = ScoreSet; // Legacy format for backward compatibility export type ScoreSetLegacy = [number, number] | { team1: number; team2: number }; // Input format for score submission (what frontend sends) export interface ScoreSetInput { team1?: number; team2?: number; tiebreak1?: number; tiebreak2?: number; } export interface ScoreSubmission { reporter_app_user_id: number; score: { teams: (TeamMember[] | any[])[]; // Updated to support new format sets: (ScoreSet | ScoreSetLegacy)[]; // Support both formats during migration }; sets_provided: boolean; update_count?: number; last_updated_at?: string; } // ============================================================================ // Public Profile API Response Types // ============================================================================ /** * GET /user/{app_user_id}/public/{sport_slug} * GET /member/{remote_member_id}/public/{sport_slug} * * Both endpoints return the same response structure. * The only difference is the input parameter (app_user_id vs remote_member_id). */ // Success Response (HTTP 200) export interface PublicProfileSuccessResponse { status: 'success'; venues: Venue[]; profile: PlayerProfile; } // Error Response (HTTP 404 or 400) export interface PublicProfileErrorResponse { status: 'fail'; message: string; code: 'USER_NOT_FOUND' | 'MEMBER_NOT_FOUND' | 'UNKNOWN_SPORT' | string; } // Union type for the complete response export type PublicProfileResponse = PublicProfileSuccessResponse | PublicProfileErrorResponse; // ============================================================================ // Core Profile Types // ============================================================================ export interface Venue { origin_id: number; slug: string; name: string; logo_url: string | null; } export interface PlayerProfile { player_info: PlayerInfo; sport_info: SportInfo; reliability: ReliabilityMetrics; recent_activity: RecentActivity; match_statistics: MatchStatistics; frequent_players: FrequentPlayer[]; recent_matches: RecentMatch[]; } export interface PlayerInfo { app_user_id: number | null; remote_member_id: number; full_account_str: string; member_since: string | null; // ISO 8601 date string is_active: boolean; } export interface SportInfo { sport_name: string; sport_slug: string; self_reported_level: string | number | null; self_reported_level_info: { level: string | number; reported_at: string | null; // ISO 8601 date string } | null; glicko_level_info: { rating: number; // Rounded to 1 decimal deviation: number; // Rounded to 1 decimal volatility: number; // Rounded to 4 decimals matches_played: number; last_updated: string | null; // ISO 8601 date string } | null; } export interface ReliabilityMetrics { score: number | null; // Overall reliability score (0-100), null if no matches reporting_rate: number; // Percentage (0-100), rounded to 1 decimal consensus_rate: number; // Percentage (0-100), rounded to 1 decimal matches_played: number; matches_reported: number; matches_in_consensus: number; matches_disputed: number; } export interface RecentActivity { matches_last_30_days: number; matches_last_90_days: number; favorite_venue: string | null; // Venue slug } export interface MatchStatistics { total_matches: number; // Always present, 0 if no matches wins: number; // Always present, 0 if no wins losses: number; // Always present, 0 if no losses win_rate: number | null; // Percentage (0-100), rounded to 1 decimal, null if no results recent_form: string; // Up to 5 chars, e.g., "WLWLW", empty string if no results longest_win_streak: number; // Always present, 0 if no streaks current_streak: number; // Positive = win streak, Negative = loss streak, 0 if none } export interface FrequentPlayer { remote_member_id: number; app_user_id: number | null; full_account_str: string; total_matches: number; as_teammate: number; as_opponent: number; self_reported_level: string | number | null; glicko_level: number | null; // Rounded to 1 decimal } export interface RecentMatch { slot_id: string; // Court slot ID as string court: string; // Court name/number start_time: string | null; // ISO 8601 date string origin_slug: string; // Venue slug sport_slug: string; players: MatchPlayer[]; // Array of 2-4 players depending on sport status: string; // e.g., "booked", "completed" match_outcome: MatchOutcome; } export interface MatchPlayer { position: number; // 0-3 for doubles (0,1 = team 1, 2,3 = team 2) remote_member_id: number; app_user_id: number | null; full_account_str: string; glicko_level: number | null; // Rounded to 1 decimal self_reported_level: string | number | null; } export interface MatchOutcome { score: ScoreData | null; sets_provided: boolean | null; confidence: 'pending' | 'confirmed' | 'trusted' | 'waiting' | 'disputed' | 'needsReview'; } export interface ScoreData { teams: number[][]; // Array of teams, each team is array of remote_member_ids sets: ScoreSet[]; // Array of set scores }