ADD new frontend
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const BakeryConfigPage: React.FC = () => {
|
||||
const [config, setConfig] = useState({
|
||||
general: {
|
||||
name: 'Panadería Artesanal San Miguel',
|
||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||
logo: '',
|
||||
website: 'https://panaderiasanmiguel.com',
|
||||
email: 'info@panaderiasanmiguel.com',
|
||||
phone: '+34 912 345 678'
|
||||
},
|
||||
location: {
|
||||
address: 'Calle Mayor 123',
|
||||
city: 'Madrid',
|
||||
postalCode: '28001',
|
||||
country: 'España',
|
||||
coordinates: {
|
||||
lat: 40.4168,
|
||||
lng: -3.7038
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
monday: { open: '07:00', close: '20:00', closed: false },
|
||||
tuesday: { open: '07:00', close: '20:00', closed: false },
|
||||
wednesday: { open: '07:00', close: '20:00', closed: false },
|
||||
thursday: { open: '07:00', close: '20:00', closed: false },
|
||||
friday: { open: '07:00', close: '20:00', closed: false },
|
||||
saturday: { open: '08:00', close: '14:00', closed: false },
|
||||
sunday: { open: '09:00', close: '13:00', closed: false }
|
||||
},
|
||||
business: {
|
||||
taxId: 'B12345678',
|
||||
registrationNumber: 'REG-2024-001',
|
||||
licenseNumber: 'LIC-FOOD-2024',
|
||||
currency: 'EUR',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es'
|
||||
},
|
||||
preferences: {
|
||||
enableOnlineOrders: true,
|
||||
enableReservations: false,
|
||||
enableDelivery: true,
|
||||
deliveryRadius: 5,
|
||||
minimumOrderAmount: 15.00,
|
||||
enableLoyaltyProgram: true,
|
||||
autoBackup: true,
|
||||
emailNotifications: true,
|
||||
smsNotifications: false
|
||||
}
|
||||
});
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'General', icon: Store },
|
||||
{ id: 'location', label: 'Ubicación', icon: MapPin },
|
||||
{ id: 'schedule', label: 'Horarios', icon: Clock },
|
||||
{ id: 'business', label: 'Empresa', icon: Globe }
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
{ key: 'monday', label: 'Lunes' },
|
||||
{ key: 'tuesday', label: 'Martes' },
|
||||
{ key: 'wednesday', label: 'Miércoles' },
|
||||
{ key: 'thursday', label: 'Jueves' },
|
||||
{ key: 'friday', label: 'Viernes' },
|
||||
{ key: 'saturday', label: 'Sábado' },
|
||||
{ key: 'sunday', label: 'Domingo' }
|
||||
];
|
||||
|
||||
const handleInputChange = (section: string, field: string, value: any) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleScheduleChange = (day: string, field: string, value: any) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
schedule: {
|
||||
...prev.schedule,
|
||||
[day]: {
|
||||
...prev.schedule[day as keyof typeof prev.schedule],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Handle save logic
|
||||
console.log('Saving bakery config:', config);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// Reset to defaults
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración de Panadería"
|
||||
description="Configura los datos básicos y preferencias de tu panadería"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Restaurar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Sidebar */}
|
||||
<div className="w-full lg:w-64">
|
||||
<Card className="p-4">
|
||||
<nav className="space-y-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-[var(--color-info)]/10 text-[var(--color-info)]'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1">
|
||||
{activeTab === 'general' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Información General</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Nombre de la Panadería
|
||||
</label>
|
||||
<Input
|
||||
value={config.general.name}
|
||||
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
|
||||
placeholder="Nombre de tu panadería"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Sitio Web
|
||||
</label>
|
||||
<Input
|
||||
value={config.general.website}
|
||||
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
|
||||
placeholder="https://tu-panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Descripción
|
||||
</label>
|
||||
<textarea
|
||||
value={config.general.description}
|
||||
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
placeholder="Describe tu panadería..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Email de Contacto
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
value={config.general.email}
|
||||
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
|
||||
className="pl-10"
|
||||
type="email"
|
||||
placeholder="contacto@panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Teléfono
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
value={config.general.phone}
|
||||
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
|
||||
className="pl-10"
|
||||
type="tel"
|
||||
placeholder="+34 912 345 678"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'location' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Ubicación</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Dirección
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.address}
|
||||
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
|
||||
placeholder="Calle, número, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Ciudad
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.city}
|
||||
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
|
||||
placeholder="Ciudad"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Código Postal
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.postalCode}
|
||||
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
|
||||
placeholder="28001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
País
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.country}
|
||||
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
|
||||
placeholder="España"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Latitud
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.coordinates.lat}
|
||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
||||
...config.location.coordinates,
|
||||
lat: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="40.4168"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Longitud
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.coordinates.lng}
|
||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
||||
...config.location.coordinates,
|
||||
lng: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="-3.7038"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'schedule' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Horarios de Apertura</h3>
|
||||
<div className="space-y-4">
|
||||
{daysOfWeek.map((day) => {
|
||||
const schedule = config.schedule[day.key as keyof typeof config.schedule];
|
||||
return (
|
||||
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
|
||||
<div className="w-20">
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">{day.label}</span>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={schedule.closed}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm text-[var(--text-secondary)]">Cerrado</span>
|
||||
</label>
|
||||
|
||||
{!schedule.closed && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Apertura</label>
|
||||
<input
|
||||
type="time"
|
||||
value={schedule.open}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
|
||||
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Cierre</label>
|
||||
<input
|
||||
type="time"
|
||||
value={schedule.close}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
|
||||
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'business' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Datos de Empresa</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
NIF/CIF
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.taxId}
|
||||
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
|
||||
placeholder="B12345678"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Número de Registro
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.registrationNumber}
|
||||
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
|
||||
placeholder="REG-2024-001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Licencia Sanitaria
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.licenseNumber}
|
||||
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
|
||||
placeholder="LIC-FOOD-2024"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Moneda
|
||||
</label>
|
||||
<select
|
||||
value={config.business.currency}
|
||||
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Zona Horaria
|
||||
</label>
|
||||
<select
|
||||
value={config.business.timezone}
|
||||
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="Europe/Madrid">Madrid (GMT+1)</option>
|
||||
<option value="Europe/London">Londres (GMT)</option>
|
||||
<option value="America/New_York">Nueva York (GMT-5)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Idioma
|
||||
</label>
|
||||
<select
|
||||
value={config.business.language}
|
||||
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="es">Español</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Changes Banner */}
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
||||
<span className="text-sm">Tienes cambios sin guardar</span>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
|
||||
Descartar
|
||||
</Button>
|
||||
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BakeryConfigPage;
|
||||
@@ -0,0 +1,481 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, RotateCcw } from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const BakeryConfigPage: React.FC = () => {
|
||||
const [config, setConfig] = useState({
|
||||
general: {
|
||||
name: 'Panadería Artesanal San Miguel',
|
||||
description: 'Panadería tradicional con más de 30 años de experiencia',
|
||||
logo: '',
|
||||
website: 'https://panaderiasanmiguel.com',
|
||||
email: 'info@panaderiasanmiguel.com',
|
||||
phone: '+34 912 345 678'
|
||||
},
|
||||
location: {
|
||||
address: 'Calle Mayor 123',
|
||||
city: 'Madrid',
|
||||
postalCode: '28001',
|
||||
country: 'España',
|
||||
coordinates: {
|
||||
lat: 40.4168,
|
||||
lng: -3.7038
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
monday: { open: '07:00', close: '20:00', closed: false },
|
||||
tuesday: { open: '07:00', close: '20:00', closed: false },
|
||||
wednesday: { open: '07:00', close: '20:00', closed: false },
|
||||
thursday: { open: '07:00', close: '20:00', closed: false },
|
||||
friday: { open: '07:00', close: '20:00', closed: false },
|
||||
saturday: { open: '08:00', close: '14:00', closed: false },
|
||||
sunday: { open: '09:00', close: '13:00', closed: false }
|
||||
},
|
||||
business: {
|
||||
taxId: 'B12345678',
|
||||
registrationNumber: 'REG-2024-001',
|
||||
licenseNumber: 'LIC-FOOD-2024',
|
||||
currency: 'EUR',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es'
|
||||
},
|
||||
preferences: {
|
||||
enableOnlineOrders: true,
|
||||
enableReservations: false,
|
||||
enableDelivery: true,
|
||||
deliveryRadius: 5,
|
||||
minimumOrderAmount: 15.00,
|
||||
enableLoyaltyProgram: true,
|
||||
autoBackup: true,
|
||||
emailNotifications: true,
|
||||
smsNotifications: false
|
||||
}
|
||||
});
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'General', icon: Store },
|
||||
{ id: 'location', label: 'Ubicación', icon: MapPin },
|
||||
{ id: 'schedule', label: 'Horarios', icon: Clock },
|
||||
{ id: 'business', label: 'Empresa', icon: Globe }
|
||||
];
|
||||
|
||||
const daysOfWeek = [
|
||||
{ key: 'monday', label: 'Lunes' },
|
||||
{ key: 'tuesday', label: 'Martes' },
|
||||
{ key: 'wednesday', label: 'Miércoles' },
|
||||
{ key: 'thursday', label: 'Jueves' },
|
||||
{ key: 'friday', label: 'Viernes' },
|
||||
{ key: 'saturday', label: 'Sábado' },
|
||||
{ key: 'sunday', label: 'Domingo' }
|
||||
];
|
||||
|
||||
const handleInputChange = (section: string, field: string, value: any) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleScheduleChange = (day: string, field: string, value: any) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
schedule: {
|
||||
...prev.schedule,
|
||||
[day]: {
|
||||
...prev.schedule[day as keyof typeof prev.schedule],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Handle save logic
|
||||
console.log('Saving bakery config:', config);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// Reset to defaults
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración de Panadería"
|
||||
description="Configura los datos básicos y preferencias de tu panadería"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Restaurar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Sidebar */}
|
||||
<div className="w-full lg:w-64">
|
||||
<Card className="p-4">
|
||||
<nav className="space-y-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1">
|
||||
{activeTab === 'general' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Información General</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre de la Panadería
|
||||
</label>
|
||||
<Input
|
||||
value={config.general.name}
|
||||
onChange={(e) => handleInputChange('general', 'name', e.target.value)}
|
||||
placeholder="Nombre de tu panadería"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Sitio Web
|
||||
</label>
|
||||
<Input
|
||||
value={config.general.website}
|
||||
onChange={(e) => handleInputChange('general', 'website', e.target.value)}
|
||||
placeholder="https://tu-panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Descripción
|
||||
</label>
|
||||
<textarea
|
||||
value={config.general.description}
|
||||
onChange={(e) => handleInputChange('general', 'description', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
placeholder="Describe tu panadería..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email de Contacto
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
value={config.general.email}
|
||||
onChange={(e) => handleInputChange('general', 'email', e.target.value)}
|
||||
className="pl-10"
|
||||
type="email"
|
||||
placeholder="contacto@panaderia.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Teléfono
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
value={config.general.phone}
|
||||
onChange={(e) => handleInputChange('general', 'phone', e.target.value)}
|
||||
className="pl-10"
|
||||
type="tel"
|
||||
placeholder="+34 912 345 678"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'location' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Ubicación</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Dirección
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.address}
|
||||
onChange={(e) => handleInputChange('location', 'address', e.target.value)}
|
||||
placeholder="Calle, número, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ciudad
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.city}
|
||||
onChange={(e) => handleInputChange('location', 'city', e.target.value)}
|
||||
placeholder="Ciudad"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Código Postal
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.postalCode}
|
||||
onChange={(e) => handleInputChange('location', 'postalCode', e.target.value)}
|
||||
placeholder="28001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
País
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.country}
|
||||
onChange={(e) => handleInputChange('location', 'country', e.target.value)}
|
||||
placeholder="España"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Latitud
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.coordinates.lat}
|
||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
||||
...config.location.coordinates,
|
||||
lat: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="40.4168"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Longitud
|
||||
</label>
|
||||
<Input
|
||||
value={config.location.coordinates.lng}
|
||||
onChange={(e) => handleInputChange('location', 'coordinates', {
|
||||
...config.location.coordinates,
|
||||
lng: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="-3.7038"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'schedule' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Horarios de Apertura</h3>
|
||||
<div className="space-y-4">
|
||||
{daysOfWeek.map((day) => {
|
||||
const schedule = config.schedule[day.key as keyof typeof config.schedule];
|
||||
return (
|
||||
<div key={day.key} className="flex items-center space-x-4 p-4 border rounded-lg">
|
||||
<div className="w-20">
|
||||
<span className="text-sm font-medium text-gray-700">{day.label}</span>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={schedule.closed}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'closed', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-gray-600">Cerrado</span>
|
||||
</label>
|
||||
|
||||
{!schedule.closed && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">Apertura</label>
|
||||
<input
|
||||
type="time"
|
||||
value={schedule.open}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'open', e.target.value)}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">Cierre</label>
|
||||
<input
|
||||
type="time"
|
||||
value={schedule.close}
|
||||
onChange={(e) => handleScheduleChange(day.key, 'close', e.target.value)}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'business' && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Datos de Empresa</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
NIF/CIF
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.taxId}
|
||||
onChange={(e) => handleInputChange('business', 'taxId', e.target.value)}
|
||||
placeholder="B12345678"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Número de Registro
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.registrationNumber}
|
||||
onChange={(e) => handleInputChange('business', 'registrationNumber', e.target.value)}
|
||||
placeholder="REG-2024-001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Licencia Sanitaria
|
||||
</label>
|
||||
<Input
|
||||
value={config.business.licenseNumber}
|
||||
onChange={(e) => handleInputChange('business', 'licenseNumber', e.target.value)}
|
||||
placeholder="LIC-FOOD-2024"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Moneda
|
||||
</label>
|
||||
<select
|
||||
value={config.business.currency}
|
||||
onChange={(e) => handleInputChange('business', 'currency', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Zona Horaria
|
||||
</label>
|
||||
<select
|
||||
value={config.business.timezone}
|
||||
onChange={(e) => handleInputChange('business', 'timezone', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="Europe/Madrid">Madrid (GMT+1)</option>
|
||||
<option value="Europe/London">Londres (GMT)</option>
|
||||
<option value="America/New_York">Nueva York (GMT-5)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Idioma
|
||||
</label>
|
||||
<select
|
||||
value={config.business.language}
|
||||
onChange={(e) => handleInputChange('business', 'language', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="es">Español</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Changes Banner */}
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
||||
<span className="text-sm">Tienes cambios sin guardar</span>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
|
||||
Descartar
|
||||
</Button>
|
||||
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BakeryConfigPage;
|
||||
1
frontend/src/pages/app/settings/bakery-config/index.ts
Normal file
1
frontend/src/pages/app/settings/bakery-config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as BakeryConfigPage } from './BakeryConfigPage';
|
||||
591
frontend/src/pages/app/settings/system/SystemSettingsPage.tsx
Normal file
591
frontend/src/pages/app/settings/system/SystemSettingsPage.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Settings, Shield, Database, Bell, Wifi, HardDrive, Activity, Save, RotateCcw, AlertTriangle } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const SystemSettingsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [settings, setSettings] = useState({
|
||||
general: {
|
||||
systemName: 'Bakery-IA Sistema',
|
||||
version: '2.1.0',
|
||||
environment: 'production',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es',
|
||||
currency: 'EUR',
|
||||
dateFormat: 'dd/mm/yyyy',
|
||||
autoUpdates: true,
|
||||
maintenanceMode: false
|
||||
},
|
||||
security: {
|
||||
sessionTimeout: 120,
|
||||
maxLoginAttempts: 5,
|
||||
passwordComplexity: true,
|
||||
twoFactorAuth: false,
|
||||
ipWhitelist: '',
|
||||
sslEnabled: true,
|
||||
encryptionLevel: 'AES256',
|
||||
auditLogging: true,
|
||||
dataRetention: 365
|
||||
},
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
name: 'bakery_ia_db',
|
||||
backupFrequency: 'daily',
|
||||
backupRetention: 30,
|
||||
maintenanceWindow: '02:00-04:00',
|
||||
connectionPool: 20,
|
||||
slowQueryLogging: true,
|
||||
performanceMonitoring: true
|
||||
},
|
||||
notifications: {
|
||||
emailEnabled: true,
|
||||
smsEnabled: false,
|
||||
pushEnabled: true,
|
||||
slackIntegration: false,
|
||||
webhookUrl: '',
|
||||
alertThreshold: 'medium',
|
||||
systemAlerts: true,
|
||||
performanceAlerts: true,
|
||||
securityAlerts: true
|
||||
},
|
||||
performance: {
|
||||
cacheEnabled: true,
|
||||
cacheTtl: 3600,
|
||||
compressionEnabled: true,
|
||||
cdnEnabled: false,
|
||||
loadBalancing: false,
|
||||
memoryLimit: '2GB',
|
||||
cpuThreshold: 80,
|
||||
diskSpaceThreshold: 85,
|
||||
logLevel: 'info'
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'General', icon: Settings },
|
||||
{ id: 'security', label: 'Seguridad', icon: Shield },
|
||||
{ id: 'database', label: 'Base de Datos', icon: Database },
|
||||
{ id: 'notifications', label: 'Notificaciones', icon: Bell },
|
||||
{ id: 'performance', label: 'Rendimiento', icon: Activity }
|
||||
];
|
||||
|
||||
const systemStats = {
|
||||
uptime: '15 días, 7 horas',
|
||||
memoryUsage: 68,
|
||||
diskUsage: 42,
|
||||
cpuUsage: 23,
|
||||
activeUsers: 12,
|
||||
lastBackup: '2024-01-26 02:15:00',
|
||||
version: '2.1.0',
|
||||
environment: 'Production'
|
||||
};
|
||||
|
||||
const systemLogs = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-26 10:30:00',
|
||||
level: 'INFO',
|
||||
category: 'System',
|
||||
message: 'Backup automático completado exitosamente',
|
||||
details: 'Database: bakery_ia_db, Size: 245MB, Duration: 3.2s'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-26 09:15:00',
|
||||
level: 'WARN',
|
||||
category: 'Performance',
|
||||
message: 'Uso de CPU alto detectado',
|
||||
details: 'CPU usage: 89% for 5 minutes, Process: data-processor'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-26 08:45:00',
|
||||
level: 'INFO',
|
||||
category: 'Security',
|
||||
message: 'Usuario admin autenticado correctamente',
|
||||
details: 'IP: 192.168.1.100, Session: sess_abc123'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: '2024-01-26 07:30:00',
|
||||
level: 'ERROR',
|
||||
category: 'Database',
|
||||
message: 'Consulta lenta detectada',
|
||||
details: 'Query duration: 5.8s, Table: sales_analytics'
|
||||
}
|
||||
];
|
||||
|
||||
const handleSettingChange = (section: string, field: string, value: any) => {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Saving system settings:', settings);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'ERROR': return 'red';
|
||||
case 'WARN': return 'yellow';
|
||||
case 'INFO': return 'blue';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageColor = (usage: number) => {
|
||||
if (usage >= 80) return 'text-[var(--color-error)]';
|
||||
if (usage >= 60) return 'text-yellow-600';
|
||||
return 'text-[var(--color-success)]';
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'general':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Nombre del Sistema
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.general.systemName}
|
||||
onChange={(e) => handleSettingChange('general', 'systemName', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Zona Horaria
|
||||
</label>
|
||||
<select
|
||||
value={settings.general.timezone}
|
||||
onChange={(e) => handleSettingChange('general', 'timezone', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="Europe/Madrid">Madrid (GMT+1)</option>
|
||||
<option value="Europe/London">Londres (GMT)</option>
|
||||
<option value="America/New_York">Nueva York (GMT-5)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Idioma del Sistema
|
||||
</label>
|
||||
<select
|
||||
value={settings.general.language}
|
||||
onChange={(e) => handleSettingChange('general', 'language', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="es">Español</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Formato de Fecha
|
||||
</label>
|
||||
<select
|
||||
value={settings.general.dateFormat}
|
||||
onChange={(e) => handleSettingChange('general', 'dateFormat', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="dd/mm/yyyy">DD/MM/YYYY</option>
|
||||
<option value="mm/dd/yyyy">MM/DD/YYYY</option>
|
||||
<option value="yyyy-mm-dd">YYYY-MM-DD</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.general.autoUpdates}
|
||||
onChange={(e) => handleSettingChange('general', 'autoUpdates', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Actualizaciones Automáticas</span>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Instalar actualizaciones de seguridad automáticamente</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.general.maintenanceMode}
|
||||
onChange={(e) => handleSettingChange('general', 'maintenanceMode', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Modo Mantenimiento</span>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Deshabilitar acceso durante mantenimiento</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'security':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Tiempo de Sesión (minutos)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.security.sessionTimeout}
|
||||
onChange={(e) => handleSettingChange('security', 'sessionTimeout', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Intentos Máximos de Login
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.security.maxLoginAttempts}
|
||||
onChange={(e) => handleSettingChange('security', 'maxLoginAttempts', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Nivel de Encriptación
|
||||
</label>
|
||||
<select
|
||||
value={settings.security.encryptionLevel}
|
||||
onChange={(e) => handleSettingChange('security', 'encryptionLevel', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="AES128">AES-128</option>
|
||||
<option value="AES256">AES-256</option>
|
||||
<option value="AES512">AES-512</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Retención de Datos (días)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.security.dataRetention}
|
||||
onChange={(e) => handleSettingChange('security', 'dataRetention', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.passwordComplexity}
|
||||
onChange={(e) => handleSettingChange('security', 'passwordComplexity', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Complejidad de Contraseñas</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.twoFactorAuth}
|
||||
onChange={(e) => handleSettingChange('security', 'twoFactorAuth', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Autenticación de Dos Factores</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.auditLogging}
|
||||
onChange={(e) => handleSettingChange('security', 'auditLogging', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Registro de Auditoría</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'database':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-yellow-800">Configuración Avanzada</p>
|
||||
<p className="text-sm text-yellow-700">Cambios incorrectos pueden afectar el rendimiento del sistema</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Frecuencia de Backup
|
||||
</label>
|
||||
<select
|
||||
value={settings.database.backupFrequency}
|
||||
onChange={(e) => handleSettingChange('database', 'backupFrequency', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="hourly">Cada Hora</option>
|
||||
<option value="daily">Diario</option>
|
||||
<option value="weekly">Semanal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Retención de Backups (días)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.database.backupRetention}
|
||||
onChange={(e) => handleSettingChange('database', 'backupRetention', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Ventana de Mantenimiento
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.database.maintenanceWindow}
|
||||
onChange={(e) => handleSettingChange('database', 'maintenanceWindow', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
placeholder="02:00-04:00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Pool de Conexiones
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.database.connectionPool}
|
||||
onChange={(e) => handleSettingChange('database', 'connectionPool', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.database.slowQueryLogging}
|
||||
onChange={(e) => handleSettingChange('database', 'slowQueryLogging', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Registro de Consultas Lentas</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.database.performanceMonitoring}
|
||||
onChange={(e) => handleSettingChange('database', 'performanceMonitoring', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)]"
|
||||
/>
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">Monitoreo de Rendimiento</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div>Contenido no disponible</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración del Sistema"
|
||||
description="Administra la configuración técnica y seguridad del sistema"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Restaurar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* System Status */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Tiempo Activo</p>
|
||||
<p className="text-lg font-bold text-[var(--color-success)]">{systemStats.uptime}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Uso de Memoria</p>
|
||||
<p className={`text-lg font-bold ${getUsageColor(systemStats.memoryUsage)}`}>
|
||||
{systemStats.memoryUsage}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<HardDrive className="h-6 w-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Uso de CPU</p>
|
||||
<p className={`text-lg font-bold ${getUsageColor(systemStats.cpuUsage)}`}>
|
||||
{systemStats.cpuUsage}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Usuarios Activos</p>
|
||||
<p className="text-lg font-bold text-[var(--color-primary)]">{systemStats.activeUsers}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<Wifi className="h-6 w-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Settings Tabs */}
|
||||
<div>
|
||||
<Card className="p-4">
|
||||
<nav className="space-y-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-[var(--color-info)]/10 text-[var(--color-info)]'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
{tabs.find(tab => tab.id === activeTab)?.label}
|
||||
</h3>
|
||||
{renderTabContent()}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Logs */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Registro del Sistema</h3>
|
||||
<div className="space-y-3">
|
||||
{systemLogs.map((log) => (
|
||||
<div key={log.id} className="flex items-start space-x-4 p-3 border rounded-lg">
|
||||
<Badge variant={getLevelColor(log.level)} className="mt-1">
|
||||
{log.level}
|
||||
</Badge>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-1">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">{log.message}</span>
|
||||
<span className="text-xs text-[var(--text-tertiary)]">{log.category}</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)]">{log.details}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">{log.timestamp}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Save Changes Banner */}
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
||||
<span className="text-sm">Tienes cambios sin guardar</span>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" className="text-[var(--color-info)] bg-white" onClick={handleReset}>
|
||||
Descartar
|
||||
</Button>
|
||||
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSettingsPage;
|
||||
@@ -0,0 +1,591 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Settings, Shield, Database, Bell, Wifi, HardDrive, Activity, Save, RotateCcw, AlertTriangle } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const SystemSettingsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [settings, setSettings] = useState({
|
||||
general: {
|
||||
systemName: 'Bakery-IA Sistema',
|
||||
version: '2.1.0',
|
||||
environment: 'production',
|
||||
timezone: 'Europe/Madrid',
|
||||
language: 'es',
|
||||
currency: 'EUR',
|
||||
dateFormat: 'dd/mm/yyyy',
|
||||
autoUpdates: true,
|
||||
maintenanceMode: false
|
||||
},
|
||||
security: {
|
||||
sessionTimeout: 120,
|
||||
maxLoginAttempts: 5,
|
||||
passwordComplexity: true,
|
||||
twoFactorAuth: false,
|
||||
ipWhitelist: '',
|
||||
sslEnabled: true,
|
||||
encryptionLevel: 'AES256',
|
||||
auditLogging: true,
|
||||
dataRetention: 365
|
||||
},
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
name: 'bakery_ia_db',
|
||||
backupFrequency: 'daily',
|
||||
backupRetention: 30,
|
||||
maintenanceWindow: '02:00-04:00',
|
||||
connectionPool: 20,
|
||||
slowQueryLogging: true,
|
||||
performanceMonitoring: true
|
||||
},
|
||||
notifications: {
|
||||
emailEnabled: true,
|
||||
smsEnabled: false,
|
||||
pushEnabled: true,
|
||||
slackIntegration: false,
|
||||
webhookUrl: '',
|
||||
alertThreshold: 'medium',
|
||||
systemAlerts: true,
|
||||
performanceAlerts: true,
|
||||
securityAlerts: true
|
||||
},
|
||||
performance: {
|
||||
cacheEnabled: true,
|
||||
cacheTtl: 3600,
|
||||
compressionEnabled: true,
|
||||
cdnEnabled: false,
|
||||
loadBalancing: false,
|
||||
memoryLimit: '2GB',
|
||||
cpuThreshold: 80,
|
||||
diskSpaceThreshold: 85,
|
||||
logLevel: 'info'
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'General', icon: Settings },
|
||||
{ id: 'security', label: 'Seguridad', icon: Shield },
|
||||
{ id: 'database', label: 'Base de Datos', icon: Database },
|
||||
{ id: 'notifications', label: 'Notificaciones', icon: Bell },
|
||||
{ id: 'performance', label: 'Rendimiento', icon: Activity }
|
||||
];
|
||||
|
||||
const systemStats = {
|
||||
uptime: '15 días, 7 horas',
|
||||
memoryUsage: 68,
|
||||
diskUsage: 42,
|
||||
cpuUsage: 23,
|
||||
activeUsers: 12,
|
||||
lastBackup: '2024-01-26 02:15:00',
|
||||
version: '2.1.0',
|
||||
environment: 'Production'
|
||||
};
|
||||
|
||||
const systemLogs = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-26 10:30:00',
|
||||
level: 'INFO',
|
||||
category: 'System',
|
||||
message: 'Backup automático completado exitosamente',
|
||||
details: 'Database: bakery_ia_db, Size: 245MB, Duration: 3.2s'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-26 09:15:00',
|
||||
level: 'WARN',
|
||||
category: 'Performance',
|
||||
message: 'Uso de CPU alto detectado',
|
||||
details: 'CPU usage: 89% for 5 minutes, Process: data-processor'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-26 08:45:00',
|
||||
level: 'INFO',
|
||||
category: 'Security',
|
||||
message: 'Usuario admin autenticado correctamente',
|
||||
details: 'IP: 192.168.1.100, Session: sess_abc123'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: '2024-01-26 07:30:00',
|
||||
level: 'ERROR',
|
||||
category: 'Database',
|
||||
message: 'Consulta lenta detectada',
|
||||
details: 'Query duration: 5.8s, Table: sales_analytics'
|
||||
}
|
||||
];
|
||||
|
||||
const handleSettingChange = (section: string, field: string, value: any) => {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section as keyof typeof prev],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log('Saving system settings:', settings);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'ERROR': return 'red';
|
||||
case 'WARN': return 'yellow';
|
||||
case 'INFO': return 'blue';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageColor = (usage: number) => {
|
||||
if (usage >= 80) return 'text-red-600';
|
||||
if (usage >= 60) return 'text-yellow-600';
|
||||
return 'text-green-600';
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'general':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre del Sistema
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.general.systemName}
|
||||
onChange={(e) => handleSettingChange('general', 'systemName', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Zona Horaria
|
||||
</label>
|
||||
<select
|
||||
value={settings.general.timezone}
|
||||
onChange={(e) => handleSettingChange('general', 'timezone', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="Europe/Madrid">Madrid (GMT+1)</option>
|
||||
<option value="Europe/London">Londres (GMT)</option>
|
||||
<option value="America/New_York">Nueva York (GMT-5)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Idioma del Sistema
|
||||
</label>
|
||||
<select
|
||||
value={settings.general.language}
|
||||
onChange={(e) => handleSettingChange('general', 'language', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="es">Español</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Formato de Fecha
|
||||
</label>
|
||||
<select
|
||||
value={settings.general.dateFormat}
|
||||
onChange={(e) => handleSettingChange('general', 'dateFormat', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="dd/mm/yyyy">DD/MM/YYYY</option>
|
||||
<option value="mm/dd/yyyy">MM/DD/YYYY</option>
|
||||
<option value="yyyy-mm-dd">YYYY-MM-DD</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.general.autoUpdates}
|
||||
onChange={(e) => handleSettingChange('general', 'autoUpdates', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">Actualizaciones Automáticas</span>
|
||||
<p className="text-xs text-gray-500">Instalar actualizaciones de seguridad automáticamente</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.general.maintenanceMode}
|
||||
onChange={(e) => handleSettingChange('general', 'maintenanceMode', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">Modo Mantenimiento</span>
|
||||
<p className="text-xs text-gray-500">Deshabilitar acceso durante mantenimiento</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'security':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tiempo de Sesión (minutos)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.security.sessionTimeout}
|
||||
onChange={(e) => handleSettingChange('security', 'sessionTimeout', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Intentos Máximos de Login
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.security.maxLoginAttempts}
|
||||
onChange={(e) => handleSettingChange('security', 'maxLoginAttempts', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nivel de Encriptación
|
||||
</label>
|
||||
<select
|
||||
value={settings.security.encryptionLevel}
|
||||
onChange={(e) => handleSettingChange('security', 'encryptionLevel', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="AES128">AES-128</option>
|
||||
<option value="AES256">AES-256</option>
|
||||
<option value="AES512">AES-512</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Retención de Datos (días)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.security.dataRetention}
|
||||
onChange={(e) => handleSettingChange('security', 'dataRetention', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.passwordComplexity}
|
||||
onChange={(e) => handleSettingChange('security', 'passwordComplexity', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Complejidad de Contraseñas</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.twoFactorAuth}
|
||||
onChange={(e) => handleSettingChange('security', 'twoFactorAuth', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Autenticación de Dos Factores</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.auditLogging}
|
||||
onChange={(e) => handleSettingChange('security', 'auditLogging', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Registro de Auditoría</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'database':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-yellow-800">Configuración Avanzada</p>
|
||||
<p className="text-sm text-yellow-700">Cambios incorrectos pueden afectar el rendimiento del sistema</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Frecuencia de Backup
|
||||
</label>
|
||||
<select
|
||||
value={settings.database.backupFrequency}
|
||||
onChange={(e) => handleSettingChange('database', 'backupFrequency', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="hourly">Cada Hora</option>
|
||||
<option value="daily">Diario</option>
|
||||
<option value="weekly">Semanal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Retención de Backups (días)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.database.backupRetention}
|
||||
onChange={(e) => handleSettingChange('database', 'backupRetention', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ventana de Mantenimiento
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.database.maintenanceWindow}
|
||||
onChange={(e) => handleSettingChange('database', 'maintenanceWindow', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
placeholder="02:00-04:00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Pool de Conexiones
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.database.connectionPool}
|
||||
onChange={(e) => handleSettingChange('database', 'connectionPool', parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.database.slowQueryLogging}
|
||||
onChange={(e) => handleSettingChange('database', 'slowQueryLogging', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Registro de Consultas Lentas</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.database.performanceMonitoring}
|
||||
onChange={(e) => handleSettingChange('database', 'performanceMonitoring', e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Monitoreo de Rendimiento</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div>Contenido no disponible</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Configuración del Sistema"
|
||||
description="Administra la configuración técnica y seguridad del sistema"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Restaurar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanges}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* System Status */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Tiempo Activo</p>
|
||||
<p className="text-lg font-bold text-green-600">{systemStats.uptime}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Uso de Memoria</p>
|
||||
<p className={`text-lg font-bold ${getUsageColor(systemStats.memoryUsage)}`}>
|
||||
{systemStats.memoryUsage}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<HardDrive className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Uso de CPU</p>
|
||||
<p className={`text-lg font-bold ${getUsageColor(systemStats.cpuUsage)}`}>
|
||||
{systemStats.cpuUsage}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Usuarios Activos</p>
|
||||
<p className="text-lg font-bold text-orange-600">{systemStats.activeUsers}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<Wifi className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Settings Tabs */}
|
||||
<div>
|
||||
<Card className="p-4">
|
||||
<nav className="space-y-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center space-x-3 px-3 py-2 text-left rounded-lg transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">
|
||||
{tabs.find(tab => tab.id === activeTab)?.label}
|
||||
</h3>
|
||||
{renderTabContent()}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Logs */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Registro del Sistema</h3>
|
||||
<div className="space-y-3">
|
||||
{systemLogs.map((log) => (
|
||||
<div key={log.id} className="flex items-start space-x-4 p-3 border rounded-lg">
|
||||
<Badge variant={getLevelColor(log.level)} className="mt-1">
|
||||
{log.level}
|
||||
</Badge>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-1">
|
||||
<span className="text-sm font-medium text-gray-900">{log.message}</span>
|
||||
<span className="text-xs text-gray-500">{log.category}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">{log.details}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{log.timestamp}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Save Changes Banner */}
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4">
|
||||
<span className="text-sm">Tienes cambios sin guardar</span>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline" className="text-blue-600 bg-white" onClick={handleReset}>
|
||||
Descartar
|
||||
</Button>
|
||||
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSave}>
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSettingsPage;
|
||||
1
frontend/src/pages/app/settings/system/index.ts
Normal file
1
frontend/src/pages/app/settings/system/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as SystemSettingsPage } from './SystemSettingsPage';
|
||||
406
frontend/src/pages/app/settings/team/TeamPage.tsx
Normal file
406
frontend/src/pages/app/settings/team/TeamPage.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||
import { Button, Card, Badge, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedRole, setSelectedRole] = useState('all');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const teamMembers = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'María González',
|
||||
email: 'maria.gonzalez@panaderia.com',
|
||||
phone: '+34 600 123 456',
|
||||
role: 'manager',
|
||||
department: 'Administración',
|
||||
status: 'active',
|
||||
joinDate: '2022-03-15',
|
||||
lastLogin: '2024-01-26 09:30:00',
|
||||
permissions: ['inventory', 'sales', 'reports', 'team'],
|
||||
avatar: '/avatars/maria.jpg',
|
||||
schedule: {
|
||||
monday: '07:00-15:00',
|
||||
tuesday: '07:00-15:00',
|
||||
wednesday: '07:00-15:00',
|
||||
thursday: '07:00-15:00',
|
||||
friday: '07:00-15:00',
|
||||
saturday: 'Libre',
|
||||
sunday: 'Libre'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Carlos Rodríguez',
|
||||
email: 'carlos.rodriguez@panaderia.com',
|
||||
phone: '+34 600 234 567',
|
||||
role: 'baker',
|
||||
department: 'Producción',
|
||||
status: 'active',
|
||||
joinDate: '2021-09-20',
|
||||
lastLogin: '2024-01-26 08:45:00',
|
||||
permissions: ['production', 'inventory'],
|
||||
avatar: '/avatars/carlos.jpg',
|
||||
schedule: {
|
||||
monday: '05:00-13:00',
|
||||
tuesday: '05:00-13:00',
|
||||
wednesday: '05:00-13:00',
|
||||
thursday: '05:00-13:00',
|
||||
friday: '05:00-13:00',
|
||||
saturday: '05:00-11:00',
|
||||
sunday: 'Libre'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Ana Martínez',
|
||||
email: 'ana.martinez@panaderia.com',
|
||||
phone: '+34 600 345 678',
|
||||
role: 'cashier',
|
||||
department: 'Ventas',
|
||||
status: 'active',
|
||||
joinDate: '2023-01-10',
|
||||
lastLogin: '2024-01-26 10:15:00',
|
||||
permissions: ['sales', 'pos'],
|
||||
avatar: '/avatars/ana.jpg',
|
||||
schedule: {
|
||||
monday: '08:00-16:00',
|
||||
tuesday: '08:00-16:00',
|
||||
wednesday: 'Libre',
|
||||
thursday: '08:00-16:00',
|
||||
friday: '08:00-16:00',
|
||||
saturday: '09:00-14:00',
|
||||
sunday: '09:00-14:00'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Luis Fernández',
|
||||
email: 'luis.fernandez@panaderia.com',
|
||||
phone: '+34 600 456 789',
|
||||
role: 'baker',
|
||||
department: 'Producción',
|
||||
status: 'inactive',
|
||||
joinDate: '2020-11-05',
|
||||
lastLogin: '2024-01-20 16:30:00',
|
||||
permissions: ['production'],
|
||||
avatar: '/avatars/luis.jpg',
|
||||
schedule: {
|
||||
monday: '13:00-21:00',
|
||||
tuesday: '13:00-21:00',
|
||||
wednesday: '13:00-21:00',
|
||||
thursday: 'Libre',
|
||||
friday: '13:00-21:00',
|
||||
saturday: 'Libre',
|
||||
sunday: '13:00-21:00'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Isabel Torres',
|
||||
email: 'isabel.torres@panaderia.com',
|
||||
phone: '+34 600 567 890',
|
||||
role: 'assistant',
|
||||
department: 'Ventas',
|
||||
status: 'active',
|
||||
joinDate: '2023-06-01',
|
||||
lastLogin: '2024-01-25 18:20:00',
|
||||
permissions: ['sales'],
|
||||
avatar: '/avatars/isabel.jpg',
|
||||
schedule: {
|
||||
monday: 'Libre',
|
||||
tuesday: '16:00-20:00',
|
||||
wednesday: '16:00-20:00',
|
||||
thursday: '16:00-20:00',
|
||||
friday: '16:00-20:00',
|
||||
saturday: '14:00-20:00',
|
||||
sunday: '14:00-20:00'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const roles = [
|
||||
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
|
||||
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
|
||||
{ value: 'baker', label: 'Panadero', count: teamMembers.filter(m => m.role === 'baker').length },
|
||||
{ value: 'cashier', label: 'Cajero', count: teamMembers.filter(m => m.role === 'cashier').length },
|
||||
{ value: 'assistant', label: 'Asistente', count: teamMembers.filter(m => m.role === 'assistant').length }
|
||||
];
|
||||
|
||||
const teamStats = {
|
||||
total: teamMembers.length,
|
||||
active: teamMembers.filter(m => m.status === 'active').length,
|
||||
departments: {
|
||||
production: teamMembers.filter(m => m.department === 'Producción').length,
|
||||
sales: teamMembers.filter(m => m.department === 'Ventas').length,
|
||||
admin: teamMembers.filter(m => m.department === 'Administración').length
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
switch (role) {
|
||||
case 'manager': return 'purple';
|
||||
case 'baker': return 'green';
|
||||
case 'cashier': return 'blue';
|
||||
case 'assistant': return 'yellow';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
return status === 'active' ? 'green' : 'red';
|
||||
};
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
switch (role) {
|
||||
case 'manager': return 'Gerente';
|
||||
case 'baker': return 'Panadero';
|
||||
case 'cashier': return 'Cajero';
|
||||
case 'assistant': return 'Asistente';
|
||||
default: return role;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredMembers = teamMembers.filter(member => {
|
||||
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
|
||||
const matchesSearch = member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
member.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return matchesRole && matchesSearch;
|
||||
});
|
||||
|
||||
const formatLastLogin = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffInDays === 0) {
|
||||
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffInDays === 1) {
|
||||
return 'Ayer';
|
||||
} else {
|
||||
return `hace ${diffInDays} días`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Equipo"
|
||||
description="Administra los miembros del equipo, roles y permisos"
|
||||
action={
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Miembro
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Team Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Equipo</p>
|
||||
<p className="text-3xl font-bold text-[var(--text-primary)]">{teamStats.total}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Activos</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">{teamStats.active}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<UserCheck className="h-6 w-6 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.departments.production}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar miembros del equipo..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role.value}
|
||||
onClick={() => setSelectedRole(role.value)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedRole === role.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
|
||||
}`}
|
||||
>
|
||||
{role.label} ({role.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Team Members List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{filteredMembers.map((member) => (
|
||||
<Card key={member.id} className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
<div className="w-12 h-12 bg-[var(--bg-quaternary)] rounded-full flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{member.name}</h3>
|
||||
<Badge variant={getStatusColor(member.status)}>
|
||||
{member.status === 'active' ? 'Activo' : 'Inactivo'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mb-3">
|
||||
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
{member.email}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
{member.phone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant={getRoleBadgeColor(member.role)}>
|
||||
{getRoleLabel(member.role)}
|
||||
</Badge>
|
||||
<Badge variant="gray">
|
||||
{member.department}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-[var(--text-tertiary)] mb-3">
|
||||
<p>Se unió: {new Date(member.joinDate).toLocaleDateString('es-ES')}</p>
|
||||
<p>Última conexión: {formatLastLogin(member.lastLogin)}</p>
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">Permisos:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{member.permissions.map((permission, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs rounded-full"
|
||||
>
|
||||
{permission}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule Preview */}
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
<p className="font-medium mb-1">Horario esta semana:</p>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{Object.entries(member.schedule).slice(0, 4).map(([day, hours]) => (
|
||||
<span key={day}>
|
||||
{day.charAt(0).toUpperCase()}: {hours}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={member.status === 'active' ? 'text-[var(--color-error)] hover:text-[var(--color-error)]' : 'text-[var(--color-success)] hover:text-[var(--color-success)]'}
|
||||
>
|
||||
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredMembers.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<Users className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No se encontraron miembros</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
No hay miembros del equipo que coincidan con los filtros seleccionados.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Add Member Modal Placeholder */}
|
||||
{showForm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<Card className="p-6 max-w-md w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Nuevo Miembro del Equipo</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Formulario para agregar un nuevo miembro del equipo.
|
||||
</p>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" onClick={() => setShowForm(false)}>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamPage;
|
||||
406
frontend/src/pages/app/settings/team/TeamPage.tsx.backup
Normal file
406
frontend/src/pages/app/settings/team/TeamPage.tsx.backup
Normal file
@@ -0,0 +1,406 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||
import { Button, Card, Badge, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedRole, setSelectedRole] = useState('all');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const teamMembers = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'María González',
|
||||
email: 'maria.gonzalez@panaderia.com',
|
||||
phone: '+34 600 123 456',
|
||||
role: 'manager',
|
||||
department: 'Administración',
|
||||
status: 'active',
|
||||
joinDate: '2022-03-15',
|
||||
lastLogin: '2024-01-26 09:30:00',
|
||||
permissions: ['inventory', 'sales', 'reports', 'team'],
|
||||
avatar: '/avatars/maria.jpg',
|
||||
schedule: {
|
||||
monday: '07:00-15:00',
|
||||
tuesday: '07:00-15:00',
|
||||
wednesday: '07:00-15:00',
|
||||
thursday: '07:00-15:00',
|
||||
friday: '07:00-15:00',
|
||||
saturday: 'Libre',
|
||||
sunday: 'Libre'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Carlos Rodríguez',
|
||||
email: 'carlos.rodriguez@panaderia.com',
|
||||
phone: '+34 600 234 567',
|
||||
role: 'baker',
|
||||
department: 'Producción',
|
||||
status: 'active',
|
||||
joinDate: '2021-09-20',
|
||||
lastLogin: '2024-01-26 08:45:00',
|
||||
permissions: ['production', 'inventory'],
|
||||
avatar: '/avatars/carlos.jpg',
|
||||
schedule: {
|
||||
monday: '05:00-13:00',
|
||||
tuesday: '05:00-13:00',
|
||||
wednesday: '05:00-13:00',
|
||||
thursday: '05:00-13:00',
|
||||
friday: '05:00-13:00',
|
||||
saturday: '05:00-11:00',
|
||||
sunday: 'Libre'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Ana Martínez',
|
||||
email: 'ana.martinez@panaderia.com',
|
||||
phone: '+34 600 345 678',
|
||||
role: 'cashier',
|
||||
department: 'Ventas',
|
||||
status: 'active',
|
||||
joinDate: '2023-01-10',
|
||||
lastLogin: '2024-01-26 10:15:00',
|
||||
permissions: ['sales', 'pos'],
|
||||
avatar: '/avatars/ana.jpg',
|
||||
schedule: {
|
||||
monday: '08:00-16:00',
|
||||
tuesday: '08:00-16:00',
|
||||
wednesday: 'Libre',
|
||||
thursday: '08:00-16:00',
|
||||
friday: '08:00-16:00',
|
||||
saturday: '09:00-14:00',
|
||||
sunday: '09:00-14:00'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Luis Fernández',
|
||||
email: 'luis.fernandez@panaderia.com',
|
||||
phone: '+34 600 456 789',
|
||||
role: 'baker',
|
||||
department: 'Producción',
|
||||
status: 'inactive',
|
||||
joinDate: '2020-11-05',
|
||||
lastLogin: '2024-01-20 16:30:00',
|
||||
permissions: ['production'],
|
||||
avatar: '/avatars/luis.jpg',
|
||||
schedule: {
|
||||
monday: '13:00-21:00',
|
||||
tuesday: '13:00-21:00',
|
||||
wednesday: '13:00-21:00',
|
||||
thursday: 'Libre',
|
||||
friday: '13:00-21:00',
|
||||
saturday: 'Libre',
|
||||
sunday: '13:00-21:00'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Isabel Torres',
|
||||
email: 'isabel.torres@panaderia.com',
|
||||
phone: '+34 600 567 890',
|
||||
role: 'assistant',
|
||||
department: 'Ventas',
|
||||
status: 'active',
|
||||
joinDate: '2023-06-01',
|
||||
lastLogin: '2024-01-25 18:20:00',
|
||||
permissions: ['sales'],
|
||||
avatar: '/avatars/isabel.jpg',
|
||||
schedule: {
|
||||
monday: 'Libre',
|
||||
tuesday: '16:00-20:00',
|
||||
wednesday: '16:00-20:00',
|
||||
thursday: '16:00-20:00',
|
||||
friday: '16:00-20:00',
|
||||
saturday: '14:00-20:00',
|
||||
sunday: '14:00-20:00'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const roles = [
|
||||
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
|
||||
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
|
||||
{ value: 'baker', label: 'Panadero', count: teamMembers.filter(m => m.role === 'baker').length },
|
||||
{ value: 'cashier', label: 'Cajero', count: teamMembers.filter(m => m.role === 'cashier').length },
|
||||
{ value: 'assistant', label: 'Asistente', count: teamMembers.filter(m => m.role === 'assistant').length }
|
||||
];
|
||||
|
||||
const teamStats = {
|
||||
total: teamMembers.length,
|
||||
active: teamMembers.filter(m => m.status === 'active').length,
|
||||
departments: {
|
||||
production: teamMembers.filter(m => m.department === 'Producción').length,
|
||||
sales: teamMembers.filter(m => m.department === 'Ventas').length,
|
||||
admin: teamMembers.filter(m => m.department === 'Administración').length
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
switch (role) {
|
||||
case 'manager': return 'purple';
|
||||
case 'baker': return 'green';
|
||||
case 'cashier': return 'blue';
|
||||
case 'assistant': return 'yellow';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
return status === 'active' ? 'green' : 'red';
|
||||
};
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
switch (role) {
|
||||
case 'manager': return 'Gerente';
|
||||
case 'baker': return 'Panadero';
|
||||
case 'cashier': return 'Cajero';
|
||||
case 'assistant': return 'Asistente';
|
||||
default: return role;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredMembers = teamMembers.filter(member => {
|
||||
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
|
||||
const matchesSearch = member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
member.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return matchesRole && matchesSearch;
|
||||
});
|
||||
|
||||
const formatLastLogin = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffInDays === 0) {
|
||||
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffInDays === 1) {
|
||||
return 'Ayer';
|
||||
} else {
|
||||
return `hace ${diffInDays} días`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Equipo"
|
||||
description="Administra los miembros del equipo, roles y permisos"
|
||||
action={
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Miembro
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Team Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Equipo</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{teamStats.total}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Activos</p>
|
||||
<p className="text-3xl font-bold text-green-600">{teamStats.active}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<UserCheck className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Producción</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{teamStats.departments.production}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Ventas</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar miembros del equipo..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role.value}
|
||||
onClick={() => setSelectedRole(role.value)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedRole === role.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{role.label} ({role.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Team Members List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{filteredMembers.map((member) => (
|
||||
<Card key={member.id} className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4 flex-1">
|
||||
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{member.name}</h3>
|
||||
<Badge variant={getStatusColor(member.status)}>
|
||||
{member.status === 'active' ? 'Activo' : 'Inactivo'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mb-3">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
{member.email}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
{member.phone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant={getRoleBadgeColor(member.role)}>
|
||||
{getRoleLabel(member.role)}
|
||||
</Badge>
|
||||
<Badge variant="gray">
|
||||
{member.department}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 mb-3">
|
||||
<p>Se unió: {new Date(member.joinDate).toLocaleDateString('es-ES')}</p>
|
||||
<p>Última conexión: {formatLastLogin(member.lastLogin)}</p>
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-gray-700 mb-2">Permisos:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{member.permissions.map((permission, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
|
||||
>
|
||||
{permission}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule Preview */}
|
||||
<div className="text-xs text-gray-500">
|
||||
<p className="font-medium mb-1">Horario esta semana:</p>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{Object.entries(member.schedule).slice(0, 4).map(([day, hours]) => (
|
||||
<span key={day}>
|
||||
{day.charAt(0).toUpperCase()}: {hours}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="outline">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={member.status === 'active' ? 'text-red-600 hover:text-red-700' : 'text-green-600 hover:text-green-700'}
|
||||
>
|
||||
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredMembers.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No se encontraron miembros</h3>
|
||||
<p className="text-gray-600">
|
||||
No hay miembros del equipo que coincidan con los filtros seleccionados.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Add Member Modal Placeholder */}
|
||||
{showForm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<Card className="p-6 max-w-md w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nuevo Miembro del Equipo</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Formulario para agregar un nuevo miembro del equipo.
|
||||
</p>
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" onClick={() => setShowForm(false)}>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamPage;
|
||||
1
frontend/src/pages/app/settings/team/index.ts
Normal file
1
frontend/src/pages/app/settings/team/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as TeamPage } from './TeamPage';
|
||||
454
frontend/src/pages/app/settings/training/TrainingPage.tsx
Normal file
454
frontend/src/pages/app/settings/training/TrainingPage.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BookOpen, Play, CheckCircle, Clock, Users, Award, Download, Search } from 'lucide-react';
|
||||
import { Button, Card, Badge, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const TrainingPage: React.FC = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const trainingModules = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Fundamentos de Panadería',
|
||||
description: 'Conceptos básicos de elaboración de pan y técnicas fundamentales',
|
||||
category: 'basics',
|
||||
duration: '2.5 horas',
|
||||
lessons: 12,
|
||||
difficulty: 'beginner',
|
||||
progress: 100,
|
||||
completed: true,
|
||||
rating: 4.8,
|
||||
instructor: 'Chef María González',
|
||||
topics: ['Ingredientes básicos', 'Proceso de amasado', 'Fermentación', 'Horneado'],
|
||||
thumbnail: '/training/bread-basics.jpg'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Técnicas Avanzadas de Bollería',
|
||||
description: 'Elaboración de croissants, hojaldre y productos fermentados complejos',
|
||||
category: 'advanced',
|
||||
duration: '4 horas',
|
||||
lessons: 18,
|
||||
difficulty: 'advanced',
|
||||
progress: 65,
|
||||
completed: false,
|
||||
rating: 4.9,
|
||||
instructor: 'Chef Pierre Laurent',
|
||||
topics: ['Masas laminadas', 'Temperaturas críticas', 'Técnicas de plegado', 'Control de calidad'],
|
||||
thumbnail: '/training/pastry-advanced.jpg'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Seguridad e Higiene Alimentaria',
|
||||
description: 'Protocolos de seguridad, HACCP y normativas sanitarias',
|
||||
category: 'safety',
|
||||
duration: '1.5 horas',
|
||||
lessons: 8,
|
||||
difficulty: 'beginner',
|
||||
progress: 0,
|
||||
completed: false,
|
||||
rating: 4.7,
|
||||
instructor: 'Dr. Ana Rodríguez',
|
||||
topics: ['HACCP', 'Limpieza y desinfección', 'Control de temperaturas', 'Trazabilidad'],
|
||||
thumbnail: '/training/food-safety.jpg'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Gestión de Inventarios',
|
||||
description: 'Optimización de stock, control de mermas y gestión de proveedores',
|
||||
category: 'management',
|
||||
duration: '3 horas',
|
||||
lessons: 15,
|
||||
difficulty: 'intermediate',
|
||||
progress: 30,
|
||||
completed: false,
|
||||
rating: 4.6,
|
||||
instructor: 'Carlos Fernández',
|
||||
topics: ['Rotación de stock', 'Punto de reorden', 'Análisis ABC', 'Negociación con proveedores'],
|
||||
thumbnail: '/training/inventory-mgmt.jpg'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Atención al Cliente',
|
||||
description: 'Técnicas de venta, resolución de quejas y fidelización',
|
||||
category: 'sales',
|
||||
duration: '2 horas',
|
||||
lessons: 10,
|
||||
difficulty: 'beginner',
|
||||
progress: 85,
|
||||
completed: false,
|
||||
rating: 4.8,
|
||||
instructor: 'Isabel Torres',
|
||||
topics: ['Técnicas de venta', 'Comunicación efectiva', 'Manejo de quejas', 'Up-selling'],
|
||||
thumbnail: '/training/customer-service.jpg'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Innovación en Productos',
|
||||
description: 'Desarrollo de nuevos productos, tendencias y análisis de mercado',
|
||||
category: 'innovation',
|
||||
duration: '3.5 horas',
|
||||
lessons: 16,
|
||||
difficulty: 'intermediate',
|
||||
progress: 0,
|
||||
completed: false,
|
||||
rating: 4.7,
|
||||
instructor: 'Chef Daniel Ramos',
|
||||
topics: ['Análisis de tendencias', 'Prototipado', 'Testing de mercado', 'Costos de producción'],
|
||||
thumbnail: '/training/product-innovation.jpg'
|
||||
}
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todos', count: trainingModules.length },
|
||||
{ value: 'basics', label: 'Básicos', count: trainingModules.filter(m => m.category === 'basics').length },
|
||||
{ value: 'advanced', label: 'Avanzado', count: trainingModules.filter(m => m.category === 'advanced').length },
|
||||
{ value: 'safety', label: 'Seguridad', count: trainingModules.filter(m => m.category === 'safety').length },
|
||||
{ value: 'management', label: 'Gestión', count: trainingModules.filter(m => m.category === 'management').length },
|
||||
{ value: 'sales', label: 'Ventas', count: trainingModules.filter(m => m.category === 'sales').length },
|
||||
{ value: 'innovation', label: 'Innovación', count: trainingModules.filter(m => m.category === 'innovation').length }
|
||||
];
|
||||
|
||||
const teamProgress = [
|
||||
{
|
||||
name: 'María González',
|
||||
role: 'Gerente',
|
||||
completedModules: 4,
|
||||
totalModules: 6,
|
||||
currentModule: 'Gestión de Inventarios',
|
||||
progress: 75,
|
||||
certificates: 3
|
||||
},
|
||||
{
|
||||
name: 'Carlos Rodríguez',
|
||||
role: 'Panadero',
|
||||
completedModules: 2,
|
||||
totalModules: 4,
|
||||
currentModule: 'Técnicas Avanzadas de Bollería',
|
||||
progress: 65,
|
||||
certificates: 2
|
||||
},
|
||||
{
|
||||
name: 'Ana Martínez',
|
||||
role: 'Cajera',
|
||||
completedModules: 3,
|
||||
totalModules: 4,
|
||||
currentModule: 'Atención al Cliente',
|
||||
progress: 85,
|
||||
certificates: 2
|
||||
}
|
||||
];
|
||||
|
||||
const trainingStats = {
|
||||
totalModules: trainingModules.length,
|
||||
completedModules: trainingModules.filter(m => m.completed).length,
|
||||
inProgress: trainingModules.filter(m => m.progress > 0 && !m.completed).length,
|
||||
totalHours: trainingModules.reduce((sum, m) => sum + parseFloat(m.duration), 0),
|
||||
avgRating: trainingModules.reduce((sum, m) => sum + m.rating, 0) / trainingModules.length
|
||||
};
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'beginner': return 'green';
|
||||
case 'intermediate': return 'yellow';
|
||||
case 'advanced': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getDifficultyLabel = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'beginner': return 'Principiante';
|
||||
case 'intermediate': return 'Intermedio';
|
||||
case 'advanced': return 'Avanzado';
|
||||
default: return difficulty;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredModules = trainingModules.filter(module => {
|
||||
const matchesCategory = selectedCategory === 'all' || module.category === selectedCategory;
|
||||
const matchesSearch = module.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
module.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Centro de Formación"
|
||||
description="Módulos de capacitación y desarrollo profesional para el equipo"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Certificados
|
||||
</Button>
|
||||
<Button>
|
||||
Nuevo Módulo
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Training Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Módulos Totales</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-info)]">{trainingStats.totalModules}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<BookOpen className="h-6 w-6 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Completados</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">{trainingStats.completedModules}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="h-6 w-6 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">En Progreso</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{trainingStats.inProgress}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Horas</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{trainingStats.totalHours}h</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Rating Promedio</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">{trainingStats.avgRating.toFixed(1)}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<Award className="h-6 w-6 text-yellow-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar módulos de formación..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.value}
|
||||
onClick={() => setSelectedCategory(category.value)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedCategory === category.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
|
||||
}`}
|
||||
>
|
||||
{category.label} ({category.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Training Modules */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{filteredModules.map((module) => (
|
||||
<Card key={module.id} className="p-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-16 h-16 bg-[var(--bg-quaternary)] rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-8 h-8 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{module.title}</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-2">{module.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{module.completed ? (
|
||||
<Badge variant="green">Completado</Badge>
|
||||
) : module.progress > 0 ? (
|
||||
<Badge variant="blue">En Progreso</Badge>
|
||||
) : (
|
||||
<Badge variant="gray">No Iniciado</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm text-[var(--text-secondary)] mb-3">
|
||||
<span className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
{module.duration}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<BookOpen className="w-4 h-4 mr-1" />
|
||||
{module.lessons} lecciones
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1" />
|
||||
{module.instructor}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Badge variant={getDifficultyColor(module.difficulty)}>
|
||||
{getDifficultyLabel(module.difficulty)}
|
||||
</Badge>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Award className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-sm font-medium text-[var(--text-secondary)]">{module.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between text-sm text-[var(--text-secondary)] mb-1">
|
||||
<span>Progreso</span>
|
||||
<span>{module.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${module.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Topics */}
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)] mb-2">Temas incluidos:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{module.topics.map((topic, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded-full"
|
||||
>
|
||||
{topic}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{module.progress > 0 ? 'Continuar' : 'Comenzar'}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
Ver Detalles
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Team Progress Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Progreso del Equipo</h3>
|
||||
<div className="space-y-4">
|
||||
{teamProgress.map((member, index) => (
|
||||
<div key={index} className="border-b border-[var(--border-primary)] pb-4 last:border-b-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-[var(--text-primary)]">{member.name}</p>
|
||||
<p className="text-sm text-[var(--text-tertiary)]">{member.role}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{member.completedModules}/{member.totalModules}
|
||||
</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Award className="w-3 h-3 text-yellow-500" />
|
||||
<span className="text-xs text-[var(--text-tertiary)]">{member.certificates}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-[var(--text-secondary)] mb-2">
|
||||
Actual: {member.currentModule}
|
||||
</p>
|
||||
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-blue-600 h-1.5 rounded-full"
|
||||
style={{ width: `${member.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Certificaciones</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3 p-3 bg-green-50 border border-green-200 rounded-lg">
|
||||
<Award className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--color-success)]">Certificado en Seguridad</p>
|
||||
<p className="text-xs text-[var(--color-success)]">Válido hasta: Dic 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 p-3 bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg">
|
||||
<Award className="w-5 h-5 text-[var(--color-info)]" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--color-info)]">Certificado Básico</p>
|
||||
<p className="text-xs text-[var(--color-info)]">Completado: Ene 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" className="w-full">
|
||||
Ver Todos los Certificados
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrainingPage;
|
||||
454
frontend/src/pages/app/settings/training/TrainingPage.tsx.backup
Normal file
454
frontend/src/pages/app/settings/training/TrainingPage.tsx.backup
Normal file
@@ -0,0 +1,454 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BookOpen, Play, CheckCircle, Clock, Users, Award, Download, Search } from 'lucide-react';
|
||||
import { Button, Card, Badge, Input } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const TrainingPage: React.FC = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const trainingModules = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Fundamentos de Panadería',
|
||||
description: 'Conceptos básicos de elaboración de pan y técnicas fundamentales',
|
||||
category: 'basics',
|
||||
duration: '2.5 horas',
|
||||
lessons: 12,
|
||||
difficulty: 'beginner',
|
||||
progress: 100,
|
||||
completed: true,
|
||||
rating: 4.8,
|
||||
instructor: 'Chef María González',
|
||||
topics: ['Ingredientes básicos', 'Proceso de amasado', 'Fermentación', 'Horneado'],
|
||||
thumbnail: '/training/bread-basics.jpg'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Técnicas Avanzadas de Bollería',
|
||||
description: 'Elaboración de croissants, hojaldre y productos fermentados complejos',
|
||||
category: 'advanced',
|
||||
duration: '4 horas',
|
||||
lessons: 18,
|
||||
difficulty: 'advanced',
|
||||
progress: 65,
|
||||
completed: false,
|
||||
rating: 4.9,
|
||||
instructor: 'Chef Pierre Laurent',
|
||||
topics: ['Masas laminadas', 'Temperaturas críticas', 'Técnicas de plegado', 'Control de calidad'],
|
||||
thumbnail: '/training/pastry-advanced.jpg'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Seguridad e Higiene Alimentaria',
|
||||
description: 'Protocolos de seguridad, HACCP y normativas sanitarias',
|
||||
category: 'safety',
|
||||
duration: '1.5 horas',
|
||||
lessons: 8,
|
||||
difficulty: 'beginner',
|
||||
progress: 0,
|
||||
completed: false,
|
||||
rating: 4.7,
|
||||
instructor: 'Dr. Ana Rodríguez',
|
||||
topics: ['HACCP', 'Limpieza y desinfección', 'Control de temperaturas', 'Trazabilidad'],
|
||||
thumbnail: '/training/food-safety.jpg'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Gestión de Inventarios',
|
||||
description: 'Optimización de stock, control de mermas y gestión de proveedores',
|
||||
category: 'management',
|
||||
duration: '3 horas',
|
||||
lessons: 15,
|
||||
difficulty: 'intermediate',
|
||||
progress: 30,
|
||||
completed: false,
|
||||
rating: 4.6,
|
||||
instructor: 'Carlos Fernández',
|
||||
topics: ['Rotación de stock', 'Punto de reorden', 'Análisis ABC', 'Negociación con proveedores'],
|
||||
thumbnail: '/training/inventory-mgmt.jpg'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Atención al Cliente',
|
||||
description: 'Técnicas de venta, resolución de quejas y fidelización',
|
||||
category: 'sales',
|
||||
duration: '2 horas',
|
||||
lessons: 10,
|
||||
difficulty: 'beginner',
|
||||
progress: 85,
|
||||
completed: false,
|
||||
rating: 4.8,
|
||||
instructor: 'Isabel Torres',
|
||||
topics: ['Técnicas de venta', 'Comunicación efectiva', 'Manejo de quejas', 'Up-selling'],
|
||||
thumbnail: '/training/customer-service.jpg'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Innovación en Productos',
|
||||
description: 'Desarrollo de nuevos productos, tendencias y análisis de mercado',
|
||||
category: 'innovation',
|
||||
duration: '3.5 horas',
|
||||
lessons: 16,
|
||||
difficulty: 'intermediate',
|
||||
progress: 0,
|
||||
completed: false,
|
||||
rating: 4.7,
|
||||
instructor: 'Chef Daniel Ramos',
|
||||
topics: ['Análisis de tendencias', 'Prototipado', 'Testing de mercado', 'Costos de producción'],
|
||||
thumbnail: '/training/product-innovation.jpg'
|
||||
}
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todos', count: trainingModules.length },
|
||||
{ value: 'basics', label: 'Básicos', count: trainingModules.filter(m => m.category === 'basics').length },
|
||||
{ value: 'advanced', label: 'Avanzado', count: trainingModules.filter(m => m.category === 'advanced').length },
|
||||
{ value: 'safety', label: 'Seguridad', count: trainingModules.filter(m => m.category === 'safety').length },
|
||||
{ value: 'management', label: 'Gestión', count: trainingModules.filter(m => m.category === 'management').length },
|
||||
{ value: 'sales', label: 'Ventas', count: trainingModules.filter(m => m.category === 'sales').length },
|
||||
{ value: 'innovation', label: 'Innovación', count: trainingModules.filter(m => m.category === 'innovation').length }
|
||||
];
|
||||
|
||||
const teamProgress = [
|
||||
{
|
||||
name: 'María González',
|
||||
role: 'Gerente',
|
||||
completedModules: 4,
|
||||
totalModules: 6,
|
||||
currentModule: 'Gestión de Inventarios',
|
||||
progress: 75,
|
||||
certificates: 3
|
||||
},
|
||||
{
|
||||
name: 'Carlos Rodríguez',
|
||||
role: 'Panadero',
|
||||
completedModules: 2,
|
||||
totalModules: 4,
|
||||
currentModule: 'Técnicas Avanzadas de Bollería',
|
||||
progress: 65,
|
||||
certificates: 2
|
||||
},
|
||||
{
|
||||
name: 'Ana Martínez',
|
||||
role: 'Cajera',
|
||||
completedModules: 3,
|
||||
totalModules: 4,
|
||||
currentModule: 'Atención al Cliente',
|
||||
progress: 85,
|
||||
certificates: 2
|
||||
}
|
||||
];
|
||||
|
||||
const trainingStats = {
|
||||
totalModules: trainingModules.length,
|
||||
completedModules: trainingModules.filter(m => m.completed).length,
|
||||
inProgress: trainingModules.filter(m => m.progress > 0 && !m.completed).length,
|
||||
totalHours: trainingModules.reduce((sum, m) => sum + parseFloat(m.duration), 0),
|
||||
avgRating: trainingModules.reduce((sum, m) => sum + m.rating, 0) / trainingModules.length
|
||||
};
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'beginner': return 'green';
|
||||
case 'intermediate': return 'yellow';
|
||||
case 'advanced': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getDifficultyLabel = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'beginner': return 'Principiante';
|
||||
case 'intermediate': return 'Intermedio';
|
||||
case 'advanced': return 'Avanzado';
|
||||
default: return difficulty;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredModules = trainingModules.filter(module => {
|
||||
const matchesCategory = selectedCategory === 'all' || module.category === selectedCategory;
|
||||
const matchesSearch = module.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
module.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Centro de Formación"
|
||||
description="Módulos de capacitación y desarrollo profesional para el equipo"
|
||||
action={
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Certificados
|
||||
</Button>
|
||||
<Button>
|
||||
Nuevo Módulo
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Training Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Módulos Totales</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{trainingStats.totalModules}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<BookOpen className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Completados</p>
|
||||
<p className="text-3xl font-bold text-green-600">{trainingStats.completedModules}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">En Progreso</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{trainingStats.inProgress}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Horas</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{trainingStats.totalHours}h</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Rating Promedio</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">{trainingStats.avgRating.toFixed(1)}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<Award className="h-6 w-6 text-yellow-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar módulos de formación..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.value}
|
||||
onClick={() => setSelectedCategory(category.value)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedCategory === category.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{category.label} ({category.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Training Modules */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{filteredModules.map((module) => (
|
||||
<Card key={module.id} className="p-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-8 h-8 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{module.title}</h3>
|
||||
<p className="text-sm text-gray-600 mb-2">{module.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{module.completed ? (
|
||||
<Badge variant="green">Completado</Badge>
|
||||
) : module.progress > 0 ? (
|
||||
<Badge variant="blue">En Progreso</Badge>
|
||||
) : (
|
||||
<Badge variant="gray">No Iniciado</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-600 mb-3">
|
||||
<span className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
{module.duration}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<BookOpen className="w-4 h-4 mr-1" />
|
||||
{module.lessons} lecciones
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-1" />
|
||||
{module.instructor}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Badge variant={getDifficultyColor(module.difficulty)}>
|
||||
{getDifficultyLabel(module.difficulty)}
|
||||
</Badge>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Award className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-sm font-medium text-gray-700">{module.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between text-sm text-gray-600 mb-1">
|
||||
<span>Progreso</span>
|
||||
<span>{module.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${module.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Topics */}
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Temas incluidos:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{module.topics.map((topic, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full"
|
||||
>
|
||||
{topic}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{module.progress > 0 ? 'Continuar' : 'Comenzar'}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
Ver Detalles
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Team Progress Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Progreso del Equipo</h3>
|
||||
<div className="space-y-4">
|
||||
{teamProgress.map((member, index) => (
|
||||
<div key={index} className="border-b border-gray-200 pb-4 last:border-b-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{member.name}</p>
|
||||
<p className="text-sm text-gray-500">{member.role}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{member.completedModules}/{member.totalModules}
|
||||
</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Award className="w-3 h-3 text-yellow-500" />
|
||||
<span className="text-xs text-gray-500">{member.certificates}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-600 mb-2">
|
||||
Actual: {member.currentModule}
|
||||
</p>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-blue-600 h-1.5 rounded-full"
|
||||
style={{ width: `${member.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Certificaciones</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3 p-3 bg-green-50 border border-green-200 rounded-lg">
|
||||
<Award className="w-5 h-5 text-green-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-800">Certificado en Seguridad</p>
|
||||
<p className="text-xs text-green-600">Válido hasta: Dic 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<Award className="w-5 h-5 text-blue-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-800">Certificado Básico</p>
|
||||
<p className="text-xs text-blue-600">Completado: Ene 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" className="w-full">
|
||||
Ver Todos los Certificados
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrainingPage;
|
||||
1
frontend/src/pages/app/settings/training/index.ts
Normal file
1
frontend/src/pages/app/settings/training/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as TrainingPage } from './TrainingPage';
|
||||
Reference in New Issue
Block a user