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
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
|
|
}
|