feat: Complete reservation management
This commit is contained in:
295
src/components/channel-manager/ReservationFormModal.tsx
Normal file
295
src/components/channel-manager/ReservationFormModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
129
src/components/channel-manager/ReservationsFilters.tsx
Normal file
129
src/components/channel-manager/ReservationsFilters.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -207,6 +207,36 @@ export const useChannelManager = () => {
|
|||||||
}
|
}
|
||||||
}, [loadListings]);
|
}, [loadListings]);
|
||||||
|
|
||||||
|
// Create reservation
|
||||||
|
const createReservation = useCallback(async (reservationData: any) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await ChannelManagerService.createReservation(reservationData);
|
||||||
|
await loadReservations();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error creating reservation');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [loadReservations]);
|
||||||
|
|
||||||
|
// Update reservation
|
||||||
|
const updateReservation = useCallback(async (id: string, reservationData: any) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await ChannelManagerService.updateReservation(id, reservationData);
|
||||||
|
await loadReservations();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Error updating reservation');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [loadReservations]);
|
||||||
|
|
||||||
// Cancel reservation
|
// Cancel reservation
|
||||||
const cancelReservation = useCallback(async (id: string, reason?: string) => {
|
const cancelReservation = useCallback(async (id: string, reason?: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -261,6 +291,8 @@ export const useChannelManager = () => {
|
|||||||
syncChannel,
|
syncChannel,
|
||||||
createListing,
|
createListing,
|
||||||
updateListing,
|
updateListing,
|
||||||
|
createReservation,
|
||||||
|
updateReservation,
|
||||||
cancelReservation,
|
cancelReservation,
|
||||||
clearError,
|
clearError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import { es } from 'date-fns/locale';
|
|||||||
import { ChannelConnectionModal } from '@/components/channel-manager/ChannelConnectionModal';
|
import { ChannelConnectionModal } from '@/components/channel-manager/ChannelConnectionModal';
|
||||||
import { ListingFormModal } from '@/components/channel-manager/ListingFormModal';
|
import { ListingFormModal } from '@/components/channel-manager/ListingFormModal';
|
||||||
import { ReservationDetailsModal } from '@/components/channel-manager/ReservationDetailsModal';
|
import { ReservationDetailsModal } from '@/components/channel-manager/ReservationDetailsModal';
|
||||||
|
import { ReservationFormModal } from '@/components/channel-manager/ReservationFormModal';
|
||||||
|
import { ReservationsFilters } from '@/components/channel-manager/ReservationsFilters';
|
||||||
import { AnalyticsTab } from '@/components/channel-manager/AnalyticsTab';
|
import { AnalyticsTab } from '@/components/channel-manager/AnalyticsTab';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
@@ -52,6 +54,8 @@ const ChannelManager = () => {
|
|||||||
syncChannel,
|
syncChannel,
|
||||||
createListing,
|
createListing,
|
||||||
updateListing,
|
updateListing,
|
||||||
|
createReservation,
|
||||||
|
updateReservation,
|
||||||
cancelReservation,
|
cancelReservation,
|
||||||
loadStats,
|
loadStats,
|
||||||
clearError
|
clearError
|
||||||
@@ -63,7 +67,18 @@ const ChannelManager = () => {
|
|||||||
const [showConnectModal, setShowConnectModal] = useState(false);
|
const [showConnectModal, setShowConnectModal] = useState(false);
|
||||||
const [showListingModal, setShowListingModal] = useState(false);
|
const [showListingModal, setShowListingModal] = useState(false);
|
||||||
const [showReservationModal, setShowReservationModal] = useState(false);
|
const [showReservationModal, setShowReservationModal] = useState(false);
|
||||||
|
const [showReservationForm, setShowReservationForm] = useState(false);
|
||||||
|
const [reservationFormMode, setReservationFormMode] = useState<'create' | 'edit'>('create');
|
||||||
const [selectedReservation, setSelectedReservation] = useState<Reservation | null>(null);
|
const [selectedReservation, setSelectedReservation] = useState<Reservation | null>(null);
|
||||||
|
const [reservationFilters, setReservationFilters] = useState({
|
||||||
|
search: '',
|
||||||
|
status: 'all',
|
||||||
|
paymentStatus: 'all',
|
||||||
|
channel: 'all',
|
||||||
|
listingType: 'all',
|
||||||
|
dateFrom: '',
|
||||||
|
dateTo: ''
|
||||||
|
});
|
||||||
|
|
||||||
const handleConnectChannel = async (data: any) => {
|
const handleConnectChannel = async (data: any) => {
|
||||||
const success = await connectChannel(data);
|
const success = await connectChannel(data);
|
||||||
@@ -104,6 +119,104 @@ const ChannelManager = () => {
|
|||||||
setShowReservationModal(true);
|
setShowReservationModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateReservation = async (data: any) => {
|
||||||
|
const success = await createReservation(data);
|
||||||
|
if (success) {
|
||||||
|
toast({
|
||||||
|
title: 'Reserva creada',
|
||||||
|
description: 'La reserva se ha creado exitosamente',
|
||||||
|
});
|
||||||
|
setShowReservationForm(false);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'No se pudo crear la reserva',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateReservation = async (data: any) => {
|
||||||
|
if (!selectedReservation) return false;
|
||||||
|
const success = await updateReservation(selectedReservation.id, data);
|
||||||
|
if (success) {
|
||||||
|
toast({
|
||||||
|
title: 'Reserva actualizada',
|
||||||
|
description: 'La reserva se ha actualizado exitosamente',
|
||||||
|
});
|
||||||
|
setShowReservationForm(false);
|
||||||
|
setSelectedReservation(null);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'No se pudo actualizar la reserva',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelReservation = async (id: string, reason?: string) => {
|
||||||
|
const success = await cancelReservation(id, reason);
|
||||||
|
if (success) {
|
||||||
|
toast({
|
||||||
|
title: 'Reserva cancelada',
|
||||||
|
description: 'La reserva se ha cancelado exitosamente',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'No se pudo cancelar la reserva',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (key: string, value: string) => {
|
||||||
|
setReservationFilters(prev => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setReservationFilters({
|
||||||
|
search: '',
|
||||||
|
status: 'all',
|
||||||
|
paymentStatus: 'all',
|
||||||
|
channel: 'all',
|
||||||
|
listingType: 'all',
|
||||||
|
dateFrom: '',
|
||||||
|
dateTo: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredReservations = reservations.filter(reservation => {
|
||||||
|
if (reservationFilters.search && !reservation.guestName.toLowerCase().includes(reservationFilters.search.toLowerCase()) &&
|
||||||
|
!reservation.guestEmail.toLowerCase().includes(reservationFilters.search.toLowerCase()) &&
|
||||||
|
!reservation.id.toLowerCase().includes(reservationFilters.search.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservationFilters.status !== 'all' && reservation.status !== reservationFilters.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservationFilters.paymentStatus !== 'all' && reservation.paymentStatus !== reservationFilters.paymentStatus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservationFilters.channel !== 'all' && reservation.channel !== reservationFilters.channel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservationFilters.listingType !== 'all' && reservation.listingType !== reservationFilters.listingType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservationFilters.dateFrom && reservation.checkIn < reservationFilters.dateFrom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservationFilters.dateTo && reservation.checkIn > reservationFilters.dateTo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
@@ -455,10 +568,48 @@ const ChannelManager = () => {
|
|||||||
<h2 className="text-2xl font-bold">Gestión de Reservas</h2>
|
<h2 className="text-2xl font-bold">Gestión de Reservas</h2>
|
||||||
<p className="text-muted-foreground">Administra todas las reservas desde un solo lugar</p>
|
<p className="text-muted-foreground">Administra todas las reservas desde un solo lugar</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setReservationFormMode('create');
|
||||||
|
setSelectedReservation(null);
|
||||||
|
setShowReservationForm(true);
|
||||||
|
}}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Nueva Reserva
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ReservationsFilters
|
||||||
|
filters={reservationFilters}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onClearFilters={handleClearFilters}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredReservations.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-12 text-center">
|
||||||
|
<Calendar className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">No se encontraron reservas</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
{reservations.length === 0
|
||||||
|
? 'Aún no hay reservas registradas. Crea tu primera reserva para comenzar.'
|
||||||
|
: 'Intenta ajustar los filtros para ver más resultados.'}
|
||||||
|
</p>
|
||||||
|
{reservations.length === 0 && (
|
||||||
|
<Button onClick={() => {
|
||||||
|
setReservationFormMode('create');
|
||||||
|
setSelectedReservation(null);
|
||||||
|
setShowReservationForm(true);
|
||||||
|
}}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Crear Primera Reserva
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{reservations.map((reservation) => (
|
{filteredReservations.map((reservation) => (
|
||||||
<Card key={reservation.id}>
|
<Card key={reservation.id}>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -503,14 +654,20 @@ const ChannelManager = () => {
|
|||||||
onClick={() => handleViewReservation(reservation)}
|
onClick={() => handleViewReservation(reservation)}
|
||||||
title="Ver detalles"
|
title="Ver detalles"
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" title="Editar">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedReservation(reservation);
|
||||||
|
setReservationFormMode('edit');
|
||||||
|
setShowReservationForm(true);
|
||||||
|
}}
|
||||||
|
title="Editar"
|
||||||
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" title="Más opciones">
|
|
||||||
<MoreHorizontal className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -518,6 +675,12 @@ const ChannelManager = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-muted-foreground text-center">
|
||||||
|
Mostrando {filteredReservations.length} de {reservations.length} reservas
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -592,9 +755,24 @@ const ChannelManager = () => {
|
|||||||
|
|
||||||
<ReservationDetailsModal
|
<ReservationDetailsModal
|
||||||
open={showReservationModal}
|
open={showReservationModal}
|
||||||
onClose={() => setShowReservationModal(false)}
|
onClose={() => {
|
||||||
|
setShowReservationModal(false);
|
||||||
|
setSelectedReservation(null);
|
||||||
|
}}
|
||||||
reservation={selectedReservation}
|
reservation={selectedReservation}
|
||||||
onCancel={cancelReservation}
|
onCancel={handleCancelReservation}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReservationFormModal
|
||||||
|
open={showReservationForm}
|
||||||
|
onClose={() => {
|
||||||
|
setShowReservationForm(false);
|
||||||
|
setSelectedReservation(null);
|
||||||
|
}}
|
||||||
|
onSubmit={reservationFormMode === 'create' ? handleCreateReservation : handleUpdateReservation}
|
||||||
|
reservation={selectedReservation}
|
||||||
|
listings={listings}
|
||||||
|
mode={reservationFormMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user