Overview
The trip history feature provides two views:
Active Trips - Current trip with real-time updates
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:
< 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:
Trip Status
Driver Info
Vehicle Info
Trip Details
// 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' ;
// Driver object
driver (): Signal < DriverSlimForPassenger | null >
// Driver name (e.g., "Juan Pérez")
driverName (): Signal < string | null >
// Average rating (e.g., 4.8)
driverRating (): Signal < number | null >
// Total trips completed
driverTrips (): Signal < number | null >
// Phone number (e.g., "+53 54321234")
driverPhoneRaw (): Signal < string | null >
// Vehicle object
vehicle (): Signal < VehicleSlimForPassenger | null >
// Plate number (e.g., "ABC-1234")
plate (): Signal < string | null >
// Formatted title (e.g., "Toyota Corolla 2019")
vehicleTitle (): Signal < string | null >
// Color (e.g., "Blanco")
vehicleColor (): Signal < string | null >
// Estimated time to arrival (minutes)
etaMinutes (): Signal < number | null >
// Current fare amount
priceAmount (): Signal < number | null >
// Currency ("CUP" or "USD")
priceCurrency (): Signal < string | null >
// Waiting time in seconds
waitingSeconds (): Signal < number | null >
// Whether waiting penalty is active
waitingPenaltyApplied (): Signal < boolean >
// Penalty description text
waitingPenaltyText (): Signal < string | null >
Template Example
< 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
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:
Status Description 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.