Skip to main content

Overview

The trip booking system allows passengers to:
  1. Select pickup location (GPS or manual)
  2. Search and select destination
  3. Choose vehicle category and service class
  4. View route and estimated fare
  5. Request a trip
The flow is managed by TripPlannerFacade, which orchestrates location services, Mapbox integration, and API calls.

Trip Planning Workflow

1. Initialize Trip Planner

src/app/features/tabs/map/map.component.ts
import { TripPlannerFacade } from '@/app/store/trips/trip-planner.facade';

export class MapComponent implements AfterViewInit {
  private facade = inject(TripPlannerFacade);
  
  async ngAfterViewInit() {
    await this.facade.init();
    // Facade automatically:
    // - Hydrates origin from cache or GPS
    // - Sets up destination autocomplete
    // - Loads vehicle categories and service classes
    // - Starts location tracking
  }
}
The facade provides autocomplete suggestions using Mapbox Places API:
src/app/features/tabs/home/home.component.ts
export class HomeComponent {
  private facade = inject(TripPlannerFacade);
  
  // Bind to input
  onDestinationInput(text: string) {
    this.facade.onDestinationInput(text);
    // Facade debounces and searches Mapbox
  }
  
  // Access suggestions
  suggestions = computed(() => this.facade.store.suggestions());
  
  // User selects a suggestion
  selectPlace(item: PlaceSuggestion) {
    this.facade.pickSuggestion(item);
    // Navigates to /map with destination set
  }
}
PlaceSuggestion Interface:
interface PlaceSuggestion {
  id: string;
  text: string;           // Short name (e.g., "Parque Céspedes")
  placeName: string;      // Full address
  coords: {
    lat: number;
    lng: number;
  };
}

3. Route Calculation

Once origin and destination are set, the facade automatically calculates the route:
// Automatic route calculation (internal)
private autoRouteEffect = effect(() => {
  const ready = this.store.readyToRoute();
  const hasRoute = !!this.store.routeSummary();
  
  if (ready && !hasRoute && !this.store.loading()) {
    this.computeRouteAndStore().pipe(take(1)).subscribe();
  }
});
RouteSummary Interface:
interface RouteSummary {
  origin: LatLng;
  destination: LatLng;
  originLabel?: string;
  destinationLabel?: string;
  distanceKm: number;        // e.g., 12.3
  durationMin: number;       // e.g., 24
  geometry: FeatureCollection<LineString>;  // GeoJSON for map
}
The route geometry is automatically drawn on the map using Mapbox GL JS. See Map Integration for details.

4. Manual Destination Adjustment

Users can drag the destination marker to adjust the endpoint:
src/app/features/tabs/map/map.component.ts
// In MapComponent
this.map.on('click', (e) => {
  const { lng, lat } = e.lngLat;
  this.facade.recalcRouteAfterAdjust({ lat, lng });
});

// Draggable marker
this.destMarker.on('dragend', () => {
  const ll = this.destMarker.getLngLat();
  this.facade.recalcRouteAfterAdjust({ lat: ll.lat, lng: ll.lng });
});

Vehicle Selection

Loading Categories

// Facade automatically loads categories on init
this.facade.ensureCatalogLoaded();

// Access categories
vehicleTypesVm = computed(() => {
  const cats = this.facade.store.vehicleCategories();
  const sel = this.facade.selectedVehicleId();
  return cats.map(c => ({
    value: c.id,
    label: c.label,  // e.g., "Sedán", "SUV"
    selected: c.id === sel
  }));
});

Service Classes

serviceTypesVm = computed(() => {
  const list = this.facade.store.serviceClasses();
  const sel = this.facade.selectedServiceClassId();
  return list.map(s => ({
    value: s.id,
    label: s.label,  // e.g., "Económico", "Premium"
    selected: s.id === sel
  }));
});

Selecting Options

// Select vehicle category
this.facade.selectVehicle(categoryId);

// Select service class
this.facade.selectServiceType(classId);

Fare Estimation

The facade automatically estimates the fare when route and selections are ready:
// Automatic estimation (internal)
private estimateFx = effect(() => {
  const rsReady = !!this.store.routeSummary();
  const vid = this.store.selectedVehicleId();
  const sid = this.store.selectedServiceClassId();
  
  if (!rsReady || !vid || !sid) return;
  
  const req = this.buildEstimateRequest();
  this.tripsApi.estimateTrip(req).subscribe(quote => {
    this.store.setFareQuote(quote);
  });
});
EstimateTripRequest:
interface EstimateTripRequest {
  vehicleCategoryId: string;
  serviceClassId: string;
  pickup: { lat: number; lng: number };
  stops: Array<{ lat: number; lng: number }>;
  currency: 'CUP' | 'USD';
}
FareQuote Response:
interface FareQuote {
  totalEstimated: number;    // e.g., 150.00
  currency: string;          // "CUP"
  breakdown?: {
    base: number;
    distance: number;
    time: number;
    surge?: number;
  };
}

Displaying Fare

