Skip to main content

Overview

The trip history feature provides two views:
  1. Active Trips - Current trip with real-time updates
  2. Trip History - Past completed and cancelled trips
Both views are accessible via the /tabs/trips route with child routes:
  • /tabs/trips/active - Active trip display
  • /tabs/trips/history - Trip history list

UI Structure

Parent Component: TripsComponent

The TripsComponent manages tab switching between active and history views. Location: src/app/features/tabs/trips/trips.component.ts
import { computed, signal } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

export class TripsComponent implements OnInit {
  segment = signal<'active' | 'history'>('active');
  
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}
  
  ngOnInit() {
    // Sync segment with URL
    this.setSegmentFromUrl(this.router.url);
    
    // Track navigation
    this.router.events
      .pipe(filter(e => e instanceof NavigationEnd))
      .subscribe(e => this.setSegmentFromUrl(e.urlAfterRedirects));
  }
  
  onSegmentChange(ev: CustomEvent) {
    const value = ev.detail.value as 'active' | 'history';
    this.router.navigate([value], { relativeTo: this.route });
  }
  
  private setSegmentFromUrl(url: string) {
    const last = url.split('/').filter(Boolean).pop();
    this.segment.set(last === 'history' ? 'history' : 'active');
  }
}
Template:
trips.component.html
<ion-segment [value]="segment()" (ionChange)="onSegmentChange($event)">
  <ion-segment-button value="active">
    <ion-label>Activo</ion-label>
  </ion-segment-button>
  <ion-segment-button value="history">
    <ion-label>Historial</ion-label>
  </ion-segment-button>
</ion-segment>

<router-outlet></router-outlet>

Active Trip Component

Displays the current active trip with real-time updates. Location: src/app/features/tabs/trips/active/active.component.ts

State Management

The ActiveComponent uses TripActiveFacade to access trip state:
import { TripActiveFacade } from '@/app/store/trips/trip-active.facade';

export class ActiveComponent implements OnInit {
  private facade = inject(TripActiveFacade);
  
  vm = computed(() => {
    const hasTrip = this.facade.hasTrip();
    const status = this.facade.status();
    
    const waitingSeconds = this.facade.waitingSeconds() ?? 0;
    const mm = Math.floor(waitingSeconds / 60).toString().padStart(2, '0');
    const ss = (waitingSeconds % 60).toString().padStart(2, '0');
    
    return {
      hasTrip,
      status,  // 'assigning' | 'accepted' | 'en_route' | 'arriving' | 'in_progress'
      tripId: this.facade.tripId(),
      
      // Driver info
      driver: this.facade.driver(),
      driverName: this.facade.driverName(),
      driverRating: this.facade.driverRating(),
      driverTrips: this.facade.driverTrips(),
      driverPhoneRaw: this.facade.driverPhoneRaw(),
      
      // Vehicle info
      vehicle: this.facade.vehicle(),
      plate: this.facade.plate(),
      vehicleTitle: this.facade.vehicleTitle(),  // "Toyota Corolla 2019"
      vehicleColor: this.facade.vehicleColor(),
      
      // Trip details
      etaMinutes: this.facade.etaMinutes(),
      priceAmount: this.facade.priceAmount(),
      priceCurrency: this.facade.priceCurrency(),
      
      // Waiting time
      waitingSeconds,
      waitingClock: `${mm}:${ss}`,
      waitingPenaltyApplied: this.facade.waitingPenaltyApplied(),
      waitingPenaltyText: this.facade.waitingPenaltyText()
    };
  });
}

TripActiveFacade Selectors

The facade exposes computed signals for each piece of trip state:
// Check if user has an active trip
hasTrip(): Signal<boolean>

// Get current trip ID
tripId(): Signal<string | null>

// Get trip status
status(): Signal<TripStatus | null>

type TripStatus =
  | 'pending'
  | 'assigning'
  | 'accepted'
  | 'en_route'
  | 'arriving'
  | 'in_progress'
  | 'completed'
  | 'cancelled';

Template Example

