almeira-web/transporte-cali-buenaventura/frontend/src/app/components/trip-pooling/trip-pooling.component.ts

375 lines
11 KiB
TypeScript

import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonCard,
IonCardHeader,
IonCardSubtitle,
IonCardTitle,
IonCardContent,
IonButton,
IonIcon,
IonBadge,
IonItem,
IonLabel,
IonSelect,
IonSelectOption,
IonInput,
IonSpinner,
IonList,
IonProgressBar,
} from '@ionic/angular/standalone';
import { WazeNavigationService, WazeStop } from '../../services/waze-navigation.service';
import { TripService, BookingPayload } from '../../services/trip.service';
@Component({
selector: 'app-trip-pooling',
standalone: true,
imports: [
CommonModule,
FormsModule,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonCard,
IonCardHeader,
IonCardSubtitle,
IonCardTitle,
IonCardContent,
IonButton,
IonIcon,
IonBadge,
IonItem,
IonLabel,
IonSelect,
IonSelectOption,
IonInput,
IonSpinner,
IonList,
IonProgressBar,
],
template: `
<ion-header>
<ion-toolbar color="primary">
<ion-title>Pacifictrans Go</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<!-- ALERTA DE PENALIDAD ACTIVA / BLOQUEO -->
<ion-card *ngIf="userPenaltyBalance > 0" color="danger">
<ion-card-header>
<ion-card-title>Cuenta Bloqueada por Penalidad</ion-card-title>
<ion-card-subtitle>Sanción por cancelación tardía</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<p>
Tienes un saldo pendiente de <strong>\${{ userPenaltyBalance | number }} COP</strong>.
Debes saldar esta penalidad para poder solicitar nuevos viajes o servicios de mensajería.
</p>
<ion-button
expand="block"
color="light"
class="ion-margin-top"
*ngIf="boldPenaltyCheckoutUrl"
(click)="openUrl(boldPenaltyCheckoutUrl)"
>
Pagar Penalidad con Bold (PSE / Nequi / QR Bre-B)
</ion-button>
</ion-card-content>
</ion-card>
<!-- FORMULARIO DE RESERVA (Si no tiene penalidad) -->
<ion-card *ngIf="userPenaltyBalance === 0 && !activeTrip">
<ion-card-header>
<ion-card-title>Solicitud de Puesto (Pool de 4)</ion-card-title>
<ion-card-subtitle>Agrupación inteligente por cercanía</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-list>
<ion-item>
<ion-label position="stacked">Ruta</ion-label>
<ion-select [(ngModel)]="booking.sentido">
<ion-select-option value="CALI_A_BUENAVENTURA">Cali ➔ Buenaventura</ion-select-option>
<ion-select-option value="BUENAVENTURA_A_CALI">Buenaventura ➔ Cali</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Dirección de recogida en Cali</ion-label>
<ion-input
placeholder="Ej. Calle 5 # 38-25, San Fernando"
[(ngModel)]="booking.direccionTexto"
></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Parada fija en Buenaventura</ion-label>
<ion-select [(ngModel)]="selectedBuenaventuraStop">
<ion-select-option *ngFor="let stop of bvStops" [value]="stop">
{{ stop.nombre }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Método de Pago Preferido</ion-label>
<ion-select [(ngModel)]="booking.metodoPago">
<ion-select-option value="BOLD">Bold (PSE, Tarjetas, Bre-B)</ion-select-option>
<ion-select-option value="NEQUI">Nequi Digital</ion-select-option>
<ion-select-option value="EFECTIVO">Efectivo al abordar</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
<ion-button
expand="block"
class="ion-margin-top"
[disabled]="loading"
(click)="submitBooking()"
>
<ion-spinner *ngIf="loading"></ion-spinner>
<span *ngIf="!loading">Solicitar Puesto ($35.000 COP)</span>
</ion-button>
</ion-card-content>
</ion-card>
<!-- DETALLE DEL VIAJE ACTIVO Y NAVEGACIÓN WAZE -->
<ion-card *ngIf="activeTrip">
<ion-card-header>
<div class="flex-header">
<ion-card-title>Viaje #{{ activeTrip._id?.slice(-6) }}</ion-card-title>
<ion-badge [color]="getStatusColor(activeTrip.estado)">
{{ activeTrip.estado }}
</ion-badge>
</div>
<ion-card-subtitle>
{{ activeTrip.origen?.ciudad }} ➔ {{ activeTrip.destino?.ciudad }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<!-- Mensaje de estado PENDING_APPROVAL -->
<div *ngIf="activeTrip.estado === 'PENDING_APPROVAL'" class="notice-box">
<p>⏳ <strong>En revisión del Administrador</strong></p>
<p>Tu solicitud ha sido agrupada en el pool. Recibirás una notificación push cuando sea aprobada.</p>
</div>
<!-- Información del Conductor (Cuando pasa a ACTIVE) -->
<div *ngIf="activeTrip.estado === 'ACTIVE'" class="driver-box">
<h4>🚗 Conductor Asignado</h4>
<p><strong>Nombre:</strong> {{ activeTrip.conductorId?.nombre || 'Confirmado' }}</p>
<p><strong>Hora estimada de recogida:</strong> {{ activeTrip.horaEstimadaRecogida | date:'shortTime' }}</p>
</div>
<!-- Secuencia de Navegación Waze para Conductores -->
<div *ngIf="wazeStops.length > 0" class="waze-section ion-margin-top">
<h4>🗺️ Ruta Optimizada Waze (Parada por Parada)</h4>
<ion-progress-bar [value]="wazeProgress.porcentaje / 100"></ion-progress-bar>
<p class="small-text">{{ wazeProgress.completadas }} de {{ wazeProgress.total }} paradas completadas</p>
<div class="current-stop-card" *ngIf="currentStop">
<p><strong>Siguiente recogida:</strong> {{ currentStop.nombrePasajero }}</p>
<p class="address-text">{{ currentStop.direccion }}</p>
<div class="action-buttons">
<ion-button color="success" (click)="openWazeForCurrentStop()">
Abrir en Waze
</ion-button>
<ion-button fill="outline" (click)="completeStop()">
Marcar Recogido
</ion-button>
</div>
</div>
</div>
<!-- Botón de Cancelación -->
<ion-button
expand="block"
color="danger"
fill="clear"
class="ion-margin-top"
(click)="cancelTrip()"
>
Cancelar Reserva
</ion-button>
</ion-card-content>
</ion-card>
</ion-content>
`,
styles: [
`
.flex-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.notice-box {
background: #fff3cd;
color: #856404;
padding: 12px;
border-radius: 8px;
margin-bottom: 12px;
}
.driver-box {
background: #e8f4fd;
color: #0b5394;
padding: 12px;
border-radius: 8px;
margin-bottom: 12px;
}
.current-stop-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 12px;
border-radius: 8px;
margin-top: 8px;
}
.address-text {
font-size: 0.9rem;
color: #6c757d;
}
.action-buttons {
display: flex;
gap: 8px;
margin-top: 10px;
}
.small-text {
font-size: 0.8rem;
color: #6c757d;
margin-top: 4px;
}
`,
],
})
export class TripPoolingComponent implements OnInit {
private wazeService = inject(WazeNavigationService);
private tripService = inject(TripService);
loading: boolean = false;
userPenaltyBalance: number = 0;
boldPenaltyCheckoutUrl?: string;
activeTrip: any = null;
bvStops = [
{ nombre: 'Terminal de Transportes Buenaventura', coordenadas: [-77.018, 3.882] },
{ nombre: 'Centro / Muelle Turístico Buenaventura', coordenadas: [-77.031, 3.889] },
{ nombre: 'Sector El Piñal Buenaventura', coordenadas: [-77.025, 3.879] },
];
selectedBuenaventuraStop = this.bvStops[0];
booking: BookingPayload = {
usuarioId: 'usr-demo-123',
direccionTexto: '',
coordenadas: [-76.532, 3.4516],
sentido: 'CALI_A_BUENAVENTURA',
puntoFijoBuenaventura: this.selectedBuenaventuraStop,
metodoPago: 'BOLD',
};
wazeStops: WazeStop[] = [];
currentStop: WazeStop | null = null;
wazeProgress = { completadas: 0, total: 0, porcentaje: 0 };
async ngOnInit() {
await this.checkUserStatus();
}
async checkUserStatus() {
try {
const res = await this.tripService.getUserStatus(this.booking.usuarioId);
if (res.success) {
this.userPenaltyBalance = res.montoPenalidad || 0;
this.boldPenaltyCheckoutUrl = res.linkLiquidacionBold;
}
} catch (err) {
console.warn('Error verificando penalidades de usuario:', err);
}
}
async submitBooking() {
this.loading = true;
this.booking.puntoFijoBuenaventura = this.selectedBuenaventuraStop;
try {
const res = await this.tripService.requestPoolTrip(this.booking);
if (res.success) {
this.activeTrip = res.trip;
if (res.secuenciaParadas) {
this.wazeStops = res.secuenciaParadas.map((p: any) => ({
orden: p.orden,
nombrePasajero: `Parada ${p.orden}`,
direccion: p.direccion,
lat: p.lat,
lng: p.lng,
completada: p.completada,
}));
this.wazeService.setStops(this.wazeStops);
this.updateWazeState();
}
}
} catch (err: any) {
alert(`Error al solicitar viaje: ${err.message}`);
} finally {
this.loading = false;
}
}
openWazeForCurrentStop() {
this.wazeService.navigateToCurrentStop();
}
completeStop() {
this.wazeService.completeCurrentStopAndAdvance();
this.updateWazeState();
}
updateWazeState() {
this.currentStop = this.wazeService.getCurrentStop();
this.wazeProgress = this.wazeService.getProgress();
}
getStatusColor(estado: string): string {
switch (estado) {
case 'PENDING_APPROVAL':
return 'warning';
case 'ACTIVE':
return 'success';
case 'IN_PROGRESS':
return 'primary';
case 'COMPLETED':
return 'medium';
case 'CANCELLED':
return 'danger';
default:
return 'medium';
}
}
async cancelTrip() {
if (!confirm('¿Seguro que deseas cancelar? Si el viaje ya está aprobado, incurrirás en una penalidad.')) {
return;
}
try {
const res = await this.tripService.cancelTrip(this.activeTrip._id, this.booking.usuarioId);
alert(res.message);
this.activeTrip = null;
await this.checkUserStatus();
} catch (err: any) {
alert(`Error cancelando viaje: ${err.message}`);
}
}
openUrl(url: string) {
window.open(url, '_system');
}
}