tripDetailsVm = computed(() => {
  const rs = this.facade.store.routeSummary();
  const fq = this.facade.store.fareQuote();
  
  return {
    hasRoute: !!rs,
    distanceText: rs ? `${rs.distanceKm.toFixed(1)} km` : '—',
    durationText: rs ? `${rs.durationMin} min` : '—',
    totalEstimated: fq?.totalEstimated ?? null,
    currency: fq?.currency ?? null
  };
});

Trip Request

Building the Payload

const payload = this.facade.buildCreateTripPayload({
  passengerId: userId,
  paymentMode: 'cash',  // 'cash' | 'card' | 'wallet'
  vehicleCategoryId: selectedVehicleId,
  serviceClassId: selectedServiceClassId,
  pickupAddress: 'Calle Enramadas 123',  // Optional
  idempotencyKey: crypto.randomUUID()    // Optional
});
CreateTripRequest Structure:
interface CreateTripRequest {
  passengerId: string;
  paymentMode: 'cash' | 'card' | 'wallet';
  pickupPoint: { lat: number; lng: number };
  pickupAddress?: string;
  stops: Array<{
    point: { lat: number; lng: number };
    address?: string;
  }>;
  vehicleCategoryId: string;
  serviceClassId: string;
  idempotencyKey?: string;
}

Submitting the Request

// High-level method
this.facade.requestTrip({
  payment: 'cash',
  pickupAddress: 'Calle Enramadas 123'
});

// Facade delegates to TripPassengerFacade
// which:
// - Creates the trip via API
// - Starts driver search with 25s timeout
// - Shows "Searching for driver" UI
// - Handles "no drivers found" scenario
Always use an idempotency key to prevent duplicate trip creation if the request is retried due to network issues.

Trip Planning Store

The TripPlannerStore maintains the planning state:
interface TripPlannerState {
  // Location
  originPoint: LatLng | null;
  originText: string | null;
  destinationPoint: LatLng | null;
  destinationText: string | null;
  
  // Route
  routeSummary: RouteSummary | null;
  
  // Catalog
  vehicleCategories: VehicleCategory[];
  serviceClasses: ServiceClass[];
  selectedVehicleId: string | null;
  selectedServiceClassId: string | null;
  
  // Estimation
  fareQuote: FareQuote | null;
  
  // UI
  suggestions: PlaceSuggestion[];
  loading: boolean;
  error: string | null;
}

Key Selectors

// Check if ready to calculate route
readyToRoute = computed(() => {
  const o = this.store.originPoint();
  const d = this.store.destinationPoint();
  return !!(o && d);
});

// Get current origin
getOrigin(): LatLng | null {
  return this.facade.getOrigin();
}

// Get current destination
getDestination(): LatLng | null {
  return this.facade.getDestination();
}

Cache and Persistence

The facade caches origin and destination in localStorage with 24-hour TTL:
// Automatic persistence (internal)
private persistFx = effect(() => {
  const o = this.store.originPoint();
  const ol = this.store.originText();
  if (o) {
    localStorage.setItem('trip.origin', JSON.stringify({
      lat: o.lat,
      lng: o.lng,
      label: ol,
      ts: Date.now()
    }));
  }
});

// Automatic hydration on init
private hydrateFromCache() {
  const raw = localStorage.getItem('trip.origin');
  if (raw) {
    const v = JSON.parse(raw);
    const fresh = Date.now() - v.ts < 24 * 3600_000;
    if (fresh) {
      this.store.setOriginPoint({ lat: v.lat, lng: v.lng });
      this.store.setOriginText(v.label);
    }
  }
}
Origin is cached to avoid re-fetching GPS location on every app launch. Destination is not cached by default to encourage fresh searches.

Location Tracking

The facade tracks user location in real-time to keep origin updated:
// Continuous location tracking (internal)
this.originFollowSub = this.loc.watchBalanced()
  .pipe(sampleTime(4000))  // Every 4 seconds
  .subscribe(sample => {
    const next = { lat: sample.lat, lng: sample.lng };
    const shouldUpdate = !last || distance(last, next) > 40;  // 40m threshold
    
    if (shouldUpdate) {
      this.store.setOriginPoint(next, { invalidate: true });
      last = next;
    }
  });
Location updates only trigger route recalculation if the user has moved more than 40 meters. This prevents excessive API calls.

Complete Example

import { TripPlannerFacade } from '@/app/store/trips/trip-planner.facade';

export class HomeComponent implements OnInit {
  private facade = inject(TripPlannerFacade);
  
  destination = '';
  suggestions = computed(() => this.facade.store.suggestions());
  
  async ngOnInit() {
    await this.facade.init();
  }
  
  onDestinationInput(text: string) {
    this.destination = text;
    this.facade.onDestinationInput(text);
  }
  
  selectSuggestion(item: PlaceSuggestion) {
    this.facade.pickSuggestion(item);
    // Navigates to /map
  }
  
  clearDestination() {
    this.destination = '';
    this.facade.clearDestination();
  }
}

Build docs developers (and LLMs) love