active.component.html
<div *ngIf="vm().hasTrip; else noTrip">
  <!-- Status banner -->
  <ion-card *ngIf="vm().status === 'assigning'">
    <ion-card-header>
      <ion-card-title>Buscando conductor...</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <ion-spinner></ion-spinner>
      <p>Estamos buscando el conductor perfecto para ti.</p>
    </ion-card-content>
  </ion-card>
  
  <!-- Driver info -->
  <ion-card *ngIf="vm().driver">
    <ion-card-header>
      <ion-avatar>
        <img [src]="vm().driver.profilePictureUrl || '/assets/default-avatar.png'" />
      </ion-avatar>
      <ion-card-title>{{ vm().driverName }}</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <div class="rating">
        <ion-icon name="star"></ion-icon>
        <span>{{ vm().driverRating }} ({{ vm().driverTrips }} viajes)</span>
      </div>
      
      <div class="vehicle">
        <p><strong>{{ vm().vehicleTitle }}</strong></p>
        <p>{{ vm().vehicleColor }} • {{ vm().plate }}</p>
        <ion-button size="small" (click)="copyPlate()">Copiar placa</ion-button>
      </div>
      
      <div class="eta" *ngIf="vm().etaMinutes">
        <ion-icon name="time-outline"></ion-icon>
        <span>Llega en {{ vm().etaMinutes }} min</span>
      </div>
    </ion-card-content>
  </ion-card>
  
  <!-- Waiting penalty warning -->
  <ion-card *ngIf="vm().waitingPenaltyApplied" color="warning">
    <ion-card-content>
      <ion-icon name="alert-circle-outline"></ion-icon>
      <p>{{ vm().waitingPenaltyText }}</p>
      <p>Tiempo de espera: {{ vm().waitingClock }}</p>
      <ion-button size="small" (click)="onOpenPenaltyDetails()">Ver detalles</ion-button>
    </ion-card-content>
  </ion-card>
  
  <!-- Price -->
  <ion-card *ngIf="vm().priceAmount">
    <ion-card-content>
      <h2>{{ vm().priceAmount }} {{ vm().priceCurrency }}</h2>
      <p>Tarifa estimada</p>
    </ion-card-content>
  </ion-card>
</div>

<ng-template #noTrip>
  <div class="empty-state">
    <ion-icon name="car-outline"></ion-icon>
    <p>No tienes ningún viaje activo</p>
    <ion-button routerLink="/home">Solicitar viaje</ion-button>
  </div>
</ng-template>

Trip History Component

Displays a list of past trips. The component is a minimal implementation ready for trip history API integration. Location: src/app/features/tabs/trips/historial/historial.component.ts

Current Implementation

historial.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-historial',
  templateUrl: './historial.component.html',
  styleUrls: ['./historial.component.scss'],
  standalone: true,
  imports: [],
})
export default class HistorialComponent implements OnInit {
  constructor() { }
  ngOnInit() {}
}

Future Enhancement Structure

When trip history API is available, the component can be enhanced:
historial.component.ts (future enhancement)
import { TripsApiService } from '@/app/core/services/http/trips-api.service';

export class HistorialComponent implements OnInit {
  private tripsApi = inject(TripsApiService);
  
  trips = signal<TripSummary[]>([]);
  loading = signal(false);
  
  async ngOnInit() {
    this.loading.set(true);
    try {
      const userId = this.authStore.user()?.id;
      const result = await firstValueFrom(
        this.tripsApi.fetchTripHistory(userId)
      );
      this.trips.set(result ?? []);
    } finally {
      this.loading.set(false);
    }
  }
  
  viewTripDetails(tripId: string) {
    this.router.navigate(['/trip-details', tripId]);
  }
}

Trip Summary Interface

interface TripSummary {
  id: string;
  status: 'completed' | 'cancelled';
  createdAt: string;           // ISO timestamp
  completedAt?: string | null;
  
  // Location
  pickupAddress?: string;
  destinationAddress?: string;
  distanceKm?: number;
  
  // Driver
  driverName?: string;
  driverRating?: number;
  
  // Pricing
  fareTotal?: number;
  currency: string;
  
  // Rating
  passengerRating?: number;    // Rating given by passenger
  passengerRatedAt?: string;
}

Planned Template

