diff --git a/src/App.tsx b/src/App.tsx index 3e439d8..59c83bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,7 +20,7 @@ import OrderConfirmation from "./pages/OrderConfirmation"; import DashboardLayout from "./components/DashboardLayout"; import Dashboard from "./pages/dashboard/Dashboard"; import AdminDashboard from "./pages/dashboard/AdminDashboard"; -import AddListing from "./pages/dashboard/AddListing"; +import ChannelManager from "./pages/dashboard/ChannelManager"; import Wallet from "./pages/dashboard/Wallet"; import MyListings from "./pages/dashboard/MyListings"; import Messages from "./pages/dashboard/Messages"; @@ -145,10 +145,10 @@ const AppRouter = () => ( } /> - - + } /> diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index 1ad68cb..f3f1c5b 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -58,7 +58,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => { const menuItems = [ { icon: Home, label: 'Dashboard', path: '/dashboard' }, { icon: Settings, label: 'Admin Panel', path: '/dashboard/admin' }, - { icon: Plus, label: 'Add listing', path: '/dashboard/add-listing' }, + { icon: Plus, label: 'Channel Manager', path: '/dashboard/channel-manager' }, { icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' }, { icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' }, ]; diff --git a/src/hooks/useChannelManager.ts b/src/hooks/useChannelManager.ts new file mode 100644 index 0000000..0de1902 --- /dev/null +++ b/src/hooks/useChannelManager.ts @@ -0,0 +1,269 @@ +import { useState, useEffect, useCallback } from 'react'; +import { ChannelManagerService } from '@/services/channelManagerApi'; + +export interface Channel { + id: string; + name: string; + type: 'booking.com' | 'expedia' | 'airbnb' | 'vrbo' | 'agoda' | 'direct' | 'other'; + status: 'connected' | 'disconnected' | 'syncing' | 'error'; + lastSync: string; + properties: string[]; + revenue: number; + bookings: number; + commission: number; +} + +export interface Listing { + id: string; + name: string; + type: 'hotel' | 'restaurant' | 'vehicle' | 'flight' | 'activity'; + status: 'active' | 'inactive' | 'draft'; + channels: string[]; + basePrice: number; + availability: boolean; + images: string[]; + description: string; + location: { + address: string; + coordinates: { lat: number; lng: number }; + }; + createdAt: string; + updatedAt: string; +} + +export interface Reservation { + id: string; + listingId: string; + listingName: string; + listingType: 'hotel' | 'restaurant' | 'vehicle' | 'flight' | 'activity'; + guestName: string; + guestEmail: string; + guestPhone: string; + checkIn: string; + checkOut?: string; + guests: number; + totalAmount: number; + status: 'confirmed' | 'pending' | 'cancelled' | 'completed'; + paymentStatus: 'paid' | 'pending' | 'refunded'; + channel: string; + specialRequests?: string; + createdAt: string; +} + +export interface ChannelManagerStats { + totalRevenue: number; + totalBookings: number; + occupancyRate: number; + averageRate: number; + channelPerformance: { + channelName: string; + revenue: number; + bookings: number; + }[]; +} + +export const useChannelManager = () => { + const [channels, setChannels] = useState([]); + const [listings, setListings] = useState([]); + const [reservations, setReservations] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Load channels + const loadChannels = useCallback(async () => { + try { + setLoading(true); + const data = await ChannelManagerService.getChannels(); + setChannels(data || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Error loading channels'); + } finally { + setLoading(false); + } + }, []); + + // Load listings + const loadListings = useCallback(async (type?: string) => { + try { + setLoading(true); + const data = await ChannelManagerService.getListings(type); + setListings(data || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Error loading listings'); + } finally { + setLoading(false); + } + }, []); + + // Load reservations + const loadReservations = useCallback(async (filters?: any) => { + try { + setLoading(true); + const data = await ChannelManagerService.getReservations(filters); + setReservations(data || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Error loading reservations'); + } finally { + setLoading(false); + } + }, []); + + // Load analytics + const loadStats = useCallback(async (dateRange: { startDate: string; endDate: string }) => { + try { + setLoading(true); + const [revenueData, channelData] = await Promise.all([ + ChannelManagerService.getRevenueReports(dateRange), + ChannelManagerService.getChannelPerformance() + ]); + + setStats({ + totalRevenue: revenueData?.totalRevenue || 0, + totalBookings: revenueData?.totalBookings || 0, + occupancyRate: revenueData?.occupancyRate || 0, + averageRate: revenueData?.averageRate || 0, + channelPerformance: channelData || [] + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Error loading statistics'); + } finally { + setLoading(false); + } + }, []); + + // Connect new channel + const connectChannel = useCallback(async (channelData: any) => { + try { + setLoading(true); + await ChannelManagerService.connectChannel(channelData); + await loadChannels(); // Reload channels after connecting + return true; + } catch (err) { + setError(err instanceof Error ? err.message : 'Error connecting channel'); + return false; + } finally { + setLoading(false); + } + }, [loadChannels]); + + // Disconnect channel + const disconnectChannel = useCallback(async (channelId: string) => { + try { + setLoading(true); + await ChannelManagerService.disconnectChannel(channelId); + await loadChannels(); // Reload channels after disconnecting + return true; + } catch (err) { + setError(err instanceof Error ? err.message : 'Error disconnecting channel'); + return false; + } finally { + setLoading(false); + } + }, [loadChannels]); + + // Sync channel + const syncChannel = useCallback(async (channelId: string) => { + try { + setLoading(true); + await ChannelManagerService.syncChannel(channelId); + await loadChannels(); // Reload channels after syncing + return true; + } catch (err) { + setError(err instanceof Error ? err.message : 'Error syncing channel'); + return false; + } finally { + setLoading(false); + } + }, [loadChannels]); + + // Create listing + const createListing = useCallback(async (listingData: any) => { + try { + setLoading(true); + await ChannelManagerService.createListing(listingData); + await loadListings(); // Reload listings after creating + return true; + } catch (err) { + setError(err instanceof Error ? err.message : 'Error creating listing'); + return false; + } finally { + setLoading(false); + } + }, [loadListings]); + + // Update listing + const updateListing = useCallback(async (id: string, listingData: any) => { + try { + setLoading(true); + await ChannelManagerService.updateListing(id, listingData); + await loadListings(); // Reload listings after updating + return true; + } catch (err) { + setError(err instanceof Error ? err.message : 'Error updating listing'); + return false; + } finally { + setLoading(false); + } + }, [loadListings]); + + // Cancel reservation + const cancelReservation = useCallback(async (id: string, reason?: string) => { + try { + setLoading(true); + await ChannelManagerService.cancelReservation(id, reason); + await loadReservations(); // Reload reservations after cancelling + return true; + } catch (err) { + setError(err instanceof Error ? err.message : 'Error cancelling reservation'); + return false; + } finally { + setLoading(false); + } + }, [loadReservations]); + + // Clear error + const clearError = useCallback(() => { + setError(null); + }, []); + + // Load initial data + useEffect(() => { + loadChannels(); + loadListings(); + loadReservations(); + + const dateRange = { + startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + endDate: new Date().toISOString().split('T')[0] + }; + loadStats(dateRange); + }, [loadChannels, loadListings, loadReservations, loadStats]); + + return { + // Data + channels, + listings, + reservations, + stats, + + // States + loading, + error, + + // Actions + loadChannels, + loadListings, + loadReservations, + loadStats, + connectChannel, + disconnectChannel, + syncChannel, + createListing, + updateListing, + cancelReservation, + clearError, + }; +}; + +export default useChannelManager; \ No newline at end of file diff --git a/src/pages/dashboard/ChannelManager.tsx b/src/pages/dashboard/ChannelManager.tsx new file mode 100644 index 0000000..a3dcaa2 --- /dev/null +++ b/src/pages/dashboard/ChannelManager.tsx @@ -0,0 +1,515 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +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, + Filter, + MoreHorizontal, + CheckCircle, + AlertTriangle, + XCircle, + RefreshCw, + Eye, + Edit, + Trash2, + Hotel, + Utensils, + Car, + Plane, + MapPin, + Globe +} from 'lucide-react'; +import { useChannelManager } from '@/hooks/useChannelManager'; +import { format } from 'date-fns'; +import { es } from 'date-fns/locale'; + +const ChannelManager = () => { + const { + channels, + listings, + reservations, + stats, + loading, + error, + connectChannel, + disconnectChannel, + syncChannel, + createListing, + clearError + } = useChannelManager(); + + const [activeTab, setActiveTab] = useState('overview'); + const [searchTerm, setSearchTerm] = useState(''); + const [filterType, setFilterType] = useState('all'); + const [showConnectModal, setShowConnectModal] = useState(false); + + const getStatusIcon = (status: string) => { + switch (status) { + case 'connected': + return ; + case 'syncing': + return ; + case 'error': + return ; + default: + return ; + } + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case 'hotel': + return ; + case 'restaurant': + return ; + case 'vehicle': + return ; + case 'flight': + return ; + case 'activity': + return ; + default: + return ; + } + }; + + const OverviewTab = () => ( +
+ {/* Key Metrics */} +
+ + +
+
+

Ingresos Totales

+

${stats?.totalRevenue.toLocaleString() || '0'}

+
+ +
+
+
+ + + +
+
+

Reservas Totales

+

{stats?.totalBookings || 0}

+
+ +
+
+
+ + + +
+
+

Tasa de Ocupación

+

{stats?.occupancyRate || 0}%

+
+ +
+
+
+ + + +
+
+

Canales Conectados

+

{channels.filter(c => c.status === 'connected').length}

+
+ +
+
+
+
+ + {/* Channel Performance */} + + + Rendimiento por Canal + Ingresos y reservas por cada canal de distribución + + +
+ {stats?.channelPerformance.map((channel, index) => ( +
+
+
+ +
+
+

{channel.channelName}

+

{channel.bookings} reservas

+
+
+
+

${channel.revenue.toLocaleString()}

+

Ingresos

+
+
+ ))} +
+
+
+ + {/* Recent Reservations */} + + + Reservas Recientes + Últimas reservas recibidas en todos los canales + + +
+ {reservations.slice(0, 5).map((reservation) => ( +
+
+ {getTypeIcon(reservation.listingType)} +
+

{reservation.guestName}

+

{reservation.listingName}

+
+
+
+ + {reservation.status} + +

+ ${reservation.totalAmount.toLocaleString()} +

+
+
+ ))} +
+
+
+
+ ); + + const ChannelsTab = () => ( +
+
+
+

Gestión de Canales

+

Conecta y administra tus canales de distribución

+
+ +
+ +
+ {channels.map((channel) => ( + + +
+
+
+ +
+
+
+

{channel.name}

+ {getStatusIcon(channel.status)} + {channel.type} +
+

+ Última sincronización: {format(new Date(channel.lastSync), 'dd/MM/yyyy HH:mm', { locale: es })} +

+

+ {channel.properties.length} propiedades conectadas +

+
+
+ +
+
+

${channel.revenue.toLocaleString()}

+

{channel.bookings} reservas

+

Comisión: {channel.commission}%

+
+ +
+ + + +
+
+
+
+
+ ))} +
+
+ ); + + 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 ( +
+
+
+

Gestión de Propiedades

+

Administra hoteles, restaurantes, vehículos y más

+
+ +
+ +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ +
+ +
+ {filteredListings.map((listing) => ( + + +
+
+
+ {listing.images[0] ? ( + {listing.name} + ) : ( + getTypeIcon(listing.type) + )} +
+
+
+

{listing.name}

+ + {listing.status} + + {listing.type} +
+

{listing.location.address}

+

+ Conectado a {listing.channels.length} canales +

+
+
+ +
+
+

${listing.basePrice}

+

+ {listing.availability ? 'Disponible' : 'No disponible'} +

+
+ +
+ + + +
+
+
+
+
+ ))} +
+
+ ); + }; + + const ReservationsTab = () => ( +
+
+
+

Gestión de Reservas

+

Administra todas las reservas desde un solo lugar

+
+
+ +
+ {reservations.map((reservation) => ( + + +
+
+ {getTypeIcon(reservation.listingType)} +
+
+

{reservation.guestName}

+ + {reservation.status} + +
+

{reservation.listingName}

+

+ {format(new Date(reservation.checkIn), 'dd/MM/yyyy', { locale: es })} + {reservation.checkOut && ` - ${format(new Date(reservation.checkOut), 'dd/MM/yyyy', { locale: es })}`} +

+

+ {reservation.guests} huéspedes • {reservation.channel} +

+
+
+ +
+
+

${reservation.totalAmount.toLocaleString()}

+

+ Pago: + {reservation.paymentStatus} + +

+
+ +
+ + + +
+
+
+
+
+ ))} +
+
+ ); + + if (error) { + return ( +
+
+ +

Error al cargar datos

+

{error}

+ +
+
+ ); + } + + return ( +
+
+
+

Channel Manager

+

+ Gestiona todas tus propiedades y canales de distribución desde un solo lugar +

+
+
+ + +
+
+ + + + Resumen + Canales + Propiedades + Reservas + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default ChannelManager; \ No newline at end of file diff --git a/src/services/channelManagerApi.ts b/src/services/channelManagerApi.ts new file mode 100644 index 0000000..76d1a2e --- /dev/null +++ b/src/services/channelManagerApi.ts @@ -0,0 +1,210 @@ +import { API_CONFIG } from '@/config/api'; + +// Extend API configuration for Channel Manager +export const CHANNEL_MANAGER_API = { + ...API_CONFIG, + ENDPOINTS: { + ...API_CONFIG.ENDPOINTS, + // Listings Management + LISTINGS: '/listings', + LISTING_DETAIL: '/listings/:id', + + // Channel Management + CHANNELS: '/channels', + CHANNEL_CONNECT: '/channels/connect', + CHANNEL_DISCONNECT: '/channels/:id/disconnect', + CHANNEL_SYNC: '/channels/:id/sync', + + // Reservations (multi-type) + RESERVATIONS: '/reservations', + RESERVATION_DETAIL: '/reservations/:id', + CREATE_RESERVATION: '/reservations', + UPDATE_RESERVATION: '/reservations/:id', + CANCEL_RESERVATION: '/reservations/:id/cancel', + + // Hotels + HOTELS: '/hotels', + HOTEL_ROOMS: '/hotels/:id/rooms', + HOTEL_AVAILABILITY: '/hotels/:id/availability', + HOTEL_RATES: '/hotels/:id/rates', + + // Restaurants + RESTAURANTS: '/restaurants', + RESTAURANT_TABLES: '/restaurants/:id/tables', + RESTAURANT_AVAILABILITY: '/restaurants/:id/availability', + RESTAURANT_MENU: '/restaurants/:id/menu', + + // Vehicles + VEHICLES: '/vehicles', + VEHICLE_AVAILABILITY: '/vehicles/:id/availability', + VEHICLE_RATES: '/vehicles/:id/rates', + + // Flights + FLIGHTS: '/flights', + FLIGHT_SEARCH: '/flights/search', + FLIGHT_BOOKING: '/flights/book', + + // Itineraries + ITINERARIES: '/itineraries', + ITINERARY_DETAIL: '/itineraries/:id', + + // Analytics & Reports + ANALYTICS: '/analytics', + REVENUE_REPORTS: '/analytics/revenue', + OCCUPANCY_REPORTS: '/analytics/occupancy', + CHANNEL_PERFORMANCE: '/analytics/channels', + } +}; + +// Channel Manager Service +export class ChannelManagerService { + private static async request(endpoint: string, options: RequestInit = {}) { + const token = localStorage.getItem('auth_token'); + + // Create AbortController for timeout functionality + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), CHANNEL_MANAGER_API.TIMEOUT); + + const response = await fetch(`${CHANNEL_MANAGER_API.BASE_URL}${endpoint}`, { + ...options, + headers: { + ...CHANNEL_MANAGER_API.DEFAULT_HEADERS, + ...(token && { Authorization: `Bearer ${token}` }), + ...options.headers, + }, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } + + // Handle 204 No Content responses + if (response.status === 204) { + return null; + } + + return await response.json(); + } + + // Listings Management + static getListings = (type?: string) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.LISTINGS}${type ? `?type=${type}` : ''}`); + + static getListingDetail = (id: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.LISTING_DETAIL.replace(':id', id)); + + static createListing = (data: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.LISTINGS, { + method: 'POST', + body: JSON.stringify(data), + }); + + static updateListing = (id: string, data: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.LISTING_DETAIL.replace(':id', id), { + method: 'PUT', + body: JSON.stringify(data), + }); + + static deleteListing = (id: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.LISTING_DETAIL.replace(':id', id), { + method: 'DELETE', + }); + + // Channel Management + static getChannels = () => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CHANNELS); + + static connectChannel = (channelData: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CHANNEL_CONNECT, { + method: 'POST', + body: JSON.stringify(channelData), + }); + + static disconnectChannel = (channelId: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CHANNEL_DISCONNECT.replace(':id', channelId), { + method: 'DELETE', + }); + + static syncChannel = (channelId: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CHANNEL_SYNC.replace(':id', channelId), { + method: 'POST', + }); + + // Reservations Management + static getReservations = (filters?: any) => { + const queryParams = filters + ? '?' + new URLSearchParams(filters).toString() + : ''; + return this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.RESERVATIONS}${queryParams}`); + }; + + static getReservationDetail = (id: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.RESERVATION_DETAIL.replace(':id', id)); + + static createReservation = (data: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CREATE_RESERVATION, { + method: 'POST', + body: JSON.stringify(data), + }); + + static updateReservation = (id: string, data: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.UPDATE_RESERVATION.replace(':id', id), { + method: 'PUT', + body: JSON.stringify(data), + }); + + static cancelReservation = (id: string, reason?: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CANCEL_RESERVATION.replace(':id', id), { + method: 'POST', + body: JSON.stringify({ reason }), + }); + + // Hotel specific methods + static getHotelRooms = (hotelId: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.HOTEL_ROOMS.replace(':id', hotelId)); + + static getHotelAvailability = (hotelId: string, dateRange: { startDate: string; endDate: string }) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.HOTEL_AVAILABILITY.replace(':id', hotelId)}?${new URLSearchParams(dateRange)}`); + + static updateHotelRates = (hotelId: string, rates: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.HOTEL_RATES.replace(':id', hotelId), { + method: 'PUT', + body: JSON.stringify(rates), + }); + + // Restaurant specific methods + static getRestaurantTables = (restaurantId: string) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.RESTAURANT_TABLES.replace(':id', restaurantId)); + + static getRestaurantAvailability = (restaurantId: string, date: string) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.RESTAURANT_AVAILABILITY.replace(':id', restaurantId)}?date=${date}`); + + // Vehicle specific methods + static getVehicleAvailability = (vehicleId: string, dateRange: { startDate: string; endDate: string }) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.VEHICLE_AVAILABILITY.replace(':id', vehicleId)}?${new URLSearchParams(dateRange)}`); + + // Flight methods + static searchFlights = (searchParams: any) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.FLIGHT_SEARCH}?${new URLSearchParams(searchParams)}`); + + static bookFlight = (bookingData: any) => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.FLIGHT_BOOKING, { + method: 'POST', + body: JSON.stringify(bookingData), + }); + + // Analytics methods + static getAnalytics = (type: string, dateRange: { startDate: string; endDate: string }) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.ANALYTICS}/${type}?${new URLSearchParams(dateRange)}`); + + static getRevenueReports = (dateRange: { startDate: string; endDate: string }) => + this.request(`${CHANNEL_MANAGER_API.ENDPOINTS.REVENUE_REPORTS}?${new URLSearchParams(dateRange)}`); + + static getChannelPerformance = () => + this.request(CHANNEL_MANAGER_API.ENDPOINTS.CHANNEL_PERFORMANCE); +} + +export default ChannelManagerService; \ No newline at end of file