New Frontend
This commit is contained in:
402
frontend/src/pages/settings/GeneralSettingsPage.tsx
Normal file
402
frontend/src/pages/settings/GeneralSettingsPage.tsx
Normal file
@@ -0,0 +1,402 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Globe,
|
||||
Clock,
|
||||
DollarSign,
|
||||
MapPin,
|
||||
Save,
|
||||
ChevronRight,
|
||||
Mail,
|
||||
Smartphone
|
||||
} from 'lucide-react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface GeneralSettings {
|
||||
language: string;
|
||||
timezone: string;
|
||||
currency: string;
|
||||
bakeryName: string;
|
||||
bakeryAddress: string;
|
||||
businessType: string;
|
||||
operatingHours: {
|
||||
open: string;
|
||||
close: string;
|
||||
};
|
||||
operatingDays: number[];
|
||||
}
|
||||
|
||||
interface NotificationSettings {
|
||||
emailNotifications: boolean;
|
||||
smsNotifications: boolean;
|
||||
dailyReports: boolean;
|
||||
weeklyReports: boolean;
|
||||
forecastAlerts: boolean;
|
||||
stockAlerts: boolean;
|
||||
orderReminders: boolean;
|
||||
}
|
||||
|
||||
const GeneralSettingsPage: React.FC = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
const { currentTenant } = useSelector((state: RootState) => state.tenant);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [settings, setSettings] = useState<GeneralSettings>({
|
||||
language: 'es',
|
||||
timezone: 'Europe/Madrid',
|
||||
currency: 'EUR',
|
||||
bakeryName: currentTenant?.name || 'Mi Panadería',
|
||||
bakeryAddress: currentTenant?.address || '',
|
||||
businessType: currentTenant?.business_type || 'individual',
|
||||
operatingHours: {
|
||||
open: currentTenant?.settings?.operating_hours?.open || '07:00',
|
||||
close: currentTenant?.settings?.operating_hours?.close || '20:00'
|
||||
},
|
||||
operatingDays: currentTenant?.settings?.operating_days || [1, 2, 3, 4, 5, 6]
|
||||
});
|
||||
|
||||
const [notifications, setNotifications] = useState<NotificationSettings>({
|
||||
emailNotifications: true,
|
||||
smsNotifications: false,
|
||||
dailyReports: true,
|
||||
weeklyReports: true,
|
||||
forecastAlerts: true,
|
||||
stockAlerts: true,
|
||||
orderReminders: true
|
||||
});
|
||||
|
||||
const handleSaveSettings = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
toast.success('Configuración guardada exitosamente');
|
||||
} catch (error) {
|
||||
toast.error('Error al guardar la configuración');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const dayLabels = ['L', 'M', 'X', 'J', 'V', 'S', 'D'];
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
<div className="space-y-8">
|
||||
{/* Business Information */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Información del Negocio</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre de la panadería
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.bakeryName}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, bakeryName: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tipo de negocio
|
||||
</label>
|
||||
<select
|
||||
value={settings.businessType}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, businessType: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="individual">Panadería Individual</option>
|
||||
<option value="central_workshop">Obrador Central</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Dirección
|
||||
</label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={settings.bakeryAddress}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, bakeryAddress: e.target.value }))}
|
||||
placeholder="Calle Mayor, 123, Madrid"
|
||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Operating Hours */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Horarios de Operación</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Hora de apertura
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={settings.operatingHours.open}
|
||||
onChange={(e) => setSettings(prev => ({
|
||||
...prev,
|
||||
operatingHours: { ...prev.operatingHours, open: e.target.value }
|
||||
}))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Hora de cierre
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={settings.operatingHours.close}
|
||||
onChange={(e) => setSettings(prev => ({
|
||||
...prev,
|
||||
operatingHours: { ...prev.operatingHours, close: e.target.value }
|
||||
}))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Días de operación
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2 sm:grid-cols-7">
|
||||
{dayLabels.map((day, index) => (
|
||||
<label key={day} className="flex items-center justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.operatingDays.includes(index + 1)}
|
||||
onChange={(e) => {
|
||||
const dayNum = index + 1;
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
operatingDays: e.target.checked
|
||||
? [...prev.operatingDays, dayNum]
|
||||
: prev.operatingDays.filter(d => d !== dayNum)
|
||||
}));
|
||||
}}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-10 h-10 bg-gray-200 peer-checked:bg-primary-500 peer-checked:text-white rounded-lg flex items-center justify-center font-medium text-sm cursor-pointer transition-colors">
|
||||
{day}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Regional Settings */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Configuración Regional</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<Globe className="inline h-4 w-4 mr-1" />
|
||||
Idioma
|
||||
</label>
|
||||
<select
|
||||
value={settings.language}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, language: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="es">Español</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<Clock className="inline h-4 w-4 mr-1" />
|
||||
Zona horaria
|
||||
</label>
|
||||
<select
|
||||
value={settings.timezone}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, timezone: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="Europe/Madrid">Europa/Madrid (CET)</option>
|
||||
<option value="Europe/London">Europa/Londres (GMT)</option>
|
||||
<option value="America/New_York">América/Nueva York (EST)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<DollarSign className="inline h-4 w-4 mr-1" />
|
||||
Moneda
|
||||
</label>
|
||||
<select
|
||||
value={settings.currency}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, currency: e.target.value }))}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="EUR">Euro (€)</option>
|
||||
<option value="USD">Dólar americano ($)</option>
|
||||
<option value="GBP">Libra esterlina (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notification Settings */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Notificaciones</h3>
|
||||
|
||||
{/* Notification Channels */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<Mail className="h-5 w-5 text-gray-600 mr-3" />
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Notificaciones por Email</div>
|
||||
<div className="text-sm text-gray-500">Recibe alertas y reportes por correo</div>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notifications.emailNotifications}
|
||||
onChange={(e) => setNotifications(prev => ({
|
||||
...prev,
|
||||
emailNotifications: e.target.checked
|
||||
}))}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<Smartphone className="h-5 w-5 text-gray-600 mr-3" />
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Notificaciones SMS</div>
|
||||
<div className="text-sm text-gray-500">Alertas urgentes por mensaje de texto</div>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notifications.smsNotifications}
|
||||
onChange={(e) => setNotifications(prev => ({
|
||||
...prev,
|
||||
smsNotifications: e.target.checked
|
||||
}))}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notification Types */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-gray-900">Tipos de Notificación</h4>
|
||||
{[
|
||||
{ key: 'dailyReports', label: 'Reportes Diarios', desc: 'Resumen diario de ventas y predicciones' },
|
||||
{ key: 'weeklyReports', label: 'Reportes Semanales', desc: 'Análisis semanal de rendimiento' },
|
||||
{ key: 'forecastAlerts', label: 'Alertas de Predicción', desc: 'Cambios significativos en demanda' },
|
||||
{ key: 'stockAlerts', label: 'Alertas de Stock', desc: 'Inventario bajo o próximos vencimientos' },
|
||||
{ key: 'orderReminders', label: 'Recordatorios de Pedidos', desc: 'Próximas entregas y fechas límite' }
|
||||
].map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between p-3 border border-gray-200 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{item.label}</div>
|
||||
<div className="text-sm text-gray-500">{item.desc}</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notifications[item.key as keyof NotificationSettings] as boolean}
|
||||
onChange={(e) => setNotifications(prev => ({
|
||||
...prev,
|
||||
[item.key]: e.target.checked
|
||||
}))}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Export */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-6">Exportar Datos</h3>
|
||||
<div className="space-y-3">
|
||||
<button className="w-full p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Exportar todas las predicciones</div>
|
||||
<div className="text-sm text-gray-500">Descargar historial completo en CSV</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Exportar datos de ventas</div>
|
||||
<div className="text-sm text-gray-500">Historial de ventas y análisis</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="w-full p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Exportar configuración</div>
|
||||
<div className="text-sm text-gray-500">Respaldo de toda la configuración</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleSaveSettings}
|
||||
disabled={isLoading}
|
||||
className="inline-flex items-center px-6 py-3 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Guardando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Guardar Cambios
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSettingsPage;
|
||||
Reference in New Issue
Block a user