historial.component.html (planned)
<ion-list *ngIf="trips().length > 0">
  <ion-item *ngFor="let trip of trips()" button (click)="viewTripDetails(trip.id)">
    <ion-label>
      <h2>{{ trip.destinationAddress || 'Destino no disponible' }}</h2>
      <p>{{ trip.createdAt | date:'short' }}</p>
      <p *ngIf="trip.status === 'completed'">
        {{ trip.fareTotal }} {{ trip.currency }} • {{ trip.distanceKm }} km
      </p>
    </ion-label>
    
    <ion-badge slot="end" [color]="trip.status === 'completed' ? 'success' : 'medium'">
      {{ trip.status === 'completed' ? 'Completado' : 'Cancelado' }}
    </ion-badge>
  </ion-item>
</ion-list>

<div *ngIf="trips().length === 0 && !loading()" class="empty-state">
  <ion-icon name="receipt-outline"></ion-icon>
  <p>No tienes viajes anteriores</p>
</div>

<ion-spinner *ngIf="loading()"></ion-spinner>

Trip Details Page

Full details view for a specific trip (active or historical). Location: src/app/pages/trip-details/trip-details.component.ts

Fetching Trip Details

export class TripDetailsComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private tripsApi = inject(TripsApiService);
  
  trip = signal<TripDetails | null>(null);
  loading = signal(false);
  
  async ngOnInit() {
    const tripId = this.route.snapshot.paramMap.get('id');
    if (!tripId) return;
    
    this.loading.set(true);
    try {
      const result = await firstValueFrom(
        this.tripsApi.fetchTripById(tripId)
      );
      this.trip.set(result);
    } finally {
      this.loading.set(false);
    }
  }
}

TripDetails Interface

interface TripDetails extends TripSummary {
  // Full location data
  pickupPoint: { lat: number; lng: number };
  stops: Array<{
    point: { lat: number; lng: number };
    address?: string;
  }>;
  
  // Full driver data
  driver?: {
    id: string;
    name: string;
    profilePictureUrl?: string;
    ratingAvg?: number;
    ratingCount?: number;
    phone?: string;
  };
  
  // Full vehicle data
  vehicle?: {
    id: string;
    plateNumber?: string;
    make?: string;
    model?: string;
    year?: number;
    color?: string;
  };
  
  // Detailed pricing
  fareBreakdown?: {
    base: number;
    distance: number;
    time: number;
    waitingPenalty?: number;
    surge?: number;
  };
  
  // Route geometry (for map)
  routeGeometry?: FeatureCollection<LineString>;
  
  // Timeline
  timeline?: Array<{
    status: TripStatus;
    at: string;
  }>;
}

API Integration

The TripsApiService handles trip-related HTTP requests: Location: src/app/core/services/http/trips-api.service.ts

Available Methods

export class TripsApiService {
  // Create a new trip
  createTrip(body: CreateTripRequest): Observable<TripResponseDto | null>
  
  // Estimate fare for a planned trip
  estimateTrip(body: EstimateTripRequest): Observable<FareQuote | null>
  
  // Fetch trip history for a passenger
  fetchTripHistory(passengerId: string): Observable<TripSummary[]>
  
  // Fetch full details for a specific trip
  fetchTripById(tripId: string): Observable<TripDetails | null>
  
  // Rate a completed trip
  rateTrip(tripId: string, rating: number, comment?: string): Observable<void>
}

Trip Status Flow

The complete status flow for a trip: Status Descriptions:
StatusDescription
pendingTrip created, awaiting assignment
assigningSystem searching for driver
acceptedDriver accepted trip
en_routeDriver heading to pickup
arrivingDriver at pickup location
in_progressPassenger onboard, trip started
completedTrip finished successfully
cancelledTrip cancelled (any stage)

Best Practices

Always show status

Display the current trip status prominently. Use color coding:
  • Yellow/Orange: assigning, accepted
  • Blue: en_route, arriving
  • Green: in_progress
  • Red: cancelled

Handle empty states

Show helpful empty states when no trips are active or in history:
<div *ngIf="!vm().hasTrip" class="empty-state">
  <p>No tienes ningún viaje activo</p>
  <ion-button routerLink="/home">Solicitar viaje</ion-button>
</div>

Cache trip history

Cache trip history locally to reduce API calls and improve perceived performance.
The active trip component automatically updates via real-time Socket.io events. See Real-Time Tracking for details.
Always handle the case where a trip may be cancelled at any stage. Show appropriate messaging and offer to create a new trip.

Build docs developers (and LLMs) love