Files
karibeo_backend_admin/src/pages/Checkout.tsx
2025-10-10 22:51:26 +00:00

673 lines
27 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCart } from '@/contexts/CartContext';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X, Loader2 } from 'lucide-react';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { useToast } from '@/hooks/use-toast';
import { useBooking } from '@/hooks/useBooking';
import { useStripe } from '@/hooks/useStripe';
import { paymentService } from '@/services/paymentService';
interface CustomerInfo {
firstName: string;
lastName: string;
email: string;
phone: string;
address: string;
city: string;
country: string;
zipCode: string;
}
interface PaymentInfo {
method: 'credit_card' | 'paypal' | 'bank_transfer' | 'cash';
cardNumber: string;
expiryDate: string;
cvv: string;
cardName: string;
}
const Checkout = () => {
const navigate = useNavigate();
const { items, getTotalPrice, clearCart } = useCart();
const { toast } = useToast();
const { createBooking } = useBooking();
const { stripe, credentials, loading: stripeLoading } = useStripe();
const [step, setStep] = useState(1);
const [isProcessing, setIsProcessing] = useState(false);
const [customerInfo, setCustomerInfo] = useState<CustomerInfo>({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
country: 'Dominican Republic',
zipCode: ''
});
const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>({
method: 'credit_card',
cardNumber: '',
expiryDate: '',
cvv: '',
cardName: ''
});
const [specialRequests, setSpecialRequests] = useState('');
if (items.length === 0) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="container mx-auto px-4 pt-24 pb-12">
<div className="text-center py-12">
<h1 className="text-2xl font-bold mb-4">Tu carrito está vacío</h1>
<p className="text-gray-600 mb-6">Agrega algunas ofertas a tu carrito para continuar.</p>
<Button onClick={() => navigate('/explore')}>
Explorar Ofertas
</Button>
</div>
</div>
<Footer />
</div>
);
}
const totalPrice = getTotalPrice();
const taxes = totalPrice * 0.18; // 18% ITBIS (impuesto dominicano)
const serviceFee = totalPrice * 0.05; // 5% fee de servicio
const finalTotal = totalPrice + taxes + serviceFee;
const paymentMethods = [
{ id: 'credit_card', name: 'Tarjeta de Crédito/Débito', icon: CreditCard },
{ id: 'paypal', name: 'PayPal', icon: Smartphone },
{ id: 'bank_transfer', name: 'Transferencia Bancaria', icon: Building },
{ id: 'cash', name: 'Pago en Efectivo', icon: Truck }
];
const handleCustomerInfoChange = (field: keyof CustomerInfo, value: string) => {
setCustomerInfo(prev => ({ ...prev, [field]: value }));
};
const handlePaymentInfoChange = (field: keyof PaymentInfo, value: string) => {
setPaymentInfo(prev => ({ ...prev, [field]: value }));
};
const saveOrderToJSON = (orderData: any) => {
const orders = JSON.parse(localStorage.getItem('karibeo_orders') || '[]');
orders.push(orderData);
localStorage.setItem('karibeo_orders', JSON.stringify(orders));
};
const processPayment = async () => {
setIsProcessing(true);
try {
// Si el método de pago es tarjeta de crédito y Stripe está disponible
if (paymentInfo.method === 'credit_card' && stripe && credentials) {
// Crear PaymentIntent desde el backend
const paymentIntent = await paymentService.createPaymentIntent({
amount: Math.round(finalTotal * 100), // Convertir a centavos
currency: 'usd',
description: `Reserva de ${items.length} item(s)`,
metadata: {
customerName: `${customerInfo.firstName} ${customerInfo.lastName}`,
customerEmail: customerInfo.email,
},
});
// En un entorno real, aquí se confirmaría el pago con Stripe Elements
// Por ahora, simulamos que el pago fue exitoso
console.log('Payment Intent created:', paymentIntent);
}
// Create reservations for each cart item
const reservationPromises = items.map(async (item) => {
const checkInDate = item.selectedDate
? new Date(item.selectedDate)
: new Date();
// Add time slot if available
if (item.timeSlot) {
const [hours, minutes] = item.timeSlot.split(':');
checkInDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
}
const bookingData = {
listingId: item.id,
guestName: `${customerInfo.firstName} ${customerInfo.lastName}`,
guestEmail: customerInfo.email,
guestPhone: customerInfo.phone,
checkIn: checkInDate.toISOString(),
guests: item.guests || 1,
totalAmount: item.price * (item.guests || 1),
specialRequests: specialRequests,
};
return await createBooking(bookingData);
});
const results = await Promise.all(reservationPromises);
// Check if all reservations were created successfully
const allSuccess = results.every(result => result.success);
if (!allSuccess) {
throw new Error('Some reservations failed to create');
}
// Simulate payment processing
await new Promise(resolve => setTimeout(resolve, 2000));
const orderData = {
id: Date.now().toString(),
date: new Date().toISOString(),
items: items,
customerInfo,
paymentInfo: {
method: paymentInfo.method,
cardLast4: paymentInfo.method === 'credit_card' ? paymentInfo.cardNumber.slice(-4) : undefined
},
specialRequests,
pricing: {
subtotal: totalPrice,
taxes,
serviceFee,
total: finalTotal
},
status: 'confirmed',
reservations: results.map(r => r.data),
stripeEnabled: !!credentials?.enabled,
};
saveOrderToJSON(orderData);
clearCart();
toast({
title: "¡Pago procesado exitosamente!",
description: "Tus reservas han sido confirmadas. Recibirás un email de confirmación.",
});
navigate('/order-confirmation', { state: { orderData } });
} catch (error) {
console.error('Payment error:', error);
toast({
title: "Error al procesar el pago",
description: "Hubo un problema al procesar tu reserva. Por favor intenta nuevamente.",
variant: "destructive",
});
} finally {
setIsProcessing(false);
}
};
const validateStep1 = () => {
return customerInfo.firstName && customerInfo.lastName && customerInfo.email && customerInfo.phone;
};
const validateStep2 = () => {
if (paymentInfo.method === 'credit_card') {
return paymentInfo.cardNumber && paymentInfo.expiryDate && paymentInfo.cvv && paymentInfo.cardName;
}
return true;
};
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="container mx-auto px-4 pt-24 pb-12">
<div className="mb-6">
<Button variant="ghost" onClick={() => navigate(-1)} className="mb-4">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver
</Button>
<h1 className="text-3xl font-bold">Checkout</h1>
{/* Progress Steps */}
<div className="flex items-center mt-6 mb-8">
<div className={`flex items-center ${step >= 1 ? 'text-primary' : 'text-gray-400'}`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
step >= 1 ? 'border-primary bg-primary text-white' : 'border-gray-300'
}`}>
{step > 1 ? <Check className="w-4 h-4" /> : '1'}
</div>
<span className="ml-2 font-medium">Información Personal</span>
</div>
<div className={`flex-1 h-px mx-4 ${step > 1 ? 'bg-primary' : 'bg-gray-300'}`} />
<div className={`flex items-center ${step >= 2 ? 'text-primary' : 'text-gray-400'}`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
step >= 2 ? 'border-primary bg-primary text-white' : 'border-gray-300'
}`}>
{step > 2 ? <Check className="w-4 h-4" /> : '2'}
</div>
<span className="ml-2 font-medium">Pago</span>
</div>
<div className={`flex-1 h-px mx-4 ${step > 2 ? 'bg-primary' : 'bg-gray-300'}`} />
<div className={`flex items-center ${step >= 3 ? 'text-primary' : 'text-gray-400'}`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
step >= 3 ? 'border-primary bg-primary text-white' : 'border-gray-300'
}`}>
3
</div>
<span className="ml-2 font-medium">Confirmación</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2">
{step === 1 && (
<Card>
<CardHeader>
<CardTitle>Información Personal</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName">Nombre *</Label>
<Input
id="firstName"
value={customerInfo.firstName}
onChange={(e) => handleCustomerInfoChange('firstName', e.target.value)}
placeholder="Tu nombre"
/>
</div>
<div>
<Label htmlFor="lastName">Apellido *</Label>
<Input
id="lastName"
value={customerInfo.lastName}
onChange={(e) => handleCustomerInfoChange('lastName', e.target.value)}
placeholder="Tu apellido"
/>
</div>
</div>
<div>
<Label htmlFor="email">Email *</Label>
<Input
id="email"
type="email"
value={customerInfo.email}
onChange={(e) => handleCustomerInfoChange('email', e.target.value)}
placeholder="tu@email.com"
/>
</div>
<div>
<Label htmlFor="phone">Teléfono *</Label>
<Input
id="phone"
value={customerInfo.phone}
onChange={(e) => handleCustomerInfoChange('phone', e.target.value)}
placeholder="+1 (809) 123-4567"
/>
</div>
<div>
<Label htmlFor="address">Dirección</Label>
<Input
id="address"
value={customerInfo.address}
onChange={(e) => handleCustomerInfoChange('address', e.target.value)}
placeholder="Calle, número, sector"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label htmlFor="city">Ciudad</Label>
<Input
id="city"
value={customerInfo.city}
onChange={(e) => handleCustomerInfoChange('city', e.target.value)}
placeholder="Santo Domingo"
/>
</div>
<div>
<Label htmlFor="country">País</Label>
<Select
value={customerInfo.country}
onValueChange={(value) => handleCustomerInfoChange('country', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Dominican Republic">República Dominicana</SelectItem>
<SelectItem value="United States">Estados Unidos</SelectItem>
<SelectItem value="Canada">Canadá</SelectItem>
<SelectItem value="Other">Otro</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="zipCode">Código Postal</Label>
<Input
id="zipCode"
value={customerInfo.zipCode}
onChange={(e) => handleCustomerInfoChange('zipCode', e.target.value)}
placeholder="10001"
/>
</div>
</div>
<div>
<Label htmlFor="requests">Solicitudes Especiales</Label>
<Textarea
id="requests"
value={specialRequests}
onChange={(e) => setSpecialRequests(e.target.value)}
placeholder="Algún requerimiento especial para tu reserva..."
rows={3}
/>
</div>
<Button
className="w-full"
onClick={() => setStep(2)}
disabled={!validateStep1()}
>
Continuar al Pago
</Button>
</CardContent>
</Card>
)}
{step === 2 && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Método de Pago</CardTitle>
{credentials?.enabled && (
<Badge variant="default" className="flex items-center gap-1">
<Shield className="w-3 h-3" />
Pagos Seguros con Stripe
</Badge>
)}
{stripeLoading && (
<Badge variant="outline" className="flex items-center gap-1">
<Loader2 className="w-3 h-3 animate-spin" />
Cargando...
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Payment Method Selection */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{paymentMethods.map((method) => {
const IconComponent = method.icon;
return (
<button
key={method.id}
onClick={() => handlePaymentInfoChange('method', method.id as any)}
className={`p-4 border-2 rounded-lg flex items-center space-x-3 transition-colors ${
paymentInfo.method === method.id
? 'border-primary bg-primary/5'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<IconComponent className="w-5 h-5" />
<span className="font-medium">{method.name}</span>
</button>
);
})}
</div>
{/* Credit Card Form */}
{paymentInfo.method === 'credit_card' && (
<div className="space-y-4 border-t pt-6">
{credentials?.enabled && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div className="flex items-start gap-3">
<Shield className="w-5 h-5 text-green-600 mt-0.5" />
<div>
<h4 className="font-medium text-green-900">Pago Seguro</h4>
<p className="text-sm text-green-700">
Tu pago está protegido por Stripe. Tus datos son encriptados y seguros.
{credentials.testMode && ' (Modo de prueba activo)'}
</p>
</div>
</div>
</div>
)}
<div>
<Label htmlFor="cardName">Nombre en la Tarjeta *</Label>
<Input
id="cardName"
value={paymentInfo.cardName}
onChange={(e) => handlePaymentInfoChange('cardName', e.target.value)}
placeholder="Como aparece en la tarjeta"
/>
</div>
<div>
<Label htmlFor="cardNumber">Número de Tarjeta *</Label>
<Input
id="cardNumber"
value={paymentInfo.cardNumber}
onChange={(e) => handlePaymentInfoChange('cardNumber', e.target.value)}
placeholder="1234 5678 9012 3456"
maxLength={19}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="expiryDate">Fecha de Vencimiento *</Label>
<Input
id="expiryDate"
value={paymentInfo.expiryDate}
onChange={(e) => handlePaymentInfoChange('expiryDate', e.target.value)}
placeholder="MM/YY"
maxLength={5}
/>
</div>
<div>
<Label htmlFor="cvv">CVV *</Label>
<Input
id="cvv"
value={paymentInfo.cvv}
onChange={(e) => handlePaymentInfoChange('cvv', e.target.value)}
placeholder="123"
maxLength={4}
/>
</div>
</div>
</div>
)}
{/* Other Payment Methods Info */}
{paymentInfo.method === 'paypal' && (
<div className="bg-blue-50 p-4 rounded-lg border-t">
<p className="text-sm text-blue-700">
Serás redirigido a PayPal para completar tu pago de forma segura.
</p>
</div>
)}
{paymentInfo.method === 'bank_transfer' && (
<div className="bg-green-50 p-4 rounded-lg border-t">
<p className="text-sm text-green-700 mb-2">
Te enviaremos los detalles de la cuenta bancaria para realizar la transferencia.
</p>
<p className="text-xs text-green-600">
Tu reserva se confirmará una vez recibamos el comprobante de pago.
</p>
</div>
)}
{paymentInfo.method === 'cash' && (
<div className="bg-orange-50 p-4 rounded-lg border-t">
<p className="text-sm text-orange-700 mb-2">
Podrás pagar en efectivo al momento del check-in o al llegar al lugar.
</p>
<p className="text-xs text-orange-600">
Tu reserva quedará pendiente hasta el pago.
</p>
</div>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={() => setStep(1)}>
Volver
</Button>
<Button
className="flex-1"
onClick={() => setStep(3)}
disabled={!validateStep2()}
>
Revisar Orden
</Button>
</div>
</CardContent>
</Card>
)}
{step === 3 && (
<Card>
<CardHeader>
<CardTitle>Confirmar Reserva</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold mb-2">Información de Contacto</h3>
<p className="text-sm">{customerInfo.firstName} {customerInfo.lastName}</p>
<p className="text-sm">{customerInfo.email}</p>
<p className="text-sm">{customerInfo.phone}</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold mb-2">Método de Pago</h3>
<p className="text-sm">
{paymentMethods.find(m => m.id === paymentInfo.method)?.name}
{paymentInfo.method === 'credit_card' && paymentInfo.cardNumber &&
` terminada en ${paymentInfo.cardNumber.slice(-4)}`
}
</p>
</div>
{specialRequests && (
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold mb-2">Solicitudes Especiales</h3>
<p className="text-sm">{specialRequests}</p>
</div>
)}
<div className="flex items-center p-4 bg-blue-50 rounded-lg">
<Shield className="w-5 h-5 text-blue-600 mr-3" />
<div>
<p className="text-sm font-medium text-blue-900">Pago Seguro</p>
<p className="text-xs text-blue-700">
Tu información está protegida con encriptación SSL
</p>
</div>
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setStep(2)}>
Volver
</Button>
<Button
className="flex-1"
onClick={processPayment}
disabled={isProcessing}
>
{isProcessing ? 'Procesando...' : `Confirmar y Pagar $${finalTotal.toFixed(2)}`}
</Button>
</div>
</CardContent>
</Card>
)}
</div>
{/* Order Summary Sidebar */}
<div className="lg:col-span-1">
<Card className="sticky top-24">
<CardHeader>
<CardTitle>Resumen de Orden</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Cart Items */}
<div className="space-y-3">
{items.map((item) => (
<div key={item.id} className="flex space-x-3">
<img
src={item.image}
alt={item.title}
className="w-16 h-16 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm truncate">{item.title}</h4>
<p className="text-xs text-gray-500">{item.location}</p>
{item.selectedDate && (
<p className="text-xs text-gray-600">Fecha: {item.selectedDate}</p>
)}
{item.guests && (
<p className="text-xs text-gray-600">Personas: {item.guests}</p>
)}
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-gray-500">Qty: {item.quantity}</span>
<span className="text-sm font-semibold">${(item.price * item.quantity).toFixed(2)}</span>
</div>
</div>
</div>
))}
</div>
<Separator />
{/* Pricing Breakdown */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Subtotal</span>
<span>${totalPrice.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span>ITBIS (18%)</span>
<span>${taxes.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span>Fee de servicio (5%)</span>
<span>${serviceFee.toFixed(2)}</span>
</div>
<Separator />
<div className="flex justify-between font-semibold">
<span>Total</span>
<span>${finalTotal.toFixed(2)}</span>
</div>
</div>
<div className="text-xs text-gray-500 mt-4">
<p> Cancelación gratuita hasta 24 horas antes</p>
<p> Recibirás confirmación por email</p>
<p> Soporte al cliente 24/7</p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<Footer />
</div>
);
};
export default Checkout;