|
@@ -0,0 +1,629 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="space-y-6">
|
|
|
|
|
+ <!-- En-tête avec navigation -->
|
|
|
|
|
+ <div class="bg-white shadow rounded-lg">
|
|
|
|
|
+ <div class="px-4 py-5 sm:p-6">
|
|
|
|
|
+ <div class="flex items-center justify-between mb-4">
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="$emit('back')"
|
|
|
|
|
+ class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fas fa-arrow-left mr-2"></i>
|
|
|
|
|
+ Retour à la liste
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex space-x-3">
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="$emit('scan-tickets', trip)"
|
|
|
|
|
+ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fas fa-qrcode mr-2"></i>
|
|
|
|
|
+ Scanner les tickets
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="refreshTripData"
|
|
|
|
|
+ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fas fa-sync-alt mr-2"></i>
|
|
|
|
|
+ Actualiser
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Informations principales du trajet -->
|
|
|
|
|
+ <div v-if="tripDetails" class="border-b border-gray-200 pb-6">
|
|
|
|
|
+ <div class="flex items-center justify-between mb-4">
|
|
|
|
|
+ <h1 class="text-2xl font-bold text-gray-900">
|
|
|
|
|
+ {{ tripDetails.route.departureStation.city }} → {{ tripDetails.route.arrivalStation.city }}
|
|
|
|
|
+ </h1>
|
|
|
|
|
+ <span
|
|
|
|
|
+ :class="getStatusBadgeClass(tripDetails.status)"
|
|
|
|
|
+ class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ :class="getStatusColor(tripDetails.status)"
|
|
|
|
|
+ class="w-2 h-2 rounded-full mr-2"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ {{ getStatusText(tripDetails.status) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Grille d'informations -->
|
|
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
|
|
+ <div class="bg-gray-50 rounded-lg p-4">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <i class="fas fa-calendar text-blue-500 text-xl mr-3"></i>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-500">Date</p>
|
|
|
|
|
+ <p class="text-lg font-semibold text-gray-900">{{ formatDate(tripDetails.date) }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bg-gray-50 rounded-lg p-4">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <i class="fas fa-clock text-green-500 text-xl mr-3"></i>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-500">Horaires</p>
|
|
|
|
|
+ <p class="text-lg font-semibold text-gray-900">{{ tripDetails.departureTime }} - {{ tripDetails.arrivalTime }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bg-gray-50 rounded-lg p-4">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <i class="fas fa-users text-purple-500 text-xl mr-3"></i>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-500">Passagers</p>
|
|
|
|
|
+ <p class="text-lg font-semibold text-gray-900">{{ tripDetails.occupiedSeats }}/{{ tripDetails.totalSeats }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bg-gray-50 rounded-lg p-4">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <i class="fas fa-bus text-yellow-500 text-xl mr-3"></i>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm font-medium text-gray-500">Véhicule</p>
|
|
|
|
|
+ <p class="text-lg font-semibold text-gray-900">{{ tripDetails.vehicle.model }} - {{ tripDetails.vehicle.licensePlate }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Barre de progression -->
|
|
|
|
|
+ <div class="mt-6">
|
|
|
|
|
+ <div class="flex justify-between text-sm text-gray-600 mb-2">
|
|
|
|
|
+ <span>Taux d'occupation</span>
|
|
|
|
|
+ <span>{{ Math.round((tripDetails.occupiedSeats / tripDetails.totalSeats) * 100) }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="w-full bg-gray-200 rounded-full h-3">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="bg-gradient-to-r from-blue-500 to-green-500 h-3 rounded-full transition-all duration-500"
|
|
|
|
|
+ :style="{ width: `${(tripDetails.occupiedSeats / tripDetails.totalSeats) * 100}%` }"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Statistiques des tickets -->
|
|
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
|
|
|
+ <div class="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
+ <div class="p-5">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <div class="flex-shrink-0">
|
|
|
|
|
+ <i class="fas fa-ticket-alt text-blue-500 text-2xl"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ml-5 w-0 flex-1">
|
|
|
|
|
+ <dl>
|
|
|
|
|
+ <dt class="text-sm font-medium text-gray-500 truncate">Total tickets</dt>
|
|
|
|
|
+ <dd class="text-lg font-medium text-gray-900">{{ ticketStats.total }}</dd>
|
|
|
|
|
+ </dl>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
+ <div class="p-5">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <div class="flex-shrink-0">
|
|
|
|
|
+ <i class="fas fa-check-circle text-green-500 text-2xl"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ml-5 w-0 flex-1">
|
|
|
|
|
+ <dl>
|
|
|
|
|
+ <dt class="text-sm font-medium text-gray-500 truncate">Validés</dt>
|
|
|
|
|
+ <dd class="text-lg font-medium text-gray-900">{{ ticketStats.validated }}</dd>
|
|
|
|
|
+ </dl>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
+ <div class="p-5">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <div class="flex-shrink-0">
|
|
|
|
|
+ <i class="fas fa-clock text-yellow-500 text-2xl"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ml-5 w-0 flex-1">
|
|
|
|
|
+ <dl>
|
|
|
|
|
+ <dt class="text-sm font-medium text-gray-500 truncate">En attente</dt>
|
|
|
|
|
+ <dd class="text-lg font-medium text-gray-900">{{ ticketStats.pending }}</dd>
|
|
|
|
|
+ </dl>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
+ <div class="p-5">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <div class="flex-shrink-0">
|
|
|
|
|
+ <i class="fas fa-times-circle text-red-500 text-2xl"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ml-5 w-0 flex-1">
|
|
|
|
|
+ <dl>
|
|
|
|
|
+ <dt class="text-sm font-medium text-gray-500 truncate">Annulés</dt>
|
|
|
|
|
+ <dd class="text-lg font-medium text-gray-900">{{ ticketStats.cancelled }}</dd>
|
|
|
|
|
+ </dl>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Liste des passagers -->
|
|
|
|
|
+ <div class="bg-white shadow rounded-lg">
|
|
|
|
|
+ <div class="px-4 py-5 sm:p-6">
|
|
|
|
|
+ <div class="flex items-center justify-between mb-6">
|
|
|
|
|
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
|
|
|
|
|
+ <i class="fas fa-users mr-2 text-blue-600"></i>
|
|
|
|
|
+ Liste des passagers
|
|
|
|
|
+ </h3>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Filtres -->
|
|
|
|
|
+ <div class="flex space-x-3">
|
|
|
|
|
+ <select
|
|
|
|
|
+ v-model="passengerFilter"
|
|
|
|
|
+ class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="">Tous les passagers</option>
|
|
|
|
|
+ <option value="validated">Validés</option>
|
|
|
|
|
+ <option value="pending">En attente</option>
|
|
|
|
|
+ <option value="cancelled">Annulés</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+
|
|
|
|
|
+ <input
|
|
|
|
|
+ v-model="passengerSearch"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ placeholder="Rechercher un passager..."
|
|
|
|
|
+ class="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Table des passagers -->
|
|
|
|
|
+ <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
|
|
|
|
+ <table class="min-w-full divide-y divide-gray-300">
|
|
|
|
|
+ <thead class="bg-gray-50">
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
+ Passager
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
+ Siège
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
+ Statut
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
+ Heure validation
|
|
|
|
|
+ </th>
|
|
|
|
|
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
+ Actions
|
|
|
|
|
+ </th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody class="bg-white divide-y divide-gray-200">
|
|
|
|
|
+ <tr v-if="filteredPassengers.length === 0">
|
|
|
|
|
+ <td colspan="5" class="px-6 py-12 text-center text-gray-500">
|
|
|
|
|
+ <i class="fas fa-users text-4xl mb-4 text-gray-300"></i>
|
|
|
|
|
+ <p>Aucun passager trouvé</p>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ <tr v-for="passenger in filteredPassengers" :key="passenger.id" class="hover:bg-gray-50">
|
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
|
+ <div class="flex-shrink-0 h-10 w-10">
|
|
|
|
|
+ <div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
|
|
|
|
+ <i class="fas fa-user text-gray-600"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ml-4">
|
|
|
|
|
+ <div class="text-sm font-medium text-gray-900">{{ passenger.firstName }} {{ passenger.lastName }}</div>
|
|
|
|
|
+ <div class="text-sm text-gray-500">{{ passenger.email }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
|
|
|
+ {{ passenger.seatNumber }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
+ <span
|
|
|
|
|
+ :class="getTicketStatusClass(passenger.ticketStatus)"
|
|
|
|
|
+ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ :class="getTicketStatusColor(passenger.ticketStatus)"
|
|
|
|
|
+ class="w-1.5 h-1.5 rounded-full mr-1.5"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ {{ getTicketStatusText(passenger.ticketStatus) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
|
|
|
+ {{ passenger.validatedAt ? formatDateTime(passenger.validatedAt) : '-' }}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="passenger.ticketStatus === 'pending'"
|
|
|
|
|
+ @click="validateTicket(passenger)"
|
|
|
|
|
+ class="text-green-600 hover:text-green-900 mr-3"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fas fa-check mr-1"></i>
|
|
|
|
|
+ Valider
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="viewPassengerDetails(passenger)"
|
|
|
|
|
+ class="text-blue-600 hover:text-blue-900"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i class="fas fa-eye mr-1"></i>
|
|
|
|
|
+ Détails
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, computed, onMounted, watch } from 'vue'
|
|
|
|
|
+import EmployeeService, { type TripDetails, type TripPassenger } from '@/services/EmployeeService'
|
|
|
|
|
+
|
|
|
|
|
+// Props
|
|
|
|
|
+const props = defineProps<{
|
|
|
|
|
+ tripId: number
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+// État réactif
|
|
|
|
|
+const isLoading = ref(false)
|
|
|
|
|
+const searchQuery = ref('')
|
|
|
|
|
+const statusFilter = ref('all')
|
|
|
|
|
+const currentPage = ref(1)
|
|
|
|
|
+const itemsPerPage = ref(10)
|
|
|
|
|
+const tripDetails = ref<TripDetails | null>(null)
|
|
|
|
|
+const passengers = ref<TripPassenger[]>([])
|
|
|
|
|
+const totalPassengers = ref(0)
|
|
|
|
|
+const totalPages = ref(1)
|
|
|
|
|
+const passengerFilter = ref('')
|
|
|
|
|
+const passengerSearch = ref('')
|
|
|
|
|
+
|
|
|
|
|
+// Computed
|
|
|
|
|
+const paginatedPassengers = computed(() => {
|
|
|
|
|
+ return passengers.value
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const statistics = computed(() => {
|
|
|
|
|
+ if (!tripDetails.value) return { total: 0, validated: 0, pending: 0, validationRate: 0 }
|
|
|
|
|
+
|
|
|
|
|
+ const total = passengers.value.length
|
|
|
|
|
+ const validated = passengers.value.filter(p => p.status === 'validated').length
|
|
|
|
|
+ const pending = passengers.value.filter(p => p.status === 'pending').length
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ total,
|
|
|
|
|
+ validated,
|
|
|
|
|
+ pending,
|
|
|
|
|
+ validationRate: total > 0 ? Math.round((validated / total) * 100) : 0
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const ticketStats = computed(() => {
|
|
|
|
|
+ const total = passengers.value.length
|
|
|
|
|
+ const validated = passengers.value.filter(p => p.ticketStatus === 'validated').length
|
|
|
|
|
+ const pending = passengers.value.filter(p => p.ticketStatus === 'pending').length
|
|
|
|
|
+ const cancelled = passengers.value.filter(p => p.ticketStatus === 'cancelled').length
|
|
|
|
|
+
|
|
|
|
|
+ return { total, validated, pending, cancelled }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const filteredPassengers = computed(() => {
|
|
|
|
|
+ let filtered = passengers.value
|
|
|
|
|
+
|
|
|
|
|
+ // Filtre par statut
|
|
|
|
|
+ if (passengerFilter.value) {
|
|
|
|
|
+ filtered = filtered.filter(p => p.ticketStatus === passengerFilter.value)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Filtre par recherche
|
|
|
|
|
+ if (passengerSearch.value) {
|
|
|
|
|
+ const query = passengerSearch.value.toLowerCase()
|
|
|
|
|
+ filtered = filtered.filter(p =>
|
|
|
|
|
+ p.name.toLowerCase().includes(query) ||
|
|
|
|
|
+ p.email.toLowerCase().includes(query) ||
|
|
|
|
|
+ p.seatNumber.toLowerCase().includes(query)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return filtered
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Méthodes
|
|
|
|
|
+const loadTripDetails = async () => {
|
|
|
|
|
+ isLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const details = await EmployeeService.getTripDetails(props.tripId)
|
|
|
|
|
+ tripDetails.value = details
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Erreur lors du chargement des détails du trajet:', error)
|
|
|
|
|
+ // Fallback avec des données de démonstration
|
|
|
|
|
+ tripDetails.value = {
|
|
|
|
|
+ id: props.tripId,
|
|
|
|
|
+ departure: 'Abidjan',
|
|
|
|
|
+ arrival: 'Bouaké',
|
|
|
|
|
+ departureTime: '08:00',
|
|
|
|
|
+ arrivalTime: '11:30',
|
|
|
|
|
+ date: '2024-01-15',
|
|
|
|
|
+ status: 'in_progress',
|
|
|
|
|
+ vehicle: { id: 1, licensePlate: 'CI-001-AB', model: 'Mercedes Sprinter', capacity: 50 },
|
|
|
|
|
+ company: { id: 1, name: 'Transport Express' },
|
|
|
|
|
+ route: {
|
|
|
|
|
+ id: 1,
|
|
|
|
|
+ departureStation: { id: 1, name: 'Gare d\'Abidjan', city: 'Abidjan' },
|
|
|
|
|
+ arrivalStation: { id: 2, name: 'Gare de Bouaké', city: 'Bouaké' }
|
|
|
|
|
+ },
|
|
|
|
|
+ driver: { id: 1, firstName: 'Jean', lastName: 'Kouassi', phone: '+225 07 12 34 56 78' },
|
|
|
|
|
+ totalSeats: 50,
|
|
|
|
|
+ occupiedSeats: 45,
|
|
|
|
|
+ revenue: 225000
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const applyFilters = () => {
|
|
|
|
|
+ currentPage.value = 1
|
|
|
|
|
+ loadPassengers()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const changePage = (page: number) => {
|
|
|
|
|
+ currentPage.value = page
|
|
|
|
|
+ loadPassengers()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getStatusColor = (status: string) => {
|
|
|
|
|
+ const colors = {
|
|
|
|
|
+ 'scheduled': 'bg-blue-500',
|
|
|
|
|
+ 'in-progress': 'bg-green-500',
|
|
|
|
|
+ 'completed': 'bg-gray-500',
|
|
|
|
|
+ 'cancelled': 'bg-red-500'
|
|
|
|
|
+ }
|
|
|
|
|
+ return colors[status] || 'bg-gray-500'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getStatusBadgeClass = (status: string) => {
|
|
|
|
|
+ const classes = {
|
|
|
|
|
+ 'scheduled': 'bg-blue-100 text-blue-800',
|
|
|
|
|
+ 'in-progress': 'bg-green-100 text-green-800',
|
|
|
|
|
+ 'completed': 'bg-gray-100 text-gray-800',
|
|
|
|
|
+ 'cancelled': 'bg-red-100 text-red-800'
|
|
|
|
|
+ }
|
|
|
|
|
+ return classes[status] || 'bg-gray-100 text-gray-800'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getStatusText = (status: string) => {
|
|
|
|
|
+ const texts = {
|
|
|
|
|
+ 'scheduled': 'Programmé',
|
|
|
|
|
+ 'in-progress': 'En cours',
|
|
|
|
|
+ 'completed': 'Terminé',
|
|
|
|
|
+ 'cancelled': 'Annulé'
|
|
|
|
|
+ }
|
|
|
|
|
+ return texts[status] || status
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getTicketStatusColor = (status: string) => {
|
|
|
|
|
+ const colors = {
|
|
|
|
|
+ 'validated': 'bg-green-500',
|
|
|
|
|
+ 'pending': 'bg-yellow-500',
|
|
|
|
|
+ 'cancelled': 'bg-red-500'
|
|
|
|
|
+ }
|
|
|
|
|
+ return colors[status] || 'bg-gray-500'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getTicketStatusClass = (status: string) => {
|
|
|
|
|
+ const classes = {
|
|
|
|
|
+ 'validated': 'bg-green-100 text-green-800',
|
|
|
|
|
+ 'pending': 'bg-yellow-100 text-yellow-800',
|
|
|
|
|
+ 'cancelled': 'bg-red-100 text-red-800'
|
|
|
|
|
+ }
|
|
|
|
|
+ return classes[status] || 'bg-gray-100 text-gray-800'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getTicketStatusText = (status: string) => {
|
|
|
|
|
+ const texts = {
|
|
|
|
|
+ 'validated': 'Validé',
|
|
|
|
|
+ 'pending': 'En attente',
|
|
|
|
|
+ 'cancelled': 'Annulé'
|
|
|
|
|
+ }
|
|
|
|
|
+ return texts[status] || status
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const formatDate = (date: string) => {
|
|
|
|
|
+ if (!date) return 'Date non définie'
|
|
|
|
|
+
|
|
|
|
|
+ const today = new Date().toISOString().split('T')[0]
|
|
|
|
|
+ const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0]
|
|
|
|
|
+
|
|
|
|
|
+ if (date === today) return 'Aujourd\'hui'
|
|
|
|
|
+ if (date === tomorrow) return 'Demain'
|
|
|
|
|
+
|
|
|
|
|
+ return new Date(date).toLocaleDateString('fr-FR', {
|
|
|
|
|
+ weekday: 'long',
|
|
|
|
|
+ year: 'numeric',
|
|
|
|
|
+ month: 'long',
|
|
|
|
|
+ day: 'numeric'
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const formatDateTime = (dateTime: string) => {
|
|
|
|
|
+ return new Date(dateTime).toLocaleString('fr-FR', {
|
|
|
|
|
+ hour: '2-digit',
|
|
|
|
|
+ minute: '2-digit',
|
|
|
|
|
+ day: '2-digit',
|
|
|
|
|
+ month: '2-digit'
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const validateTicket = (passenger: TripPassenger) => {
|
|
|
|
|
+ passenger.ticketStatus = 'validated'
|
|
|
|
|
+ passenger.validatedAt = new Date().toISOString()
|
|
|
|
|
+
|
|
|
|
|
+ // Ici, vous feriez un appel API pour valider le ticket
|
|
|
|
|
+ console.log('Ticket validé pour:', passenger.firstName + ' ' + passenger.lastName)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const viewPassengerDetails = (passenger: TripPassenger) => {
|
|
|
|
|
+ // Ouvrir un modal ou naviguer vers les détails du passager
|
|
|
|
|
+ console.log('Voir détails de:', passenger.firstName + ' ' + passenger.lastName)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const refreshTripData = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Recharger les données du trajet depuis l'API
|
|
|
|
|
+ console.log('Actualisation des données du trajet...')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Erreur lors de l\'actualisation:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Lifecycle
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadTripDetails()
|
|
|
|
|
+ loadPassengers()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Watchers
|
|
|
|
|
+watch([searchQuery, statusFilter], () => {
|
|
|
|
|
+ applyFilters()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+watch(() => props.tripId, () => {
|
|
|
|
|
+ loadTripDetails()
|
|
|
|
|
+ loadPassengers()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const loadPassengers = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const status = statusFilter.value === 'all' ? undefined : statusFilter.value
|
|
|
|
|
+ const search = searchQuery.value || undefined
|
|
|
|
|
+
|
|
|
|
|
+ const result = await EmployeeService.getTripPassengers(
|
|
|
|
|
+ props.tripId,
|
|
|
|
|
+ currentPage.value,
|
|
|
|
|
+ itemsPerPage.value,
|
|
|
|
|
+ status,
|
|
|
|
|
+ search
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ passengers.value = result.passengers
|
|
|
|
|
+ totalPassengers.value = result.pagination.total
|
|
|
|
|
+ totalPages.value = result.pagination.totalPages
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Erreur lors du chargement des passagers:', error)
|
|
|
|
|
+ // Fallback avec des données de démonstration
|
|
|
|
|
+ passengers.value = [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 1,
|
|
|
|
|
+ firstName: 'Marie',
|
|
|
|
|
+ lastName: 'Koné',
|
|
|
|
|
+ phone: '+225 07 12 34 56 78',
|
|
|
|
|
+ email: 'marie.kone@email.com',
|
|
|
|
|
+ seatNumber: 'A1',
|
|
|
|
|
+ ticketNumber: 'TK001234',
|
|
|
|
|
+ status: 'validated',
|
|
|
|
|
+ validatedAt: '2024-01-15T07:45:00Z',
|
|
|
|
|
+ qrCode: 'QR001234'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ totalPassengers.value = 1
|
|
|
|
|
+ totalPages.value = 1
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+/* Animations pour les transitions */
|
|
|
|
|
+.transition-all {
|
|
|
|
|
+ transition-property: all;
|
|
|
|
|
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
+ transition-duration: 500ms;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* Responsive design */
|
|
|
|
|
+@media (max-width: 768px) {
|
|
|
|
|
+ .grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
|
|
|
|
|
+ grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .grid-cols-1.md\:grid-cols-4 {
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .overflow-hidden {
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .min-w-full {
|
|
|
|
|
+ min-width: 600px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (min-width: 769px) and (max-width: 1024px) {
|
|
|
|
|
+ .md\:grid-cols-2 {
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .md\:grid-cols-4 {
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (min-width: 1025px) {
|
|
|
|
|
+ .lg\:grid-cols-4 {
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* Gradient pour la barre de progression */
|
|
|
|
|
+.bg-gradient-to-r {
|
|
|
|
|
+ background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.from-blue-500 {
|
|
|
|
|
+ --tw-gradient-from: #3b82f6;
|
|
|
|
|
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(59, 130, 246, 0));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.to-green-500 {
|
|
|
|
|
+ --tw-gradient-to: #10b981;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|