/** * BUS-Tickets - Booking Screen * Copyright (c) 2024-2026 IT Enterprise */ import { useState, useEffect } from 'react'; import { View, Text, ScrollView, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator, } from 'react-native'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '@/contexts/ThemeContext'; import { useAuth } from '@/contexts/AuthContext'; import { useConfig } from '@/contexts/ConfigContext'; import { useLocale } from '@/contexts/LocaleContext'; import { useProviders } from '@/contexts/ProvidersContext'; import { usePayment, PaymentStatus } from '@/hooks/usePayment'; import type { Trip, Passenger, PaymentConfig } from '@/types'; // Helper functions (avoid import issues) const formatPrice = (price: { amount: number; currency: string }) => { return `${price.amount} ${price.currency}`; }; const formatTime = (dateStr: string) => { const date = new Date(dateStr); return date.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' }); }; const formatShortDate = (dateStr: string) => { const date = new Date(dateStr); return date.toLocaleDateString('cs-CZ', { day: 'numeric', month: 'short' }); }; const formatDuration = (minutes: number) => { const hours = Math.floor(minutes / 60); const mins = Math.round(minutes % 60); return `${hours}h ${mins}m`; }; type BookingStep = 'seats' | 'passenger' | 'payment'; // Helper functions for payment providers const getProviderIcon = (provider: string): string => { const icons: Record = { monobank: 'card', liqpay: 'card-outline', stripe: 'card', paypal: 'logo-paypal', gopay: 'wallet', fondy: 'card', cash: 'cash-outline', bank_transfer: 'business-outline', }; return icons[provider] || 'card'; }; const getProviderColor = (provider: string): string => { const colors: Record = { monobank: '#000000', liqpay: '#7AB72B', stripe: '#635BFF', paypal: '#003087', gopay: '#2E7D32', fondy: '#FF6B00', cash: '#28a745', bank_transfer: '#0D47A1', }; return colors[provider] || '#007AFF'; }; export default function BookingScreen() { const router = useRouter(); const params = useLocalSearchParams<{ tripId: string; passengers: string; providerId?: string }>(); const { colors } = useTheme(); const { isAuthenticated, user } = useAuth(); const { config } = useConfig(); const { t, formatCurrency, formatDate, locale } = useLocale(); const { getProvider, activeProviders } = useProviders(); const [trip, setTrip] = useState(null); const [isLoading, setIsLoading] = useState(true); const [step, setStep] = useState('seats'); const [selectedSeats, setSelectedSeats] = useState([]); const [passenger, setPassenger] = useState({ name: user?.name || '', email: user?.email || '', phone: user?.phone || '', }); const [selectedProvider, setSelectedProvider] = useState(null); const [reservationIds, setReservationIds] = useState([]); // Payment hook with callbacks const { status: paymentStatus, error: paymentError, transaction, availableProviders, isPolling, initiatePayment, openPaymentPage, startPolling, reset: resetPayment, } = usePayment({ onSuccess: (result) => { Alert.alert(t.payment.success, t.booking.successMessage, [ { text: t.tickets.title, onPress: () => router.replace('/tickets'), }, ]); }, onError: (error) => { Alert.alert(t.payment.failed, error || t.errors.paymentFailed); }, onCancelled: () => { Alert.alert(t.payment.cancelled, t.payment.cancelled); }, }); const isProcessing = paymentStatus === 'initiating' || paymentStatus === 'processing'; const passengerCount = parseInt(params.passengers || '1', 10); useEffect(() => { loadTrip(); }, [params.tripId]); const loadTrip = async () => { setIsLoading(true); try { const apiUrl = config.backend.url; const response = await fetch(`${apiUrl}/api/v1/trip/${params.tripId}`); const data = await response.json(); if (data.success && data.data) { const t = data.data; const loadedTrip: Trip = { id: t.id, route: { id: t.route?.id || 0, name: t.route?.name || '', origin: { id: t.route?.origin?.id || 0, name: t.route?.origin?.name || '', city: t.route?.origin?.city || t.route?.origin?.name || '', country: 'CZ', }, destination: { id: t.route?.destination?.id || 0, name: t.route?.destination?.name || '', city: t.route?.destination?.city || t.route?.destination?.name || '', country: 'UA', }, }, departureTime: t.departureTime || t.tripDate, arrivalTime: t.arrivalTime || t.tripDate, bus: { id: t.bus?.id || 0, name: t.bus?.name || 'Bus', plateNumber: t.bus?.plateNumber || '', capacity: t.bus?.capacity || t.totalSeats || 50, amenities: ['wifi', 'ac'], }, availableSeats: t.availableSeats || 0, totalSeats: t.totalSeats || t.bus?.capacity || 50, price: { amount: t.price?.amount || 0, currency: t.price?.currency || 'CZK', }, status: t.status || 'scheduled', }; setTrip(loadedTrip); } else { throw new Error('Trip not found'); } } catch (error) { console.error('Error loading trip:', error); Alert.alert('Error', 'Could not load trip details'); router.back(); } finally { setIsLoading(false); } }; const handleSeatSelect = (seatNumber: number) => { if (selectedSeats.includes(seatNumber)) { setSelectedSeats(selectedSeats.filter((s) => s !== seatNumber)); } else if (selectedSeats.length < passengerCount) { setSelectedSeats([...selectedSeats, seatNumber]); } }; const handleContinue = () => { if (step === 'seats') { if (selectedSeats.length !== passengerCount) { Alert.alert(t.booking.selectSeat, `${t.booking.selectSeat}: ${passengerCount}`); return; } setStep('passenger'); } else if (step === 'passenger') { if (!passenger.name || !passenger.email || !passenger.phone) { Alert.alert(t.booking.passengerDetails, t.errors.invalidInput); return; } setStep('payment'); } }; const handlePayment = async () => { if (!selectedProvider) { Alert.alert(t.payment.selectMethod, t.payment.selectMethod); return; } try { // First create reservation via API const reservations = await createReservations(); if (!reservations || reservations.length === 0) { throw new Error(t.errors.bookingFailed); } setReservationIds(reservations); // Initiate payment with selected provider const result = await initiatePayment( reservations, selectedProvider.id as number, ); // Handle different payment flows based on provider if (result.paymentUrl) { // For PayPal, Monobank, Stripe - open payment page await openPaymentPage(result.paymentUrl); // Start polling for status updates startPolling(result.transactionId); } else if (result.bankDetails) { // Bank transfer - show bank details Alert.alert( t.payment.card, `IBAN: ${result.bankDetails.iban}\n` + `SWIFT: ${result.bankDetails.swift}\n` + `VS: ${result.bankDetails.variableSymbol}\n` + `${t.common.total}: ${result.bankDetails.amount} ${result.bankDetails.currency}`, [{ text: 'OK' }] ); } else if (selectedProvider.provider === 'cash') { // Cash payment - just confirm booking Alert.alert(t.payment.confirmCash, t.payment.cashNote, [ { text: t.tickets.title, onPress: () => router.replace('/tickets'), }, ]); } } catch (error) { const message = error instanceof Error ? error.message : t.errors.paymentFailed; Alert.alert(t.common.error, message); } }; const createReservations = async (): Promise => { try { const response = await fetch(`${config.backend.url}/api/v1/reservations`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tripId: parseInt(params.tripId!, 10), passengers: passengerCount, seats: selectedSeats, passenger: { name: passenger.name, email: passenger.email, phone: passenger.phone, }, paymentMethod: selectedProvider?.provider || 'cash', }), }); const data = await response.json(); if (!data.success) { throw new Error(data.error?.message || 'Rezervace selhala'); } // Return array of reservation IDs return data.data?.reservationId ? [data.data.reservationId] : []; } catch (error) { console.error('Create reservations error:', error); throw error; } }; const styles = createStyles(colors); if (isLoading || !trip) { return ( Loading trip... ); } const duration = (new Date(trip.arrivalTime).getTime() - new Date(trip.departureTime).getTime()) / 1000 / 60; const totalPrice = trip.price.amount * passengerCount; // Generate seat grid const seatRows = Math.ceil(trip.totalSeats / 4); const occupiedSeats = [3, 7, 12, 15, 22, 28, 33, 41]; // Mock occupied seats return ( {/* Trip Summary */} {trip.route.origin.city} {trip.route.destination.city} {formatShortDate(trip.departureTime)} • {formatTime(trip.departureTime)} •{' '} {formatDuration(duration)} {/* Step indicator */} {(['seats', 'passenger', 'payment'] as BookingStep[]).map((s, index) => ( {index + 1} {s === 'seats' ? 'Seats' : s === 'passenger' ? 'Details' : 'Payment'} ))} {/* Step Content */} {step === 'seats' && ( Select Your Seats Select {passengerCount} seat(s) • {selectedSeats.length} selected {/* Seat legend */} Available Selected Occupied {/* Seat grid */} {Array.from({ length: seatRows }, (_, rowIndex) => ( {Array.from({ length: 4 }, (_, colIndex) => { const seatNumber = rowIndex * 4 + colIndex + 1; if (seatNumber > trip.totalSeats) return null; const isOccupied = occupiedSeats.includes(seatNumber); const isSelected = selectedSeats.includes(seatNumber); return ( !isOccupied && handleSeatSelect(seatNumber)} disabled={isOccupied} > {seatNumber} ); })} ))} )} {step === 'passenger' && ( Passenger Details Full Name setPassenger({ ...passenger, name: text })} /> Email setPassenger({ ...passenger, email: text })} keyboardType="email-address" autoCapitalize="none" /> Phone setPassenger({ ...passenger, phone: text })} keyboardType="phone-pad" /> Selected seats: {selectedSeats.sort((a, b) => a - b).join(', ')} )} {step === 'payment' && ( Vyberte platební metodu {/* Payment status indicator */} {isProcessing && ( {paymentStatus === 'initiating' ? 'Zahajuji platbu...' : isPolling ? 'Čekám na potvrzení platby...' : 'Zpracovávám...'} )} {paymentError && ( {paymentError} )} {availableProviders.map((provider) => { const isSelected = selectedProvider?.id === provider.id; const providerIcon = getProviderIcon(provider.provider); return ( setSelectedProvider(provider)} disabled={isProcessing} > {provider.name} {provider.testMode && ( TEST )} {(provider.supportsApplePay || provider.supportsGooglePay) && ( {provider.supportsApplePay && ( Apple Pay )} {provider.supportsGooglePay && ( Google Pay )} )} {isSelected && ( )} ); })} {/* Order summary */} Order Summary Route {trip.route.origin.city} → {trip.route.destination.city} Date {formatShortDate(trip.departureTime)} Seats {selectedSeats.sort((a, b) => a - b).join(', ')} Passenger {passenger.name} Total {formatPrice({ amount: totalPrice, currency: trip.price.currency })} )} {/* Bottom bar */} Total {formatPrice({ amount: totalPrice, currency: trip.price.currency })} {step === 'payment' ? ( {isProcessing ? ( {paymentStatus === 'initiating' ? 'Zahajuji...' : 'Zpracovávám...'} ) : ( Zaplatit )} ) : ( Continue )} ); } const createStyles = (colors: any) => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, loadingText: { fontSize: 16, color: colors.textSecondary, }, scrollView: { flex: 1, }, content: { padding: 16, paddingBottom: 100, }, tripSummary: { backgroundColor: colors.card, borderRadius: 12, padding: 16, marginBottom: 16, }, routeRow: { flexDirection: 'row', alignItems: 'center', gap: 8, }, cityText: { fontSize: 18, fontWeight: '600', color: colors.text, }, detailsRow: { marginTop: 8, }, detailText: { fontSize: 14, color: colors.textSecondary, }, stepIndicator: { flexDirection: 'row', justifyContent: 'center', gap: 32, marginBottom: 24, }, stepItem: { alignItems: 'center', }, stepCircle: { width: 32, height: 32, borderRadius: 16, backgroundColor: colors.card, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: colors.border, }, stepCircleActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, stepNumber: { fontSize: 14, fontWeight: '600', color: colors.textSecondary, }, stepNumberActive: { color: '#fff', }, stepLabel: { fontSize: 12, color: colors.textSecondary, marginTop: 4, }, stepContent: { backgroundColor: colors.card, borderRadius: 12, padding: 16, }, sectionTitle: { fontSize: 18, fontWeight: '600', color: colors.text, marginBottom: 8, }, sectionSubtitle: { fontSize: 14, color: colors.textSecondary, marginBottom: 16, }, seatLegend: { flexDirection: 'row', justifyContent: 'center', gap: 24, marginBottom: 16, }, legendItem: { flexDirection: 'row', alignItems: 'center', gap: 6, }, seatSmall: { width: 16, height: 16, borderRadius: 4, }, seatAvailable: { backgroundColor: colors.background, borderWidth: 1, borderColor: colors.border, }, seatSelected: { backgroundColor: colors.primary, }, seatOccupied: { backgroundColor: colors.textSecondary, }, legendText: { fontSize: 12, color: colors.textSecondary, }, busLayout: { alignItems: 'center', }, driverArea: { width: 40, height: 40, borderRadius: 8, backgroundColor: colors.background, justifyContent: 'center', alignItems: 'center', marginBottom: 16, }, seatsContainer: { gap: 8, }, seatRow: { flexDirection: 'row', gap: 8, }, seat: { width: 40, height: 40, borderRadius: 8, backgroundColor: colors.background, justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: colors.border, }, seatAisle: { marginRight: 16, }, seatText: { fontSize: 12, fontWeight: '500', color: colors.text, }, seatTextOccupied: { color: '#fff', }, seatTextSelected: { color: '#fff', }, formGroup: { marginBottom: 16, }, inputLabel: { fontSize: 14, fontWeight: '500', color: colors.text, marginBottom: 8, }, input: { backgroundColor: colors.background, borderRadius: 8, padding: 12, fontSize: 16, color: colors.text, borderWidth: 1, borderColor: colors.border, }, selectedSeatsInfo: { flexDirection: 'row', marginTop: 16, padding: 12, backgroundColor: colors.background, borderRadius: 8, }, selectedSeatsLabel: { fontSize: 14, color: colors.textSecondary, }, selectedSeatsValue: { fontSize: 14, fontWeight: '600', color: colors.text, marginLeft: 8, }, paymentStatusBar: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 12, backgroundColor: colors.primary + '15', borderRadius: 8, marginBottom: 16, }, paymentStatusText: { fontSize: 14, color: colors.primary, fontWeight: '500', }, errorBanner: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 12, backgroundColor: '#dc354520', borderRadius: 8, marginBottom: 16, }, errorText: { fontSize: 14, color: '#dc3545', flex: 1, }, paymentOption: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 16, backgroundColor: colors.background, borderRadius: 12, marginBottom: 8, borderWidth: 2, borderColor: 'transparent', }, paymentOptionSelected: { borderColor: colors.primary, backgroundColor: colors.primary + '08', }, paymentOptionDisabled: { opacity: 0.5, }, paymentInfo: { flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1, }, providerIconBg: { width: 48, height: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, paymentDetails: { flex: 1, }, paymentName: { fontSize: 16, color: colors.text, }, paymentNameSelected: { fontWeight: '600', }, testModeBadge: { backgroundColor: '#ff9800', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, alignSelf: 'flex-start', marginTop: 4, }, testModeText: { color: '#fff', fontSize: 10, fontWeight: 'bold', }, walletBadges: { flexDirection: 'row', gap: 8, marginTop: 4, }, walletBadge: { fontSize: 11, color: colors.textSecondary, backgroundColor: colors.border, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, }, orderSummary: { marginTop: 16, padding: 16, backgroundColor: colors.background, borderRadius: 8, }, summaryTitle: { fontSize: 16, fontWeight: '600', color: colors.text, marginBottom: 12, }, summaryRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8, }, summaryLabel: { fontSize: 14, color: colors.textSecondary, }, summaryValue: { fontSize: 14, color: colors.text, }, summaryTotal: { marginTop: 8, paddingTop: 12, borderTopWidth: 1, borderTopColor: colors.border, }, totalLabel: { fontSize: 16, fontWeight: '600', color: colors.text, }, totalValue: { fontSize: 18, fontWeight: '700', color: colors.primary, }, bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 16, backgroundColor: colors.card, borderTopWidth: 1, borderTopColor: colors.border, }, priceContainer: {}, priceLabel: { fontSize: 12, color: colors.textSecondary, }, priceValue: { fontSize: 20, fontWeight: '700', color: colors.primary, }, continueButton: { backgroundColor: colors.primary, paddingHorizontal: 32, paddingVertical: 14, borderRadius: 8, minWidth: 140, }, buttonDisabled: { opacity: 0.6, }, buttonContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, }, continueButtonText: { fontSize: 16, fontWeight: '600', color: '#fff', }, });