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

@@ -35,6 +35,8 @@ import { es } from 'date-fns/locale';
import { ChannelConnectionModal } from '@/components/channel-manager/ChannelConnectionModal';
import { ListingFormModal } from '@/components/channel-manager/ListingFormModal';
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 { useToast } from '@/hooks/use-toast';
@@ -52,6 +54,8 @@ const ChannelManager = () => {
syncChannel,
createListing,
updateListing,
createReservation,
updateReservation,
cancelReservation,
loadStats,
clearError
@@ -63,7 +67,18 @@ const ChannelManager = () => {
const [showConnectModal, setShowConnectModal] = useState(false);
const [showListingModal, setShowListingModal] = 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 [reservationFilters, setReservationFilters] = useState({
search: '',
status: 'all',
paymentStatus: 'all',
channel: 'all',
listingType: 'all',
dateFrom: '',
dateTo: ''
});
const handleConnectChannel = async (data: any) => {
const success = await connectChannel(data);
@@ -104,6 +119,104 @@ const ChannelManager = () => {
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) => {
switch (status) {
case 'connected':
@@ -455,69 +568,119 @@ const ChannelManager = () => {
<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>
</div>
<Button onClick={() => {
setReservationFormMode('create');
setSelectedReservation(null);
setShowReservationForm(true);
}}>
<Plus className="w-4 h-4 mr-2" />
Nueva Reserva
</Button>
</div>
<div className="grid gap-4">
{reservations.map((reservation) => (
<Card key={reservation.id}>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{getTypeIcon(reservation.listingType)}
<div>
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold">{reservation.guestName}</h3>
<Badge variant={
reservation.status === 'confirmed' ? 'default' :
reservation.status === 'pending' ? 'secondary' :
reservation.status === 'cancelled' ? 'destructive' : 'outline'
}>
{reservation.status}
</Badge>
<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">
{filteredReservations.map((reservation) => (
<Card key={reservation.id}>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{getTypeIcon(reservation.listingType)}
<div>
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold">{reservation.guestName}</h3>
<Badge variant={
reservation.status === 'confirmed' ? 'default' :
reservation.status === 'pending' ? 'secondary' :
reservation.status === 'cancelled' ? 'destructive' : 'outline'
}>
{reservation.status}
</Badge>
</div>
<p className="text-sm font-medium">{reservation.listingName}</p>
<p className="text-sm text-muted-foreground">
{format(new Date(reservation.checkIn), 'dd/MM/yyyy', { locale: es })}
{reservation.checkOut && ` - ${format(new Date(reservation.checkOut), 'dd/MM/yyyy', { locale: es })}`}
</p>
<p className="text-sm text-muted-foreground">
{reservation.guests} huéspedes {reservation.channel}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-xl font-bold">${reservation.totalAmount.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">
Pago: <Badge variant={reservation.paymentStatus === 'paid' ? 'default' : 'secondary'}>
{reservation.paymentStatus}
</Badge>
</p>
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleViewReservation(reservation)}
title="Ver detalles"
>
<Eye className="w-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedReservation(reservation);
setReservationFormMode('edit');
setShowReservationForm(true);
}}
title="Editar"
>
<Edit className="w-4 h-4" />
</Button>
</div>
</div>
<p className="text-sm font-medium">{reservation.listingName}</p>
<p className="text-sm text-muted-foreground">
{format(new Date(reservation.checkIn), 'dd/MM/yyyy', { locale: es })}
{reservation.checkOut && ` - ${format(new Date(reservation.checkOut), 'dd/MM/yyyy', { locale: es })}`}
</p>
<p className="text-sm text-muted-foreground">
{reservation.guests} huéspedes {reservation.channel}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-xl font-bold">${reservation.totalAmount.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">
Pago: <Badge variant={reservation.paymentStatus === 'paid' ? 'default' : 'secondary'}>
{reservation.paymentStatus}
</Badge>
</p>
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleViewReservation(reservation)}
title="Ver detalles"
>
<Eye className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm" title="Editar">
<Edit className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm" title="Más opciones">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
))}
</div>
<div className="text-sm text-muted-foreground text-center">
Mostrando {filteredReservations.length} de {reservations.length} reservas
</div>
</>
)}
</div>
);
@@ -592,9 +755,24 @@ const ChannelManager = () => {
<ReservationDetailsModal
open={showReservationModal}
onClose={() => setShowReservationModal(false)}
onClose={() => {
setShowReservationModal(false);
setSelectedReservation(null);
}}
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>
);