diff --git a/src/App.tsx b/src/App.tsx index 2684242..25795f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,9 @@ import Security from "./pages/dashboard/Security"; import VehicleManagement from "./pages/dashboard/VehicleManagement"; import Sustainability from "./pages/dashboard/Sustainability"; import Establishments from "./pages/dashboard/Establishments"; +import Analytics from "./pages/dashboard/Analytics"; +import SearchPlaces from "./pages/dashboard/SearchPlaces"; +import SearchEstablishments from "./pages/dashboard/SearchEstablishments"; // Commerce pages (for retail stores) import CommerceStore from "./pages/dashboard/commerce/Store"; import CommercePOS from "./pages/dashboard/commerce/POSTerminal"; @@ -302,6 +305,30 @@ const AppRouter = () => ( } /> + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + {/* Commerce Routes */} diff --git a/src/components/dashboard/ReviewReplyDialog.tsx b/src/components/dashboard/ReviewReplyDialog.tsx index b523ebd..f67cf37 100644 --- a/src/components/dashboard/ReviewReplyDialog.tsx +++ b/src/components/dashboard/ReviewReplyDialog.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Camera, Send, X, Star } from 'lucide-react'; -import { ReviewPhotoUpload } from './ReviewPhotoUpload'; +import { ImageUploader } from '@/components/shared/ImageUploader'; interface Review { id: string; @@ -151,10 +151,21 @@ export function ReviewReplyDialog({ review, onReply, onClose }: ReviewReplyDialo {/* Photo Upload Dialog */} {showPhotoUpload && ( - setShowPhotoUpload(false)} - /> + setShowPhotoUpload(false)}> + + + Subir Imágenes + + { + setImages([...images, ...urls]); + setShowPhotoUpload(false); + }} + maxFiles={5} + category="review-reply" + /> + + )} diff --git a/src/pages/dashboard/Analytics.tsx b/src/pages/dashboard/Analytics.tsx new file mode 100644 index 0000000..4192466 --- /dev/null +++ b/src/pages/dashboard/Analytics.tsx @@ -0,0 +1,249 @@ +import { useState } from 'react'; +import DashboardLayout from '@/components/DashboardLayout'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useAnalytics } from '@/hooks/useAnalytics'; +import { TrendingUp, Users, DollarSign, MapPin, Loader2 } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; + +const Analytics = () => { + const [period, setPeriod] = useState('month'); + const { analytics, loading, error, refetch } = useAnalytics(period); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('es-DO', { + style: 'currency', + currency: 'DOP', + }).format(amount); + }; + + const StatCard = ({ + title, + value, + icon: Icon, + description + }: { + title: string; + value: string | number; + icon: any; + description?: string; + }) => ( + + + {title} + + + +
{value}
+ {description && ( +

{description}

+ )} +
+
+ ); + + return ( + +
+ {/* Header */} +
+
+

Analytics

+

+ Métricas y estadísticas de tu plataforma +

+
+ +
+ + {/* Loading State */} + {loading && ( +
+
+ {[1, 2, 3, 4].map((i) => ( + + + + + + + + + ))} +
+
+ + + + + + + + + + + + + + + + +
+
+ )} + + {/* Error State */} + {error && ( + + +
+

{error}

+ +
+
+
+ )} + + {/* Analytics Data */} + {analytics && !loading && ( + <> + {/* Stats Grid */} +
+ + + + +
+ + {/* Charts Grid */} +
+ {/* Top Destinations */} + + + Destinos Más Visitados + + Top destinos del período seleccionado + + + +
+ {analytics.topDestinations.map((destination, index) => ( +
+
+
+ {index + 1} +
+ {destination.name} +
+ + {destination.visits.toLocaleString()} visitas + +
+ ))} +
+
+
+ + {/* Revenue by Category */} + + + Ingresos por Categoría + + Distribución de ingresos por tipo de servicio + + + +
+ {analytics.revenueByCategory.map((item) => ( +
+
+ {item.category} + + {formatCurrency(item.revenue)} + +
+
+
+
+
+ ))} +
+ + +
+ + {/* User Growth Chart */} + + + Crecimiento de Usuarios + + Evolución de usuarios registrados - Tasa de conversión: {analytics.conversionRate.toFixed(2)}% + + + +
+ {analytics.userGrowth.map((point) => ( +
+
p.users))) * 100}%`, + minHeight: '20px', + }} + /> + + {new Date(point.date).toLocaleDateString('es', { day: 'numeric', month: 'short' })} + +
+ ))} +
+ + + + )} +
+ + ); +}; + +export default Analytics; diff --git a/src/pages/dashboard/SearchEstablishments.tsx b/src/pages/dashboard/SearchEstablishments.tsx new file mode 100644 index 0000000..97dd2c7 --- /dev/null +++ b/src/pages/dashboard/SearchEstablishments.tsx @@ -0,0 +1,212 @@ +import { useState } from 'react'; +import DashboardLayout from '@/components/DashboardLayout'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { SearchBar } from '@/components/shared/SearchBar'; +import { useSearch } from '@/hooks/useSearch'; +import { Building2, Star, Loader2, MapPin, CheckCircle } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; + +const SearchEstablishments = () => { + const [type, setType] = useState('all'); + const [minRating, setMinRating] = useState(0); + const [verifiedOnly, setVerifiedOnly] = useState(false); + const { searching, results, searchEstablishments } = useSearch(); + + const handleSearch = (query: string) => { + if (query.trim()) { + searchEstablishments(query, { + type: type !== 'all' ? type : undefined, + minRating: minRating > 0 ? minRating : undefined, + verified: verifiedOnly, + }); + } + }; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + + )); + }; + + const getTypeLabel = (type: string) => { + const labels: Record = { + restaurant: 'Restaurante', + hotel: 'Hotel', + shop: 'Tienda', + attraction: 'Atracción', + }; + return labels[type] || type; + }; + + return ( + +
+ {/* Header */} +
+

Buscar Establecimientos

+

+ Encuentra hoteles, restaurantes, tiendas y más +

+
+ + {/* Search and Filters */} + + + Búsqueda + + Usa filtros para refinar tu búsqueda + + + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+
+ + {/* Results */} + {searching && ( +
+ +
+ )} + + {!searching && results.length === 0 && ( + + +
+ +

No hay resultados para mostrar

+

Intenta realizar una búsqueda

+
+
+
+ )} + + {!searching && results.length > 0 && ( +
+ {results.map((establishment: any) => ( + + +
+
+
+ {establishment.name} + {establishment.verified && ( + + )} +
+
+
+ {renderStars(Math.floor(establishment.rating))} +
+ {establishment.rating.toFixed(1)} +
+
+ + {getTypeLabel(establishment.type)} + +
+
+ +

+ {establishment.description} +

+ + {establishment.location && ( +
+ + {establishment.location.address} +
+ )} + + {establishment.owner && ( +
+ Propietario: + {establishment.owner.name} +
+ )} + +
+ + {establishment.status === 'active' ? 'Activo' : + establishment.status === 'pending' ? 'Pendiente' : + 'Suspendido'} + + + {new Date(establishment.createdAt).toLocaleDateString('es')} + +
+
+
+ ))} +
+ )} +
+
+ ); +}; + +export default SearchEstablishments; diff --git a/src/pages/dashboard/SearchPlaces.tsx b/src/pages/dashboard/SearchPlaces.tsx new file mode 100644 index 0000000..2436249 --- /dev/null +++ b/src/pages/dashboard/SearchPlaces.tsx @@ -0,0 +1,165 @@ +import { useState } from 'react'; +import DashboardLayout from '@/components/DashboardLayout'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { SearchBar } from '@/components/shared/SearchBar'; +import { useSearch } from '@/hooks/useSearch'; +import { MapPin, Star, Loader2 } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; + +const SearchPlaces = () => { + const [category, setCategory] = useState('all'); + const [minRating, setMinRating] = useState(0); + const { searching, results, searchPlaces } = useSearch(); + + const handleSearch = (query: string) => { + if (query.trim()) { + searchPlaces(query, { + category: category !== 'all' ? category : undefined, + minRating: minRating > 0 ? minRating : undefined, + }); + } + }; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + + )); + }; + + return ( + +
+ {/* Header */} +
+

Buscar Lugares

+

+ Encuentra lugares de interés turístico +

+
+ + {/* Search and Filters */} + + + Búsqueda + + Usa filtros para refinar tu búsqueda + + + + + +
+
+ + +
+ +
+ + +
+
+
+
+ + {/* Results */} + {searching && ( +
+ +
+ )} + + {!searching && results.length === 0 && ( + + +
+ +

No hay resultados para mostrar

+

Intenta realizar una búsqueda

+
+
+
+ )} + + {!searching && results.length > 0 && ( +
+ {results.map((place: any) => ( + + +
+
+ {place.name} +
+
+ {renderStars(Math.floor(place.rating))} +
+ {place.rating.toFixed(1)} +
+
+ {place.category && ( + + {place.category} + + )} +
+
+ +

+ {place.description} +

+ {place.location && ( +
+ + {place.location.address} +
+ )} +
+ + {place.status === 'active' ? 'Activo' : 'Inactivo'} + + + {new Date(place.createdAt).toLocaleDateString('es')} + +
+
+
+ ))} +
+ )} +
+
+ ); +}; + +export default SearchPlaces;