402 lines
18 KiB
TypeScript
402 lines
18 KiB
TypeScript
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; |