748 lines
28 KiB
TypeScript
748 lines
28 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import {
|
|
Settings,
|
|
Zap,
|
|
TrendingUp,
|
|
Calendar,
|
|
Users,
|
|
DollarSign,
|
|
Plus,
|
|
Search,
|
|
MoreHorizontal,
|
|
CheckCircle,
|
|
AlertTriangle,
|
|
XCircle,
|
|
RefreshCw,
|
|
Eye,
|
|
Edit,
|
|
Hotel,
|
|
Utensils,
|
|
Car,
|
|
Plane,
|
|
MapPin,
|
|
Globe,
|
|
BarChart3
|
|
} from 'lucide-react';
|
|
import { useChannelManager, Reservation } from '@/hooks/useChannelManager';
|
|
import { format } from 'date-fns';
|
|
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';
|
|
|
|
const ChannelManager = () => {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const {
|
|
channels,
|
|
listings,
|
|
reservations,
|
|
stats,
|
|
loading,
|
|
error,
|
|
connectChannel,
|
|
disconnectChannel,
|
|
syncChannel,
|
|
createListing,
|
|
updateListing,
|
|
createReservation,
|
|
updateReservation,
|
|
cancelReservation,
|
|
loadStats,
|
|
clearError
|
|
} = useChannelManager();
|
|
|
|
// Get active tab from URL or default to 'overview'
|
|
const queryParams = new URLSearchParams(location.search);
|
|
const activeTab = queryParams.get('tab') || 'overview';
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [filterType, setFilterType] = useState('all');
|
|
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);
|
|
if (success) {
|
|
toast({
|
|
title: 'Canal conectado',
|
|
description: 'El canal se ha conectado exitosamente',
|
|
});
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'No se pudo conectar el canal',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
return success;
|
|
};
|
|
|
|
const handleCreateListing = async (data: any) => {
|
|
const success = await createListing(data);
|
|
if (success) {
|
|
toast({
|
|
title: 'Propiedad creada',
|
|
description: 'La propiedad se ha creado exitosamente',
|
|
});
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'No se pudo crear la propiedad',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
return success;
|
|
};
|
|
|
|
const handleViewReservation = (reservation: Reservation) => {
|
|
setSelectedReservation(reservation);
|
|
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':
|
|
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
case 'syncing':
|
|
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
|
|
case 'error':
|
|
return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
|
|
default:
|
|
return <XCircle className="w-4 h-4 text-red-500" />;
|
|
}
|
|
};
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'hotel':
|
|
return <Hotel className="w-4 h-4" />;
|
|
case 'restaurant':
|
|
return <Utensils className="w-4 h-4" />;
|
|
case 'vehicle':
|
|
return <Car className="w-4 h-4" />;
|
|
case 'flight':
|
|
return <Plane className="w-4 h-4" />;
|
|
case 'activity':
|
|
return <MapPin className="w-4 h-4" />;
|
|
default:
|
|
return <Globe className="w-4 h-4" />;
|
|
}
|
|
};
|
|
|
|
const OverviewTab = () => (
|
|
<div className="space-y-6">
|
|
{/* Key Metrics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">Ingresos Totales</p>
|
|
<p className="text-2xl font-bold">${stats?.totalRevenue.toLocaleString() || '0'}</p>
|
|
</div>
|
|
<DollarSign className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">Reservas Totales</p>
|
|
<p className="text-2xl font-bold">{stats?.totalBookings || 0}</p>
|
|
</div>
|
|
<Calendar className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">Tasa de Ocupación</p>
|
|
<p className="text-2xl font-bold">{stats?.occupancyRate || 0}%</p>
|
|
</div>
|
|
<TrendingUp className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">Canales Conectados</p>
|
|
<p className="text-2xl font-bold">{channels.filter(c => c.status === 'connected').length}</p>
|
|
</div>
|
|
<Zap className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Channel Performance */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Rendimiento por Canal</CardTitle>
|
|
<CardDescription>Ingresos y reservas por cada canal de distribución</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{stats?.channelPerformance.map((channel, index) => (
|
|
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
|
<Globe className="w-5 h-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{channel.channelName}</p>
|
|
<p className="text-sm text-muted-foreground">{channel.bookings} reservas</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-semibold">${channel.revenue.toLocaleString()}</p>
|
|
<p className="text-sm text-muted-foreground">Ingresos</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Recent Reservations */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Reservas Recientes</CardTitle>
|
|
<CardDescription>Últimas reservas recibidas en todos los canales</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{reservations.slice(0, 5).map((reservation) => (
|
|
<div key={reservation.id} className="flex items-center justify-between p-3 border rounded-lg">
|
|
<div className="flex items-center space-x-3">
|
|
{getTypeIcon(reservation.listingType)}
|
|
<div>
|
|
<p className="font-medium">{reservation.guestName}</p>
|
|
<p className="text-sm text-muted-foreground">{reservation.listingName}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<Badge variant={
|
|
reservation.status === 'confirmed' ? 'default' :
|
|
reservation.status === 'pending' ? 'secondary' :
|
|
reservation.status === 'cancelled' ? 'destructive' : 'outline'
|
|
}>
|
|
{reservation.status}
|
|
</Badge>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
${reservation.totalAmount.toLocaleString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
const ChannelsTab = () => (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">Gestión de Canales</h2>
|
|
<p className="text-muted-foreground">Conecta y administra tus canales de distribución</p>
|
|
</div>
|
|
<Button onClick={() => setShowConnectModal(true)}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Conectar Canal
|
|
</Button>
|
|
</div>
|
|
|
|
{channels.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-12 text-center">
|
|
<Globe className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">No hay canales conectados</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Conecta tu primer canal de distribución para comenzar a sincronizar reservas
|
|
</p>
|
|
<Button onClick={() => setShowConnectModal(true)}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Conectar Primer Canal
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{channels.map((channel) => (
|
|
<Card key={channel.id}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
|
<Globe className="w-6 h-6 text-primary" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<h3 className="text-lg font-semibold">{channel.name}</h3>
|
|
{getStatusIcon(channel.status)}
|
|
<Badge variant="outline">{channel.type}</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Última sincronización: {format(new Date(channel.lastSync), 'dd/MM/yyyy HH:mm', { locale: es })}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{channel.properties.length} propiedades conectadas
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="text-right">
|
|
<p className="text-2xl font-bold">${channel.revenue.toLocaleString()}</p>
|
|
<p className="text-sm text-muted-foreground">{channel.bookings} reservas</p>
|
|
<p className="text-xs text-muted-foreground">Comisión: {channel.commission}%</p>
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => syncChannel(channel.id)}
|
|
disabled={loading}
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
<Button variant="outline" size="sm">
|
|
<Settings className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => disconnectChannel(channel.id)}
|
|
>
|
|
<XCircle className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const ListingsTab = () => {
|
|
const filteredListings = listings.filter(listing => {
|
|
const matchesSearch = listing.name.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesType = filterType === 'all' || listing.type === filterType;
|
|
return matchesSearch && matchesType;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">Gestión de Propiedades</h2>
|
|
<p className="text-muted-foreground">Administra hoteles, restaurantes, vehículos y más</p>
|
|
</div>
|
|
<Button onClick={() => setShowListingModal(true)}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Nueva Propiedad
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex space-x-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
<Input
|
|
placeholder="Buscar propiedades..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
<Select value={filterType} onValueChange={setFilterType}>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue placeholder="Filtrar por tipo" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todos los tipos</SelectItem>
|
|
<SelectItem value="hotel">Hoteles</SelectItem>
|
|
<SelectItem value="restaurant">Restaurantes</SelectItem>
|
|
<SelectItem value="vehicle">Vehículos</SelectItem>
|
|
<SelectItem value="flight">Vuelos</SelectItem>
|
|
<SelectItem value="activity">Actividades</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="grid gap-4">
|
|
{filteredListings.map((listing) => (
|
|
<Card key={listing.id}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-16 h-16 bg-muted rounded-lg flex items-center justify-center">
|
|
{listing.images[0] ? (
|
|
<img
|
|
src={listing.images[0]}
|
|
alt={listing.name}
|
|
className="w-full h-full object-cover rounded-lg"
|
|
/>
|
|
) : (
|
|
getTypeIcon(listing.type)
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<h3 className="text-lg font-semibold">{listing.name}</h3>
|
|
<Badge variant={listing.status === 'active' ? 'default' : 'secondary'}>
|
|
{listing.status}
|
|
</Badge>
|
|
<Badge variant="outline">{listing.type}</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">{listing.location.address}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Conectado a {listing.channels.length} canales
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="text-right">
|
|
<p className="text-xl font-bold">${listing.basePrice}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{listing.availability ? 'Disponible' : 'No disponible'}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
<Button variant="outline" size="sm" 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>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ReservationsTab = () => (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<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>
|
|
|
|
<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>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground text-center">
|
|
Mostrando {filteredReservations.length} de {reservations.length} reservas
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mb-6">
|
|
<h1 className="text-3xl font-bold">Channel Manager</h1>
|
|
<p className="text-muted-foreground">
|
|
Gestiona todos tus canales de distribución, propiedades y reservas desde un solo lugar
|
|
</p>
|
|
</div>
|
|
|
|
{activeTab === 'overview' && <OverviewTab />}
|
|
{activeTab === 'channels' && <ChannelsTab />}
|
|
{activeTab === 'listings' && <ListingsTab />}
|
|
{activeTab === 'reservations' && <ReservationsTab />}
|
|
{activeTab === 'analytics' && <AnalyticsTab stats={stats} onDateRangeChange={loadStats} />}
|
|
|
|
{/* Modals */}
|
|
<ChannelConnectionModal
|
|
open={showConnectModal}
|
|
onClose={() => setShowConnectModal(false)}
|
|
onConnect={handleConnectChannel}
|
|
/>
|
|
|
|
<ListingFormModal
|
|
open={showListingModal}
|
|
onClose={() => setShowListingModal(false)}
|
|
onCreate={handleCreateListing}
|
|
onUpdate={updateListing}
|
|
channels={channels}
|
|
/>
|
|
|
|
<ReservationDetailsModal
|
|
open={showReservationModal}
|
|
onClose={() => {
|
|
setShowReservationModal(false);
|
|
setSelectedReservation(null);
|
|
}}
|
|
reservation={selectedReservation}
|
|
onCancel={handleCancelReservation}
|
|
/>
|
|
|
|
<ReservationFormModal
|
|
open={showReservationForm}
|
|
onClose={() => {
|
|
setShowReservationForm(false);
|
|
setSelectedReservation(null);
|
|
}}
|
|
onSubmit={reservationFormMode === 'create' ? handleCreateReservation : handleUpdateReservation}
|
|
reservation={selectedReservation}
|
|
listings={listings}
|
|
mode={reservationFormMode}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChannelManager;
|