feat: Complete reservation management

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 21:57:48 +00:00
parent 5e0260f764
commit f5927dd299
4 changed files with 695 additions and 61 deletions

View File

@@ -0,0 +1,295 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
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 { Loader2 } from 'lucide-react';
import { Reservation, Listing } from '@/hooks/useChannelManager';
interface ReservationFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: any) => Promise<boolean>;
reservation?: Reservation | null;
listings: Listing[];
mode: 'create' | 'edit';
}
export const ReservationFormModal: React.FC<ReservationFormModalProps> = ({
open,
onClose,
onSubmit,
reservation,
listings,
mode
}) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
listingId: '',
guestName: '',
guestEmail: '',
guestPhone: '',
checkIn: '',
checkOut: '',
guests: 1,
totalAmount: 0,
status: 'pending' as 'confirmed' | 'pending' | 'cancelled' | 'completed',
paymentStatus: 'pending' as 'paid' | 'pending' | 'refunded',
channel: 'direct',
specialRequests: '',
});
useEffect(() => {
if (reservation && mode === 'edit') {
setFormData({
listingId: reservation.listingId,
guestName: reservation.guestName,
guestEmail: reservation.guestEmail,
guestPhone: reservation.guestPhone,
checkIn: reservation.checkIn,
checkOut: reservation.checkOut || '',
guests: reservation.guests,
totalAmount: reservation.totalAmount,
status: reservation.status,
paymentStatus: reservation.paymentStatus,
channel: reservation.channel,
specialRequests: reservation.specialRequests || '',
});
} else if (mode === 'create') {
setFormData({
listingId: '',
guestName: '',
guestEmail: '',
guestPhone: '',
checkIn: '',
checkOut: '',
guests: 1,
totalAmount: 0,
status: 'pending',
paymentStatus: 'pending',
channel: 'direct',
specialRequests: '',
});
}
}, [reservation, mode, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const success = await onSubmit(formData);
if (success) {
onClose();
}
setLoading(false);
};
const handleChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{mode === 'create' ? 'Crear Nueva Reserva' : 'Editar Reserva'}
</DialogTitle>
<DialogDescription>
{mode === 'create'
? 'Complete los datos para crear una nueva reserva'
: 'Modifique los datos de la reserva'}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Listing Selection */}
<div className="space-y-2">
<Label htmlFor="listing">Propiedad *</Label>
<Select
value={formData.listingId}
onValueChange={(value) => handleChange('listingId', value)}
disabled={mode === 'edit'}
>
<SelectTrigger>
<SelectValue placeholder="Seleccione una propiedad" />
</SelectTrigger>
<SelectContent>
{listings.map((listing) => (
<SelectItem key={listing.id} value={listing.id}>
{listing.name} ({listing.type})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Guest Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="guestName">Nombre del Huésped *</Label>
<Input
id="guestName"
value={formData.guestName}
onChange={(e) => handleChange('guestName', e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="guestEmail">Email *</Label>
<Input
id="guestEmail"
type="email"
value={formData.guestEmail}
onChange={(e) => handleChange('guestEmail', e.target.value)}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="guestPhone">Teléfono *</Label>
<Input
id="guestPhone"
type="tel"
value={formData.guestPhone}
onChange={(e) => handleChange('guestPhone', e.target.value)}
required
/>
</div>
{/* Dates */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="checkIn">Check-in *</Label>
<Input
id="checkIn"
type="datetime-local"
value={formData.checkIn}
onChange={(e) => handleChange('checkIn', e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="checkOut">Check-out</Label>
<Input
id="checkOut"
type="datetime-local"
value={formData.checkOut}
onChange={(e) => handleChange('checkOut', e.target.value)}
/>
</div>
</div>
{/* Guests and Amount */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="guests">Número de Huéspedes *</Label>
<Input
id="guests"
type="number"
min="1"
value={formData.guests}
onChange={(e) => handleChange('guests', parseInt(e.target.value))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="totalAmount">Monto Total *</Label>
<Input
id="totalAmount"
type="number"
min="0"
step="0.01"
value={formData.totalAmount}
onChange={(e) => handleChange('totalAmount', parseFloat(e.target.value))}
required
/>
</div>
</div>
{/* Status */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="status">Estado de Reserva</Label>
<Select
value={formData.status}
onValueChange={(value) => handleChange('status', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pending">Pendiente</SelectItem>
<SelectItem value="confirmed">Confirmada</SelectItem>
<SelectItem value="completed">Completada</SelectItem>
<SelectItem value="cancelled">Cancelada</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="paymentStatus">Estado de Pago</Label>
<Select
value={formData.paymentStatus}
onValueChange={(value) => handleChange('paymentStatus', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pending">Pendiente</SelectItem>
<SelectItem value="paid">Pagado</SelectItem>
<SelectItem value="refunded">Reembolsado</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Channel */}
<div className="space-y-2">
<Label htmlFor="channel">Canal</Label>
<Select
value={formData.channel}
onValueChange={(value) => handleChange('channel', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="direct">Directo</SelectItem>
<SelectItem value="booking.com">Booking.com</SelectItem>
<SelectItem value="expedia">Expedia</SelectItem>
<SelectItem value="airbnb">Airbnb</SelectItem>
<SelectItem value="vrbo">VRBO</SelectItem>
<SelectItem value="agoda">Agoda</SelectItem>
</SelectContent>
</Select>
</div>
{/* Special Requests */}
<div className="space-y-2">
<Label htmlFor="specialRequests">Solicitudes Especiales</Label>
<Textarea
id="specialRequests"
value={formData.specialRequests}
onChange={(e) => handleChange('specialRequests', e.target.value)}
rows={3}
placeholder="Solicitudes o notas especiales..."
/>
</div>
<DialogFooter className="gap-2">
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{mode === 'create' ? 'Crear Reserva' : 'Guardar Cambios'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,129 @@
import React from 'react';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { Search, X } from 'lucide-react';
interface ReservationsFiltersProps {
filters: {
search: string;
status: string;
paymentStatus: string;
channel: string;
listingType: string;
dateFrom: string;
dateTo: string;
};
onFilterChange: (key: string, value: string) => void;
onClearFilters: () => void;
}
export const ReservationsFilters: React.FC<ReservationsFiltersProps> = ({
filters,
onFilterChange,
onClearFilters
}) => {
const hasActiveFilters = Object.values(filters).some(value => value !== '' && value !== 'all');
return (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Search */}
<div className="relative md:col-span-2">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por huésped, email, ID..."
value={filters.search}
onChange={(e) => onFilterChange('search', e.target.value)}
className="pl-10"
/>
</div>
{/* Status Filter */}
<Select value={filters.status} onValueChange={(value) => onFilterChange('status', value)}>
<SelectTrigger>
<SelectValue placeholder="Estado de reserva" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los estados</SelectItem>
<SelectItem value="pending">Pendiente</SelectItem>
<SelectItem value="confirmed">Confirmada</SelectItem>
<SelectItem value="completed">Completada</SelectItem>
<SelectItem value="cancelled">Cancelada</SelectItem>
</SelectContent>
</Select>
{/* Payment Status Filter */}
<Select value={filters.paymentStatus} onValueChange={(value) => onFilterChange('paymentStatus', value)}>
<SelectTrigger>
<SelectValue placeholder="Estado de pago" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los pagos</SelectItem>
<SelectItem value="pending">Pendiente</SelectItem>
<SelectItem value="paid">Pagado</SelectItem>
<SelectItem value="refunded">Reembolsado</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Channel Filter */}
<Select value={filters.channel} onValueChange={(value) => onFilterChange('channel', value)}>
<SelectTrigger>
<SelectValue placeholder="Canal" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los canales</SelectItem>
<SelectItem value="direct">Directo</SelectItem>
<SelectItem value="booking.com">Booking.com</SelectItem>
<SelectItem value="expedia">Expedia</SelectItem>
<SelectItem value="airbnb">Airbnb</SelectItem>
<SelectItem value="vrbo">VRBO</SelectItem>
<SelectItem value="agoda">Agoda</SelectItem>
</SelectContent>
</Select>
{/* Listing Type Filter */}
<Select value={filters.listingType} onValueChange={(value) => onFilterChange('listingType', value)}>
<SelectTrigger>
<SelectValue placeholder="Tipo de propiedad" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los tipos</SelectItem>
<SelectItem value="hotel">Hotel</SelectItem>
<SelectItem value="restaurant">Restaurante</SelectItem>
<SelectItem value="vehicle">Vehículo</SelectItem>
<SelectItem value="flight">Vuelo</SelectItem>
<SelectItem value="activity">Actividad</SelectItem>
</SelectContent>
</Select>
{/* Date From */}
<Input
type="date"
value={filters.dateFrom}
onChange={(e) => onFilterChange('dateFrom', e.target.value)}
placeholder="Desde"
/>
{/* Date To */}
<Input
type="date"
value={filters.dateTo}
onChange={(e) => onFilterChange('dateTo', e.target.value)}
placeholder="Hasta"
/>
</div>
{hasActiveFilters && (
<div className="flex justify-end">
<Button variant="outline" size="sm" onClick={onClearFilters}>
<X className="mr-2 h-4 w-4" />
Limpiar Filtros
</Button>
</div>
)}
</div>
);
};