Add Commerce module and enhance Admin Panel sections
This commit is contained in:
@@ -38,6 +38,7 @@ import Personalization from "./pages/dashboard/Personalization";
|
|||||||
import Security from "./pages/dashboard/Security";
|
import Security from "./pages/dashboard/Security";
|
||||||
import VehicleManagement from "./pages/dashboard/VehicleManagement";
|
import VehicleManagement from "./pages/dashboard/VehicleManagement";
|
||||||
import Sustainability from "./pages/dashboard/Sustainability";
|
import Sustainability from "./pages/dashboard/Sustainability";
|
||||||
|
import Establishments from "./pages/dashboard/Establishments";
|
||||||
// Hotel pages
|
// Hotel pages
|
||||||
import HotelRooms from "./pages/dashboard/hotel/Rooms";
|
import HotelRooms from "./pages/dashboard/hotel/Rooms";
|
||||||
import HotelCheckIn from "./pages/dashboard/hotel/CheckIn";
|
import HotelCheckIn from "./pages/dashboard/hotel/CheckIn";
|
||||||
@@ -284,6 +285,14 @@ const AppRouter = () => (
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
|
<Route path="/dashboard/establishments" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardLayout>
|
||||||
|
<Establishments />
|
||||||
|
</DashboardLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
<Route path="/dashboard/hotel-management" element={
|
<Route path="/dashboard/hotel-management" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
Radio,
|
Radio,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Leaf
|
Leaf,
|
||||||
|
Store
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -159,6 +160,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
{ icon: Shield, label: 'Security', path: '/dashboard/security' },
|
{ icon: Shield, label: 'Security', path: '/dashboard/security' },
|
||||||
{ icon: Car, label: 'Vehicle Management', path: '/dashboard/vehicle-management' },
|
{ icon: Car, label: 'Vehicle Management', path: '/dashboard/vehicle-management' },
|
||||||
{ icon: Leaf, label: 'Sustainability', path: '/dashboard/sustainability' },
|
{ icon: Leaf, label: 'Sustainability', path: '/dashboard/sustainability' },
|
||||||
|
{ icon: Store, label: 'Comercios', path: '/dashboard/establishments' },
|
||||||
{ icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' },
|
{ icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' },
|
||||||
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -539,9 +539,93 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
|||||||
<TabsContent value="guides" className="space-y-4">
|
<TabsContent value="guides" className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-lg font-semibold">Gestión de Guías Turísticos</h3>
|
<h3 className="text-lg font-semibold">Gestión de Guías Turísticos</h3>
|
||||||
<Badge variant="secondary" className="text-sm">
|
<Dialog>
|
||||||
{Array.isArray(guides) ? guides.length : 0} Guías Registrados
|
<DialogTrigger asChild>
|
||||||
</Badge>
|
<Button className="flex items-center gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Nuevo Guía
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Crear Nuevo Guía</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Número de Licencia</Label>
|
||||||
|
<Input
|
||||||
|
value={newGuide.licenseNumber}
|
||||||
|
onChange={(e) => setNewGuide({ ...newGuide, licenseNumber: e.target.value })}
|
||||||
|
placeholder="Ej: GUIDE-2024-001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Especialidades (separadas por coma)</Label>
|
||||||
|
<Input
|
||||||
|
value={newGuide.specialties.join(', ')}
|
||||||
|
onChange={(e) => setNewGuide({
|
||||||
|
...newGuide,
|
||||||
|
specialties: e.target.value.split(',').map(s => s.trim())
|
||||||
|
})}
|
||||||
|
placeholder="Historia, Naturaleza, Aventura"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Idiomas (separados por coma)</Label>
|
||||||
|
<Input
|
||||||
|
value={newGuide.languages.join(', ')}
|
||||||
|
onChange={(e) => setNewGuide({
|
||||||
|
...newGuide,
|
||||||
|
languages: e.target.value.split(',').map(l => l.trim())
|
||||||
|
})}
|
||||||
|
placeholder="Español, Inglés, Francés"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Tarifa por Hora ($)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newGuide.hourlyRate}
|
||||||
|
onChange={(e) => setNewGuide({ ...newGuide, hourlyRate: parseFloat(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Tarifa Diaria ($)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newGuide.dailyRate}
|
||||||
|
onChange={(e) => setNewGuide({ ...newGuide, dailyRate: parseFloat(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Biografía</Label>
|
||||||
|
<Textarea
|
||||||
|
value={newGuide.bio}
|
||||||
|
onChange={(e) => setNewGuide({ ...newGuide, bio: e.target.value })}
|
||||||
|
placeholder="Experiencia y descripción del guía..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/tourism/guides', {
|
||||||
|
...newGuide,
|
||||||
|
userId: 1
|
||||||
|
});
|
||||||
|
setNewGuide({ licenseNumber: '', specialties: [''], languages: [''], hourlyRate: 0, dailyRate: 0, bio: '', certifications: {} });
|
||||||
|
loadData();
|
||||||
|
toast({ title: 'Guía creado', description: 'El guía se creó correctamente.' });
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({ title: 'Error', description: error?.message || 'No se pudo crear el guía.' });
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Crear Guía
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
@@ -565,6 +649,32 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
|||||||
<Badge variant={guide.isVerified ? "default" : "secondary"}>
|
<Badge variant={guide.isVerified ? "default" : "secondary"}>
|
||||||
{guide.isVerified ? 'Verificado' : 'Pendiente'}
|
{guide.isVerified ? 'Verificado' : 'Pendiente'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => console.log('Edit guide:', guide.id)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/tourism/guides/${guide.id}`);
|
||||||
|
loadData();
|
||||||
|
toast({ title: 'Guía eliminado', description: 'Se eliminó correctamente.' });
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: 'Error', description: 'No se pudo eliminar el guía.' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -589,9 +699,88 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
|||||||
<TabsContent value="taxis" className="space-y-4">
|
<TabsContent value="taxis" className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-lg font-semibold">Gestión de Taxis</h3>
|
<h3 className="text-lg font-semibold">Gestión de Taxis</h3>
|
||||||
<Badge variant="secondary" className="text-sm">
|
<Dialog>
|
||||||
{Array.isArray(taxis) ? taxis.length : 0} Taxis Disponibles
|
<DialogTrigger asChild>
|
||||||
</Badge>
|
<Button className="flex items-center gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Nuevo Taxi
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Registrar Nuevo Taxi</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Número de Licencia</Label>
|
||||||
|
<Input
|
||||||
|
value={newTaxi.licenseNumber}
|
||||||
|
onChange={(e) => setNewTaxi({ ...newTaxi, licenseNumber: e.target.value })}
|
||||||
|
placeholder="Ej: TAXI-2024-001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Placa del Vehículo</Label>
|
||||||
|
<Input
|
||||||
|
value={newTaxi.vehiclePlate}
|
||||||
|
onChange={(e) => setNewTaxi({ ...newTaxi, vehiclePlate: e.target.value })}
|
||||||
|
placeholder="ABC-123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Modelo</Label>
|
||||||
|
<Input
|
||||||
|
value={newTaxi.vehicleModel}
|
||||||
|
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleModel: e.target.value })}
|
||||||
|
placeholder="Toyota Corolla"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Año</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newTaxi.vehicleYear}
|
||||||
|
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleYear: parseInt(e.target.value) || new Date().getFullYear() })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Color</Label>
|
||||||
|
<Input
|
||||||
|
value={newTaxi.vehicleColor}
|
||||||
|
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleColor: e.target.value })}
|
||||||
|
placeholder="Blanco"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Capacidad</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newTaxi.vehicleCapacity}
|
||||||
|
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleCapacity: parseInt(e.target.value) || 4 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/tourism/taxis', {
|
||||||
|
...newTaxi,
|
||||||
|
userId: 1 // You should get this from auth
|
||||||
|
});
|
||||||
|
setNewTaxi({ licenseNumber: '', vehiclePlate: '', vehicleModel: '', vehicleYear: new Date().getFullYear(), vehicleColor: '', vehicleCapacity: 4 });
|
||||||
|
loadData();
|
||||||
|
toast({ title: 'Taxi registrado', description: 'El taxi se registró correctamente.' });
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({ title: 'Error', description: error?.message || 'No se pudo registrar el taxi.' });
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Registrar Taxi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
@@ -615,6 +804,32 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
|||||||
<Badge variant={taxi.isAvailable ? "default" : "secondary"}>
|
<Badge variant={taxi.isAvailable ? "default" : "secondary"}>
|
||||||
{taxi.isAvailable ? 'Disponible' : 'Ocupado'}
|
{taxi.isAvailable ? 'Disponible' : 'Ocupado'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => console.log('Edit taxi:', taxi.id)}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/tourism/taxis/${taxi.id}`);
|
||||||
|
loadData();
|
||||||
|
toast({ title: 'Taxi eliminado', description: 'Se eliminó correctamente.' });
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: 'Error', description: 'No se pudo eliminar el taxi.' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
85
src/components/establishments/BusinessAnalytics.tsx
Normal file
85
src/components/establishments/BusinessAnalytics.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { TrendingUp, Users, DollarSign, Eye } from 'lucide-react';
|
||||||
|
|
||||||
|
const BusinessAnalytics = () => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Comercios</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-blue-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">769</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">+12% este mes</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Visitas Totales</CardTitle>
|
||||||
|
<Eye className="h-4 w-4 text-purple-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">45.2K</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Este mes</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Usuarios Activos</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-green-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">2,345</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Propietarios</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Ingresos</CardTitle>
|
||||||
|
<DollarSign className="h-4 w-4 text-orange-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">$124K</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Este mes</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Comercios Más Populares</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[
|
||||||
|
{ name: 'Restaurante El Faro', visits: '2.4K', category: 'Restaurante' },
|
||||||
|
{ name: 'Hotel Paradise', visits: '1.8K', category: 'Hotel' },
|
||||||
|
{ name: 'Boutique Fashion', visits: '1.5K', category: 'Tienda' },
|
||||||
|
{ name: 'Spa Wellness', visits: '1.2K', category: 'Servicios' },
|
||||||
|
{ name: 'Club Nocturno', visits: '980', category: 'Entretenimiento' }
|
||||||
|
].map((item, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between border-b pb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{item.name}</p>
|
||||||
|
<p className="text-sm text-gray-600">{item.category}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-semibold">{item.visits}</p>
|
||||||
|
<p className="text-xs text-gray-600">visitas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusinessAnalytics;
|
||||||
47
src/components/establishments/BusinessCategories.tsx
Normal file
47
src/components/establishments/BusinessCategories.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { UtensilsCrossed, Hotel, ShoppingBag, Music, Wrench, Building } from 'lucide-react';
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 1, name: 'Restaurantes', icon: UtensilsCrossed, count: 145, color: 'bg-orange-100 text-orange-600' },
|
||||||
|
{ id: 2, name: 'Hoteles', icon: Hotel, count: 67, color: 'bg-blue-100 text-blue-600' },
|
||||||
|
{ id: 3, name: 'Tiendas', icon: ShoppingBag, count: 234, color: 'bg-purple-100 text-purple-600' },
|
||||||
|
{ id: 4, name: 'Entretenimiento', icon: Music, count: 89, color: 'bg-pink-100 text-pink-600' },
|
||||||
|
{ id: 5, name: 'Servicios', icon: Wrench, count: 156, color: 'bg-green-100 text-green-600' },
|
||||||
|
{ id: 6, name: 'Otros', icon: Building, count: 78, color: 'bg-gray-100 text-gray-600' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const BusinessCategories = () => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{categories.map((category) => {
|
||||||
|
const Icon = category.icon;
|
||||||
|
return (
|
||||||
|
<Card key={category.id} className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-12 h-12 rounded-lg ${category.color} flex items-center justify-center`}>
|
||||||
|
<Icon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">{category.count}</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{category.count} establecimientos registrados en esta categoría
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusinessCategories;
|
||||||
306
src/components/establishments/BusinessList.tsx
Normal file
306
src/components/establishments/BusinessList.tsx
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Store, Plus, Edit, Trash2, MapPin, Phone, Globe, Clock, Star, Search } from 'lucide-react';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { apiClient } from '@/services/adminApi';
|
||||||
|
|
||||||
|
const BusinessList = () => {
|
||||||
|
const [businesses, setBusinesses] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [newBusiness, setNewBusiness] = useState({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
category: '',
|
||||||
|
address: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
website: '',
|
||||||
|
coordinates: { x: 0, y: 0 },
|
||||||
|
openingHours: {
|
||||||
|
monday: '9:00-18:00',
|
||||||
|
tuesday: '9:00-18:00',
|
||||||
|
wednesday: '9:00-18:00',
|
||||||
|
thursday: '9:00-18:00',
|
||||||
|
friday: '9:00-18:00',
|
||||||
|
saturday: '10:00-14:00',
|
||||||
|
sunday: 'Cerrado'
|
||||||
|
},
|
||||||
|
images: ['']
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadBusinesses = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await apiClient.get('/establishments?page=1&limit=50');
|
||||||
|
setBusinesses(Array.isArray(data) ? data : (data as any)?.establishments || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading businesses:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadBusinesses();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/establishments', {
|
||||||
|
...newBusiness,
|
||||||
|
coordinates: `(${newBusiness.coordinates.x},${newBusiness.coordinates.y})`,
|
||||||
|
images: newBusiness.images.filter(img => img.trim() !== '')
|
||||||
|
});
|
||||||
|
setNewBusiness({
|
||||||
|
name: '', description: '', category: '', address: '', phone: '', email: '', website: '',
|
||||||
|
coordinates: { x: 0, y: 0 }, images: [''],
|
||||||
|
openingHours: {
|
||||||
|
monday: '9:00-18:00', tuesday: '9:00-18:00', wednesday: '9:00-18:00',
|
||||||
|
thursday: '9:00-18:00', friday: '9:00-18:00', saturday: '10:00-14:00', sunday: 'Cerrado'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadBusinesses();
|
||||||
|
toast({ title: 'Comercio creado', description: 'El establecimiento se creó correctamente.' });
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({ title: 'Error', description: error?.message || 'No se pudo crear el comercio.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/establishments/${id}`);
|
||||||
|
loadBusinesses();
|
||||||
|
toast({ title: 'Comercio eliminado', description: 'Se eliminó correctamente.' });
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: 'Error', description: 'No se pudo eliminar el comercio.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredBusinesses = businesses.filter(b =>
|
||||||
|
b.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
b.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar comercios..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Nuevo Comercio
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Registrar Nuevo Comercio</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Nombre del Comercio</Label>
|
||||||
|
<Input
|
||||||
|
value={newBusiness.name}
|
||||||
|
onChange={(e) => setNewBusiness({ ...newBusiness, name: e.target.value })}
|
||||||
|
placeholder="Restaurante El Caribe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Categoría</Label>
|
||||||
|
<Select value={newBusiness.category} onValueChange={(v) => setNewBusiness({ ...newBusiness, category: v })}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Selecciona categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="restaurant">Restaurante</SelectItem>
|
||||||
|
<SelectItem value="hotel">Hotel</SelectItem>
|
||||||
|
<SelectItem value="store">Tienda</SelectItem>
|
||||||
|
<SelectItem value="entertainment">Entretenimiento</SelectItem>
|
||||||
|
<SelectItem value="services">Servicios</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Descripción</Label>
|
||||||
|
<Textarea
|
||||||
|
value={newBusiness.description}
|
||||||
|
onChange={(e) => setNewBusiness({ ...newBusiness, description: e.target.value })}
|
||||||
|
placeholder="Describe el comercio..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Dirección</Label>
|
||||||
|
<Input
|
||||||
|
value={newBusiness.address}
|
||||||
|
onChange={(e) => setNewBusiness({ ...newBusiness, address: e.target.value })}
|
||||||
|
placeholder="Calle Principal #123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Teléfono</Label>
|
||||||
|
<Input
|
||||||
|
value={newBusiness.phone}
|
||||||
|
onChange={(e) => setNewBusiness({ ...newBusiness, phone: e.target.value })}
|
||||||
|
placeholder="+1 809-555-0123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={newBusiness.email}
|
||||||
|
onChange={(e) => setNewBusiness({ ...newBusiness, email: e.target.value })}
|
||||||
|
placeholder="contacto@negocio.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Sitio Web</Label>
|
||||||
|
<Input
|
||||||
|
value={newBusiness.website}
|
||||||
|
onChange={(e) => setNewBusiness({ ...newBusiness, website: e.target.value })}
|
||||||
|
placeholder="https://www.negocio.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Latitud</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
value={newBusiness.coordinates.y}
|
||||||
|
onChange={(e) => setNewBusiness({
|
||||||
|
...newBusiness,
|
||||||
|
coordinates: { ...newBusiness.coordinates, y: parseFloat(e.target.value) || 0 }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Longitud</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
value={newBusiness.coordinates.x}
|
||||||
|
onChange={(e) => setNewBusiness({
|
||||||
|
...newBusiness,
|
||||||
|
coordinates: { ...newBusiness.coordinates, x: parseFloat(e.target.value) || 0 }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>URLs de Imágenes (separadas por coma)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={newBusiness.images.join(', ')}
|
||||||
|
onChange={(e) => setNewBusiness({
|
||||||
|
...newBusiness,
|
||||||
|
images: e.target.value.split(',').map(url => url.trim())
|
||||||
|
})}
|
||||||
|
placeholder="https://ejemplo.com/imagen1.jpg, https://ejemplo.com/imagen2.jpg"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleCreate} className="w-full">
|
||||||
|
Crear Comercio
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{filteredBusinesses.map((business) => (
|
||||||
|
<Card key={business.id} className="hover:shadow-lg transition-shadow">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-16 h-16 rounded-lg bg-orange-100 flex items-center justify-center">
|
||||||
|
<Store className="h-8 w-8 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>{business.name}</CardTitle>
|
||||||
|
<CardDescription className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge variant="outline">{business.category}</Badge>
|
||||||
|
{business.isVerified && (
|
||||||
|
<Badge className="bg-green-100 text-green-800">Verificado</Badge>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="flex items-center gap-1">
|
||||||
|
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500" />
|
||||||
|
{business.rating || '4.5'}
|
||||||
|
</Badge>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => console.log('Edit:', business.id)}>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handleDelete(business.id)}>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">{business.description}</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
{business.address || 'Sin dirección'}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<Phone className="h-4 w-4" />
|
||||||
|
{business.phone || 'Sin teléfono'}
|
||||||
|
</div>
|
||||||
|
{business.website && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<a href={business.website} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
Sitio web
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
{business.isOpen ? 'Abierto' : 'Cerrado'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusinessList;
|
||||||
98
src/components/establishments/BusinessVerification.tsx
Normal file
98
src/components/establishments/BusinessVerification.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { CheckCircle, XCircle, Clock, FileText } from 'lucide-react';
|
||||||
|
|
||||||
|
const pendingVerifications = [
|
||||||
|
{ id: 1, name: 'Restaurante La Marina', category: 'Restaurante', submitted: '2025-10-08', documents: 3 },
|
||||||
|
{ id: 2, name: 'Hotel Vista Mar', category: 'Hotel', submitted: '2025-10-09', documents: 5 },
|
||||||
|
{ id: 3, name: 'Boutique Fashion', category: 'Tienda', submitted: '2025-10-10', documents: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const BusinessVerification = () => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Pendientes</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-yellow-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">12</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Solicitudes por revisar</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Aprobados</CardTitle>
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">245</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Comercios verificados</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Rechazados</CardTitle>
|
||||||
|
<XCircle className="h-4 w-4 text-red-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">8</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">Este mes</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Solicitudes Pendientes</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{pendingVerifications.map((item) => (
|
||||||
|
<div key={item.id} className="border rounded-lg p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-lg bg-yellow-100 flex items-center justify-center">
|
||||||
|
<FileText className="h-6 w-6 text-yellow-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">{item.name}</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge variant="outline" className="text-xs">{item.category}</Badge>
|
||||||
|
<span className="text-xs text-gray-600">
|
||||||
|
Enviado: {item.submitted}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-600">
|
||||||
|
{item.documents} documentos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
Revisar
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="default" className="bg-green-600 hover:bg-green-700">
|
||||||
|
<CheckCircle className="h-4 w-4 mr-1" />
|
||||||
|
Aprobar
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
<XCircle className="h-4 w-4 mr-1" />
|
||||||
|
Rechazar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusinessVerification;
|
||||||
52
src/pages/dashboard/Establishments.tsx
Normal file
52
src/pages/dashboard/Establishments.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import BusinessList from '@/components/establishments/BusinessList';
|
||||||
|
import BusinessCategories from '@/components/establishments/BusinessCategories';
|
||||||
|
import BusinessAnalytics from '@/components/establishments/BusinessAnalytics';
|
||||||
|
import BusinessVerification from '@/components/establishments/BusinessVerification';
|
||||||
|
import { Store } from 'lucide-react';
|
||||||
|
|
||||||
|
const Establishments = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('list');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<Store className="w-8 h-8 text-orange-600" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Gestión de Comercios</h1>
|
||||||
|
<p className="text-gray-600">Administra establecimientos, categorías y verificaciones</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||||
|
<TabsList className="grid w-full grid-cols-4 lg:w-auto">
|
||||||
|
<TabsTrigger value="list">Comercios</TabsTrigger>
|
||||||
|
<TabsTrigger value="categories">Categorías</TabsTrigger>
|
||||||
|
<TabsTrigger value="verification">Verificación</TabsTrigger>
|
||||||
|
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="list">
|
||||||
|
<BusinessList />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="categories">
|
||||||
|
<BusinessCategories />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="verification">
|
||||||
|
<BusinessVerification />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="analytics">
|
||||||
|
<BusinessAnalytics />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Establishments;
|
||||||
Reference in New Issue
Block a user