feat: Complete Channel Manager

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 21:48:09 +00:00
parent 3d626de63f
commit 5e0260f764
5 changed files with 959 additions and 101 deletions

View File

@@ -14,7 +14,6 @@ import {
DollarSign,
Plus,
Search,
Filter,
MoreHorizontal,
CheckCircle,
AlertTriangle,
@@ -22,19 +21,25 @@ import {
RefreshCw,
Eye,
Edit,
Trash2,
Hotel,
Utensils,
Car,
Plane,
MapPin,
Globe
Globe,
BarChart3
} from 'lucide-react';
import { useChannelManager } from '@/hooks/useChannelManager';
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 { AnalyticsTab } from '@/components/channel-manager/AnalyticsTab';
import { useToast } from '@/hooks/use-toast';
const ChannelManager = () => {
const { toast } = useToast();
const {
channels,
listings,
@@ -46,6 +51,9 @@ const ChannelManager = () => {
disconnectChannel,
syncChannel,
createListing,
updateListing,
cancelReservation,
loadStats,
clearError
} = useChannelManager();
@@ -53,6 +61,48 @@ const ChannelManager = () => {
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 [selectedReservation, setSelectedReservation] = useState<Reservation | null>(null);
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 getStatusIcon = (status: string) => {
switch (status) {
@@ -216,63 +266,79 @@ const ChannelManager = () => {
</Button>
</div>
<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>
{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>
<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 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>
</div>
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
@@ -290,7 +356,7 @@ const ChannelManager = () => {
<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>
<Button onClick={() => setShowListingModal(true)}>
<Plus className="w-4 h-4 mr-2" />
Nueva Propiedad
</Button>
@@ -362,13 +428,13 @@ const ChannelManager = () => {
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" title="Ver detalles">
<Eye className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" title="Editar">
<Edit className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" title="Más opciones">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
@@ -431,13 +497,18 @@ const ChannelManager = () => {
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Button
variant="outline"
size="sm"
onClick={() => handleViewReservation(reservation)}
title="Ver detalles"
>
<Eye className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" title="Editar">
<Edit className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" title="Más opciones">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
@@ -450,46 +521,37 @@ const ChannelManager = () => {
</div>
);
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertTriangle className="w-12 h-12 text-yellow-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Error al cargar datos</h3>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={clearError}>Reintentar</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Channel Manager</h1>
<p className="text-muted-foreground">
Gestiona todas tus propiedades y canales de distribución desde un solo lugar
</p>
</div>
<div className="flex space-x-2">
<Button variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
Sincronizar Todo
</Button>
<Button>
<Settings className="w-4 h-4 mr-2" />
Configuración
</Button>
</div>
<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>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview">Resumen</TabsTrigger>
<TabsTrigger value="channels">Canales</TabsTrigger>
<TabsTrigger value="listings">Propiedades</TabsTrigger>
<TabsTrigger value="reservations">Reservas</TabsTrigger>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="overview">
<TrendingUp className="w-4 h-4 mr-2" />
Resumen
</TabsTrigger>
<TabsTrigger value="channels">
<Zap className="w-4 h-4 mr-2" />
Canales
</TabsTrigger>
<TabsTrigger value="listings">
<Hotel className="w-4 h-4 mr-2" />
Propiedades
</TabsTrigger>
<TabsTrigger value="reservations">
<Calendar className="w-4 h-4 mr-2" />
Reservas
</TabsTrigger>
<TabsTrigger value="analytics">
<BarChart3 className="w-4 h-4 mr-2" />
Analytics
</TabsTrigger>
</TabsList>
<TabsContent value="overview">
@@ -507,9 +569,35 @@ const ChannelManager = () => {
<TabsContent value="reservations">
<ReservationsTab />
</TabsContent>
<TabsContent value="analytics">
<AnalyticsTab stats={stats} onDateRangeChange={loadStats} />
</TabsContent>
</Tabs>
{/* 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)}
reservation={selectedReservation}
onCancel={cancelReservation}
/>
</div>
);
};
export default ChannelManager;
export default ChannelManager;