You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

559 lines
15 KiB
TypeScript

export type AOrBProps<A, B> = (Partial<B> & A) | (Partial<A> & 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<ApiRefreshTokenResponseSuccess>)
export type ApiTradeTokenResponseSuccess = ApiTokenResponseSuccess & {
registered: boolean;
}
export type ApiTradeTokenReponse = ApiTradeTokenResponseSuccess | ({ message: string } & Partial<ApiTradeTokenResponseSuccess>)
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<P extends number|null=number> {
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<E = {}> = {
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<NameProp>
& { origin_slug: string | undefined; }
export type BookingResponse = AOrBProps<BookingResponseOk, BookingResponseError>;
// ---------- 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<P extends string, T> = {
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<T extends CourtSlot> {
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
}