feat: migrate to API v2.0 with facility-based routing (Build 358+)
continuous-integration/drone/push Build is passing Details

Complete migration to match backend breaking changes from Build 355+ and Build 358+:

**Terminology Migration (Build 355+):**
- Rename club → facility (club_id → facility_id, club_name → facility_name)
- Rename remote → origin/provider (remote_server_id → origin_id, remote_type → provider)
- Update all field references: remote_name → origin_name, remote_logo_url → origin_logo_url

**Facility-Based Routing (Build 358+):**
- Change routing from provider-based to facility-based
- Update URL parameters: remote_slug → facility_slug in all booking/slot endpoints
- Update discovery endpoint: /remotes → /facilities
- Update slot responses: remote object → facility object with new field structure
- Update user settings: default_remote_sport.origin_slug → facility_slug

**Files Updated:**
- 32 files modified (TypeScript types, API clients, hooks, components)
- 150+ field name changes across the codebase
- All response interfaces updated to match new API contract

**Verification:**
- TypeScript compilation: ✓ No errors
- ESLint: ✓ No migration-related issues

BREAKING CHANGES: Requires backend Build 358+ for facility-based routing
master
Guillermo Pages 3 months ago
parent 3b7f937505
commit 6ebc57dd36

@ -602,7 +602,7 @@ export default function BookingAdminTestComponent() {
</div>
<div>
<div className="text-slate-600 font-semibold">Court</div>
<div className="text-slate-900">{booking.slot.court.name} (Club: {booking.slot.court.club_name})</div>
<div className="text-slate-900">{booking.slot.court.name} (Club: {booking.slot.court.facility_name})</div>
</div>
<div>
<div className="text-slate-600 font-semibold">Capacity</div>

@ -139,8 +139,8 @@ export default function AdminClubsList() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{clubs.map((club) => (
<Link
key={club.club_id}
href={`/${locale}/admin/clubs/${club.club_id}`}
key={club.facility_id}
href={`/${locale}/admin/clubs/${club.facility_id}`}
className="group bg-white border-2 border-slate-200 rounded-2xl p-6 hover:border-slate-300 hover:shadow-xl transition-all duration-200"
>
<div className="flex items-start justify-between mb-5">

@ -179,7 +179,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
<div className="space-y-4">
<div className="flex justify-between items-center py-3 border-b border-slate-100">
<span className="text-slate-600 font-medium">{t('Type')}:</span>
<span className="font-semibold text-slate-900">{clubDetail.provider.remote_type}</span>
<span className="font-semibold text-slate-900">{clubDetail.provider.provider}</span>
</div>
<div className="flex justify-between items-center py-3 border-b border-slate-100">
<span className="text-slate-600 font-medium">{t('Manages Slot Storage')}:</span>

@ -144,7 +144,7 @@ export default function CourtSlotCard({ currentUser, slot, remoteSlug, sportSlug
const buildShareMessage = () => {
const playerNames = (slot.players || []).map(p => cleanName(p.full_account_str));
if (playerNames.length === 0 && settings && settings.default_remote_member_id.remotes) {
const claimed = settings.default_remote_member_id.remotes.find(r => r.remote_slug === remoteSlug);
const claimed = settings.default_remote_member_id.remotes.find(r => r.origin_slug === remoteSlug);
if (claimed) {
const member = remoteMembers.find(m => m.remote_member_id === claimed.remote_member_id);
if (member) playerNames.push(cleanName(member.full_account_str));

@ -162,8 +162,8 @@ export default function Navigation({ pageTitle }: NavigationProps) {
};
const bookingLink = localizedLink(
settings?.default_remote_sport.remote_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.remote_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/today`
settings?.default_remote_sport.facility_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.facility_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/today`
: '/select-remote'
);
@ -373,7 +373,7 @@ export default function Navigation({ pageTitle }: NavigationProps) {
>
<div
onClick={() => {
const remoteSlug = settings?.default_remote_sport.remote_slug;
const remoteSlug = settings?.default_remote_sport.facility_slug;
const sportSlug = settings?.default_remote_sport.sport_slug;
const hasCreds = !!getClaimedRemoteMember(settings);
closeMenu();
@ -528,7 +528,7 @@ export default function Navigation({ pageTitle }: NavigationProps) {
isOpen={showPartnerModal}
onClose={() => setShowPartnerModal(false)}
onSelectPartner={() => {}}
remoteSlug={settings?.default_remote_sport.remote_slug || ''}
remoteSlug={settings?.default_remote_sport.facility_slug || ''}
sportSlug={settings?.default_remote_sport.sport_slug || ''}
selectedPartners={[]}
context="lookup"

@ -45,7 +45,7 @@ export default function PastBookingCard({ booking, currentUserId, index, onRepor
{/* Row 3: Remote info - Mobile */}
<div className="text-xs text-slate-500 mb-4 sm:hidden">
{booking.remote_name} {t('Court {court}', { court: booking.court })}
{booking.origin_name} {t('Court {court}', { court: booking.court })}
</div>
{/* Desktop header - hidden on mobile */}
@ -62,7 +62,7 @@ export default function PastBookingCard({ booking, currentUserId, index, onRepor
{dateInfo.day}
</div>
<div className="text-xs text-slate-500">
{booking.remote_name} {t('Court {court}', { court: booking.court })}
{booking.origin_name} {t('Court {court}', { court: booking.court })}
</div>
</div>

@ -62,7 +62,7 @@ export default function PendingScoresList({ onReport }: Props) {
<div className="text-sm text-slate-500 font-medium">
{new Date(b.start).toLocaleDateString()} {new Date(b.start).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}
</div>
<div className="text-lg font-semibold text-slate-700">{b.remote_name}</div>
<div className="text-lg font-semibold text-slate-700">{b.origin_name}</div>
<div className="text-sm text-slate-600">{b.sport_name} Court {b.court}</div>
{/* Player display */}
{b.players && b.players.length > 0 && (

@ -14,7 +14,7 @@ export default function PlayerPicker({ isOpen, onClose, remoteSlug, onSelect }:
const { remoteMembers } = useUserSettings()
const { t } = useTranslation()
if (!isOpen) return null
const members = remoteMembers.filter(m => m.remote_slug === remoteSlug)
const members = remoteMembers.filter(m => m.origin_slug === remoteSlug)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6 animate-fadeIn">

@ -5,7 +5,7 @@ import useTranslation from '@/src/hooks/useTranslation';
import apiFetch from '@/src/utils/apiFetch';
interface RemoteInfo {
remote_name: string;
origin_name: string;
logo_url?: string;
sport_name: string;
sport_logo_url?: string;
@ -28,12 +28,12 @@ export default function RemoteInfo({ remoteSlug, sportSlug, remotesData, isLoadi
// If remotesData is provided, use it instead of fetching
if (remotesData) {
const remote = (remotesData.remotes || []).find((r: any) => r.remote_slug === remoteSlug);
const remote = (remotesData.remotes || []).find((r: any) => r.origin_slug === remoteSlug);
const sport = remote?.sports?.find((s: any) => s.sport_slug === sportSlug);
if (remote && sport) {
setRemoteInfo({
remote_name: remote.remote_name,
logo_url: remote.remote_logo_url || remote.logo_url,
origin_name: remote.origin_name,
logo_url: remote.origin_logo_url || remote.logo_url,
sport_name: sport.sport_name,
sport_logo_url: sport.sport_logo_url || sport.logo_url || `/images/${sportSlug}-logo.png`,
});
@ -46,15 +46,15 @@ export default function RemoteInfo({ remoteSlug, sportSlug, remotesData, isLoadi
// Fallback to fetching if no data provided
setLoading(true);
apiFetch(`/remotes`)
apiFetch(`/facilities`)
.then(res => res.json())
.then(data => {
const remote = (data.remotes || []).find((r: any) => r.remote_slug === remoteSlug);
const remote = (data.remotes || []).find((r: any) => r.origin_slug === remoteSlug);
const sport = remote?.sports?.find((s: any) => s.sport_slug === sportSlug);
if (remote && sport) {
setRemoteInfo({
remote_name: remote.remote_name,
logo_url: remote.remote_logo_url || remote.logo_url,
origin_name: remote.origin_name,
logo_url: remote.origin_logo_url || remote.logo_url,
sport_name: sport.sport_name,
sport_logo_url: sport.sport_logo_url || sport.logo_url || `/images/${sportSlug}-logo.png`,
});
@ -83,17 +83,17 @@ export default function RemoteInfo({ remoteSlug, sportSlug, remotesData, isLoadi
<div className="w-12 h-12 rounded-lg bg-white shadow-sm p-2 border border-gray-100">
<img
src={remoteInfo.logo_url}
alt={remoteInfo.remote_name}
alt={remoteInfo.origin_name}
className="w-full h-full object-contain"
/>
</div>
</div>
)}
{/* Club name and sport */}
<div className="flex items-center flex-grow">
<h2 className="text-lg font-semibold text-gray-900">
{remoteInfo.remote_name}
{remoteInfo.origin_name}
</h2>
<span className="text-gray-400 mx-2">|</span>
<div className="flex items-center space-x-2">

@ -5,8 +5,8 @@ import LoadingSpinner from './LoadingSpinner';
import useTranslation from '../hooks/useTranslation';
interface Remote {
remote_slug: string;
remote_name: string;
origin_slug: string;
origin_name: string;
sports: { sport_slug: string; sport_name: string }[];
}
@ -41,7 +41,7 @@ export default function RemoteSportSelector({
// Simulate API call delay for better UX
await new Promise(resolve => setTimeout(resolve, 500));
onSelect(selectedRemote.remote_slug, sportSlug);
onSelect(selectedRemote.origin_slug, sportSlug);
setSaving(false);
};
@ -59,7 +59,7 @@ export default function RemoteSportSelector({
<h2 className="text-2xl font-bold text-slate-800">{title}</h2>
<p className="text-slate-600 mt-1">
{selectedRemote
? `What sport do you play at ${selectedRemote.remote_name}?`
? `What sport do you play at ${selectedRemote.origin_name}?`
: subtitle
}
</p>
@ -97,7 +97,7 @@ export default function RemoteSportSelector({
// Remote Selection
remotes.map((remote, index) => (
<div
key={remote.remote_slug}
key={remote.origin_slug}
className="group cursor-pointer animate-fadeIn"
style={{ animationDelay: `${index * 50}ms` }}
onClick={() => handleRemoteSelect(remote)}
@ -107,12 +107,12 @@ export default function RemoteSportSelector({
<div className="p-4 text-center">
<div className="w-12 h-12 mx-auto mb-3 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-md group-hover:scale-110 transition-transform duration-300">
<span className="text-lg font-bold text-white">
{remote.remote_name.charAt(0).toUpperCase()}
{remote.origin_name.charAt(0).toUpperCase()}
</span>
</div>
<h3 className="text-lg font-bold text-slate-800 mb-1 group-hover:text-indigo-600 transition-colors">
{remote.remote_name}
{remote.origin_name}
</h3>
<div className="flex items-center justify-center text-xs text-slate-500 mb-3">

@ -51,8 +51,8 @@ export default function UserBookingsTimeline({ currentUser, bookingsData }: Comp
for (const slot of sport.slots || []) {
slots.push({
...slot,
remote_slug: remote.remote_slug,
remote_name: remote.remote_name,
origin_slug: remote.origin_slug,
origin_name: remote.origin_name,
sport_slug: sport.sport_slug,
sport_name: sport.sport_name,
src_timezone: remote.src_timezone,
@ -212,7 +212,7 @@ export default function UserBookingsTimeline({ currentUser, bookingsData }: Comp
const timeUntil = getTimeUntilMatch(timeSlot.date.toISOString());
// Get unique remotes and courts for this time slot
const uniqueRemotes = Array.from(new Set(timeSlot.bookings.map(b => b.remote_name)));
const uniqueRemotes = Array.from(new Set(timeSlot.bookings.map(b => b.origin_name)));
const uniqueCourts = Array.from(new Set(timeSlot.bookings.map(b => b.court).filter(Boolean)));
return (

@ -196,7 +196,7 @@ export default function BookingDrawer({ bookingId, onClose, onUpdate }: BookingD
<p className="text-sm font-medium text-slate-900 mt-1">
{booking.slot.court.name}
</p>
<p className="text-xs text-slate-600 mt-0.5">{booking.slot.court.club_name}</p>
<p className="text-xs text-slate-600 mt-0.5">{booking.slot.court.facility_name}</p>
</div>
</div>
</div>

@ -11,8 +11,8 @@ import { getSlotStatus } from '@/src/utils/slotStatus';
const mockRemotesData = {
remotes: [
{
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
logo_url: '/images/tennis-club-logo.png',
sports: [
{
@ -32,7 +32,7 @@ const mockCurrentUser: RemoteMemberWithNameAndRemoteSlug = {
self_reported_level: 3.5,
full_account_str: 'Demo User',
name: 'Demo User',
remote_slug: 'tennis-club-zurich'
origin_slug: 'tennis-club-zurich'
};
// Generate mock players
@ -63,9 +63,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null,
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {
@ -87,9 +87,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null,
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {
@ -111,9 +111,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null,
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {
@ -136,9 +136,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: { remote_member_id: 5, app_user_id: 5 },
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {
@ -160,9 +160,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: { remote_member_id: 2, app_user_id: 2 },
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {
@ -185,9 +185,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null,
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {
@ -209,9 +209,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null,
i: null,
remote: {
remote_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png',
origin_slug: 'tennis-club-zurich',
origin_name: 'Tennis Club Zurich',
origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich'
},
sport: {

@ -11,8 +11,8 @@ interface Match {
date: string;
venue_id: number;
slot_id?: string;
remote_slug?: string;
remote_name?: string;
origin_slug?: string;
origin_name?: string;
sport_slug?: string;
court?: string;
outcome?: {
@ -71,8 +71,8 @@ export default function ProfileMatchHistory({ matches, venues, currentUserId, t,
end: endDate.toISOString(),
duration: match.duration_minutes || 90,
src_timezone: 'UTC', // Default timezone
remote_name: match.remote_name || venue?.name || 'Unknown Venue',
remote_slug: match.remote_slug || '',
origin_name: match.origin_name || venue?.name || 'Unknown Venue',
origin_slug: match.origin_slug || '',
sport_name: match.match_type || 'Padel',
sport_slug: match.sport_slug || 'padel',
court: match.court || 'Court 1',

@ -92,7 +92,7 @@ export function ProfileView({ type, id, sportSlug, translations, locale }: Profi
// Transform venues array to a map for ProfileDisplay
const venuesMap: Record<number, { name: string; address?: string }> = {};
data.venues.forEach(venue => {
venuesMap[venue.remote_server_id] = {
venuesMap[venue.origin_id] = {
name: venue.name,
address: undefined
};
@ -134,15 +134,15 @@ export function ProfileView({ type, id, sportSlug, translations, locale }: Profi
}
// Find venue ID from slug
const venue = data.venues.find(v => v.slug === match.remote_slug);
const venue = data.venues.find(v => v.slug === match.origin_slug);
return {
id: parseInt(match.slot_id) || index,
date: match.start_time,
venue_id: venue?.remote_server_id || 1,
venue_id: venue?.origin_id || 1,
slot_id: match.slot_id,
remote_slug: match.remote_slug,
remote_name: venue?.name,
origin_slug: match.origin_slug,
origin_name: venue?.name,
sport_slug: match.sport_slug,
court: match.court,
outcome,

@ -6,7 +6,7 @@ interface Match {
slot_id: string;
court: string;
start_time: string;
remote_slug: string;
origin_slug: string;
sport_slug: string;
players: Array<any>;
status: string;
@ -23,7 +23,7 @@ interface Match {
interface RecentMatchesProps {
matches: Match[];
venues: Array<{
remote_server_id: number;
origin_id: number;
slug: string;
name: string;
logo_url: string;
@ -137,7 +137,7 @@ export function RecentMatches({
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">
{getVenueName(match.remote_slug)} - {match.court}
{getVenueName(match.origin_slug)} - {match.court}
</span>
</div>

@ -23,7 +23,7 @@ interface ContextValue {
export const defaultEmptySettings = {
onboarding: { dismissed: false },
default_remote_sport: { remote_slug: null, sport_slug: null },
default_remote_sport: { facility_slug: null, sport_slug: null },
remote_credentials: null,
default_remote_member_id: { remotes: [] }
}

@ -5,10 +5,10 @@ import { useLocalizedLink } from "./useLocalizedLink";
export default function useBookingLink() {
const { settings } = useUserSettings();
const { localizedLink } = useLocalizedLink();
const path = settings?.default_remote_sport.remote_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.remote_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/${isLaterThanHour(21) ? 'tomorrow' : 'today'}`
const path = settings?.default_remote_sport.facility_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.facility_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/${isLaterThanHour(21) ? 'tomorrow' : 'today'}`
: '/select-remote';
return localizedLink(path);
}

@ -27,10 +27,10 @@ export default function useCourtSlots(
useEffect(() => {
if (remotesData && remoteSlug) {
const remote = (remotesData.remotes || []).find(
(r: any) => r.remote_slug === remoteSlug
(r: any) => r.origin_slug === remoteSlug
);
if (remote) {
setRemoteName(remote.remote_name);
setRemoteName(remote.origin_name);
setRemoteTimezone(remote.src_timezone || 'UTC');
}
}
@ -87,14 +87,14 @@ export default function useCourtSlots(
setOldestUpdatedAt(data.oldest_updated_at);
}
// Extract remote and sport info from the first court
if (courts.length > 0 && courts[0].remote && courts[0].sport) {
const remoteInfo = courts[0].remote;
// Extract facility and sport info from the first court
if (courts.length > 0 && courts[0].facility && courts[0].sport) {
const facilityInfo = courts[0].facility;
const sportInfo = courts[0].sport;
// Extract values
extractedRemoteName = remoteInfo.remote_name || '';
extractedTimezone = remoteInfo.src_timezone || 'UTC';
extractedRemoteName = facilityInfo.facility_name || '';
extractedTimezone = facilityInfo.facility_timezone || 'UTC';
// Set remote name and timezone if available
if (extractedRemoteName) setRemoteName(extractedRemoteName);
@ -103,9 +103,9 @@ export default function useCourtSlots(
// Create remotesData format for RemoteSportInfo component
newRemotesData = {
remotes: [{
remote_slug: remoteInfo.remote_slug || remoteSlug,
remote_name: extractedRemoteName,
remote_logo_url: remoteInfo.remote_logo_url,
origin_slug: facilityInfo.facility_slug || remoteSlug,
origin_name: extractedRemoteName,
origin_logo_url: facilityInfo.facility_logo_url,
src_timezone: extractedTimezone,
sports: [{
sport_slug: sportInfo.sport_slug || sportSlug,

@ -5,15 +5,15 @@ export function useEnhancedParams() {
const slot_id = Array.isArray(params.slot_id)
? params.slot_id[0]
: params.slot_id;
const remote_slug = Array.isArray(params.remote_slug)
? params.remote_slug[0]
: params.remote_slug;
const origin_slug = Array.isArray(params.origin_slug)
? params.origin_slug[0]
: params.origin_slug;
const sport_slug = Array.isArray(params.sport_slug)
? params.sport_slug[0]
: params.sport_slug;
return {
slot_id,
remote_slug,
origin_slug,
sport_slug,
};
}

@ -21,8 +21,8 @@ export default function usePastBookings(locale: Locale) {
for (const slot of sport.slots || []) {
slots.push({
...slot,
remote_slug: remote.remote_slug,
remote_name: remote.remote_name,
origin_slug: remote.origin_slug,
origin_name: remote.origin_name,
sport_slug: sport.sport_slug,
sport_name: sport.sport_name,
src_timezone: remote.src_timezone,

@ -15,8 +15,8 @@ export interface UserBooking {
players: Player[];
booked_by: number;
booked_by_app_user?: boolean;
remote_slug: string;
remote_name: string;
origin_slug: string;
origin_name: string;
sport_slug: string;
sport_name: string;
src_timezone: string;
@ -42,8 +42,8 @@ export default function useUserBookings(locale: Locale) {
for (const slot of sport.slots || []) {
slots.push({
...slot,
remote_slug: remote.remote_slug,
remote_name: remote.remote_name,
origin_slug: remote.origin_slug,
origin_name: remote.origin_name,
sport_slug: sport.sport_slug,
sport_name: sport.sport_name,
src_timezone: remote.src_timezone,

@ -171,13 +171,13 @@ export async function getAdminClubDetail(
export const MOCK_CLUBS: AdminClubsResponse = [
{
club_id: 1,
facility_id: 1,
name: 'Central Padel Geneva',
timezone: 'Europe/Zurich',
courts: 4,
},
{
club_id: 2,
facility_id: 2,
name: 'Riverside Tennis Lausanne',
timezone: 'Europe/Zurich',
courts: 6,
@ -186,7 +186,7 @@ export const MOCK_CLUBS: AdminClubsResponse = [
export const MOCK_CLUB_DETAIL: AdminClubDetail = {
club: {
club_id: 1,
facility_id: 1,
name: 'Central Padel Geneva',
timezone: 'Europe/Zurich',
},
@ -213,7 +213,7 @@ export const MOCK_CLUB_DETAIL: AdminClubDetail = {
slot_definitions: [],
upcoming_slots: [],
provider: {
remote_type: 'local',
provider: 'local',
capabilities: {
manages_slot_storage: true,
supports_payment_verification: false,

@ -338,7 +338,7 @@ export function getMockCourts(clubId: number): Court[] {
export function getMockClubProfile(clubId: number): ClubProfile {
return {
club_id: clubId,
facility_id: clubId,
name: 'Central Padel',
timezone: 'Europe/London',
settings: {

@ -1,8 +1,8 @@
export type AOrBProps<A, B> = (Partial<B> & A) | (Partial<A> & B);
export type Remote = {
remote_logo_url: string;
remote_name: string;
origin_logo_url: string;
origin_name: string;
src_timezone: string;
} & RemoteSlugProp;
@ -20,7 +20,7 @@ export type BaseRemoteMemberData = {
self_reported_level: number | null;
} & RemoteMemberIdProp;
export type RemoteSlugProp = { remote_slug: string; }
export type RemoteSlugProp = { origin_slug: string; }
export type FullAccountStrProp = {
full_account_str: string;
@ -294,23 +294,23 @@ export type CourtSlotWithDurationAndStatus = CourtSlot<
export type RemoteMemberWithNameAndRemoteSlug =
RemoteMember
& NameProp
& { remote_slug: string | undefined; }
& { origin_slug: string | undefined; }
export type RemoteMemberWithRemoteSlugAndMaybeName =
RemoteMember
& Partial<NameProp>
& { remote_slug: string | undefined; }
& { origin_slug: string | undefined; }
export type BookingResponse = AOrBProps<BookingResponseOk, BookingResponseError>;
// ---------- user settings
export interface DefaultRemoteSport {
remote_slug: string | null;
facility_slug: string | null;
sport_slug: string | null;
}
export interface DefaultRemoteMemberId {
remotes?: { remote_slug: string; remote_member_id: number }[];
remotes?: { origin_slug: string; remote_member_id: number }[];
}
export interface UserSettings {
@ -437,7 +437,7 @@ export type PublicProfileResponse = PublicProfileSuccessResponse | PublicProfile
// ============================================================================
export interface Venue {
remote_server_id: number;
origin_id: number;
slug: string;
name: string;
logo_url: string | null;
@ -519,7 +519,7 @@ export interface RecentMatch {
slot_id: string; // Court slot ID as string
court: string; // Court name/number
start_time: string | null; // ISO 8601 date string
remote_slug: string; // Venue slug
origin_slug: string; // Venue slug
sport_slug: string;
players: MatchPlayer[]; // Array of 2-4 players depending on sport
status: string; // e.g., "booked", "completed"

@ -13,8 +13,8 @@
}
interface Remote {
remote_slug: string;
remote_name: string;
origin_slug: string;
origin_name: string;
remote_members: RemoteMember[];
}

@ -16,7 +16,7 @@
* Returns: unwrapped array of clubs
*/
export interface AdminClub {
club_id: number;
facility_id: number;
name: string;
timezone: string;
courts: number; // Court count
@ -30,7 +30,7 @@ export type AdminClubsResponse = AdminClub[];
*/
export interface AdminClubDetail {
club: {
club_id: number;
facility_id: number;
name: string;
timezone: string;
};
@ -61,7 +61,7 @@ export interface AdminSlot {
slot_id: number; // slot_instance_id
court_id: number;
court_name: string;
club_id: number;
facility_id: number;
starts_at: string; // ISO 8601 timestamp
ends_at: string; // ISO 8601 timestamp
booking: AdminSlotBooking;
@ -91,7 +91,7 @@ export interface AdminSlotMeta {
}
export interface AdminProviderInfo {
remote_type: 'local' | 'fairplay';
provider: 'local' | 'fairplay';
capabilities: {
manages_slot_storage: boolean;
supports_payment_verification: boolean;

@ -42,8 +42,8 @@ export type SlotStatus = 'open' | 'booked' | 'held' | 'cancelled';
export interface CourtInfo {
court_id: number;
name: string;
club_id: number;
club_name: string;
facility_id: number;
facility_name: string;
}
export interface ProviderInfo {

@ -39,8 +39,8 @@ export interface BookingSlot {
court: {
court_id: number;
name: string;
club_id: number;
club_name: string;
facility_id: number;
facility_name: string;
};
}

@ -73,7 +73,7 @@ export interface ClubProfileSettings {
}
export interface ClubProfile {
club_id: number;
facility_id: number;
name: string;
timezone: string; // IANA timezone
settings?: ClubProfileSettings;

@ -142,17 +142,17 @@ export const isCurrentUserGen = (
p: { remote_member_id: number }
) => currentUser && p.remote_member_id === currentUser.remote_member_id;
export const getCurrentUser = (settings: UserSettings | null, remoteMembers: RemoteMemberWithRemoteSlugAndMaybeName[], remote_slug?: string): RemoteMemberWithNameAndRemoteSlug => {
export const getCurrentUser = (settings: UserSettings | null, remoteMembers: RemoteMemberWithRemoteSlugAndMaybeName[], origin_slug?: string): RemoteMemberWithNameAndRemoteSlug => {
if (settings && settings.default_remote_member_id.remotes) {
const remoteSlug = remote_slug ?? settings.default_remote_sport.remote_slug;
const remoteSlug = origin_slug ?? settings.default_remote_sport.facility_slug;
const entry = settings.default_remote_member_id.remotes.find(
r => r.remote_slug === remoteSlug
r => r.origin_slug === remoteSlug
);
const member = remoteMembers.find(m => m.remote_member_id === entry?.remote_member_id);
if (member) return { ...member, name: cleanName(member.full_account_str)};
}
// Use -Infinity to indicate no user (won't match any real remote_member_id)
return { remote_member_id: -Infinity, full_account_str: 'You', remote_slug, name: 'You', self_reported_level: null, glicko_level: null };
return { remote_member_id: -Infinity, full_account_str: 'You', origin_slug, name: 'You', self_reported_level: null, glicko_level: null };
}
export const getEnhancedPlayerGen = (

@ -6,11 +6,11 @@ export function hasClaimedAnyCredentials(settings: UserSettings | null) {
export function getClaimedRemoteMember(settings: UserSettings | null) {
if (!settings || !settings.default_remote_member_id.remotes) return undefined;
const remoteMembers = settings.default_remote_member_id && settings.default_remote_member_id.remotes.filter(r => r.remote_slug === settings.default_remote_sport.remote_slug);
const remoteMembers = settings.default_remote_member_id && settings.default_remote_member_id.remotes.filter(r => r.origin_slug === settings.default_remote_sport.facility_slug);
return remoteMembers && remoteMembers[0];
}
export function hasClaimedCredentialsForRemote(settings: UserSettings | null, remoteSlug: string) {
if (!settings || !settings.default_remote_member_id.remotes) return false;
return !!settings.default_remote_member_id.remotes.find(r => r.remote_slug === remoteSlug);
return !!settings.default_remote_member_id.remotes.find(r => r.origin_slug === remoteSlug);
}

Loading…
Cancel
Save