Add alerts ssytems to the frontend

This commit is contained in:
Urtzi Alfaro
2025-09-21 17:35:36 +02:00
parent 57fd2f22f0
commit f08667150d
17 changed files with 2086 additions and 786 deletions

View File

@@ -8,6 +8,7 @@ import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
import { useTranslation } from 'react-i18next';
import CommunicationPreferences, { type NotificationPreferences } from './CommunicationPreferences';
interface ProfileFormData {
first_name: string;
@@ -24,58 +25,6 @@ interface PasswordData {
confirmPassword: string;
}
interface NotificationPreferences {
notifications: {
inventory: {
app: boolean;
email: boolean;
sms: boolean;
frequency: string;
};
sales: {
app: boolean;
email: boolean;
sms: boolean;
frequency: string;
};
production: {
app: boolean;
email: boolean;
sms: boolean;
frequency: string;
};
system: {
app: boolean;
email: boolean;
sms: boolean;
frequency: string;
};
marketing: {
app: boolean;
email: boolean;
sms: boolean;
frequency: string;
};
};
global: {
doNotDisturb: boolean;
quietHours: {
enabled: boolean;
start: string;
end: string;
};
language: string;
timezone: string;
soundEnabled: boolean;
vibrationEnabled: boolean;
};
channels: {
email: string;
phone: string;
slack: boolean;
webhook: string;
};
}
const ProfilePage: React.FC = () => {
const user = useAuthUser();
@@ -120,19 +69,11 @@ const ProfilePage: React.FC = () => {
timezone: profile.timezone || 'Europe/Madrid'
});
// Update preferences with profile data
setPreferences(prev => ({
// Update notification preferences with profile data
setNotificationPreferences(prev => ({
...prev,
global: {
...prev.global,
language: profile.language || 'es',
timezone: profile.timezone || 'Europe/Madrid'
},
channels: {
...prev.channels,
email: profile.email || '',
phone: profile.phone || ''
}
language: profile.language || 'es',
timezone: profile.timezone || 'Europe/Madrid'
}));
}
}, [profile]);
@@ -151,58 +92,23 @@ const ProfilePage: React.FC = () => {
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [preferences, setPreferences] = useState<NotificationPreferences>({
notifications: {
inventory: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
sales: {
app: true,
email: true,
sms: false,
frequency: 'hourly'
},
production: {
app: true,
email: false,
sms: true,
frequency: 'immediate'
},
system: {
app: true,
email: true,
sms: false,
frequency: 'daily'
},
marketing: {
app: false,
email: true,
sms: false,
frequency: 'weekly'
}
},
global: {
doNotDisturb: false,
quietHours: {
enabled: false,
start: '22:00',
end: '07:00'
},
language: 'es',
timezone: 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: '',
phone: '',
slack: false,
webhook: ''
}
const [notificationPreferences, setNotificationPreferences] = useState<NotificationPreferences>({
email_enabled: true,
email_alerts: true,
email_marketing: false,
email_reports: true,
whatsapp_enabled: false,
whatsapp_alerts: false,
whatsapp_reports: false,
push_enabled: true,
push_alerts: true,
push_reports: false,
quiet_hours_start: '22:00',
quiet_hours_end: '08:00',
timezone: 'Europe/Madrid',
digest_frequency: 'daily',
max_emails_per_day: 10,
language: 'es'
});
const languageOptions = [
@@ -316,152 +222,43 @@ const ProfilePage: React.FC = () => {
}
};
// Communication Preferences handlers
const categories = [
{
id: 'inventory',
name: 'Inventario',
description: 'Alertas de stock, reposiciones y vencimientos',
icon: '📦'
},
{
id: 'sales',
name: 'Ventas',
description: 'Pedidos, transacciones y reportes de ventas',
icon: '💰'
},
{
id: 'production',
name: 'Producción',
description: 'Hornadas, calidad y tiempos de producción',
icon: '🍞'
},
{
id: 'system',
name: 'Sistema',
description: 'Actualizaciones, mantenimiento y errores',
icon: '⚙️'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Campañas, promociones y análisis',
icon: '📢'
}
];
const frequencies = [
{ value: 'immediate', label: 'Inmediato' },
{ value: 'hourly', label: 'Cada hora' },
{ value: 'daily', label: 'Diario' },
{ value: 'weekly', label: 'Semanal' }
];
const handleNotificationChange = (category: string, channel: string, value: boolean) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
[channel]: value
}
}
}));
setHasPreferencesChanges(true);
};
const handleFrequencyChange = (category: string, frequency: string) => {
setPreferences(prev => ({
...prev,
notifications: {
...prev.notifications,
[category]: {
...prev.notifications[category as keyof typeof prev.notifications],
frequency
}
}
}));
setHasPreferencesChanges(true);
};
const handleGlobalChange = (setting: string, value: any) => {
setPreferences(prev => ({
...prev,
global: {
...prev.global,
[setting]: value
}
}));
setHasPreferencesChanges(true);
};
const handleChannelChange = (channel: string, value: string | boolean) => {
setPreferences(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: value
}
}));
setHasPreferencesChanges(true);
};
const handleSavePreferences = async () => {
// Notification preferences handlers
const handleSaveNotificationPreferences = async (preferences: NotificationPreferences) => {
try {
await updateProfileMutation.mutateAsync({
language: preferences.global.language,
timezone: preferences.global.timezone,
phone: preferences.channels.phone,
notification_preferences: preferences.notifications
language: preferences.language,
timezone: preferences.timezone,
notification_preferences: preferences
});
addToast('Preferencias guardadas correctamente', 'success');
setNotificationPreferences(preferences);
setHasPreferencesChanges(false);
} catch (error) {
addToast('Error al guardar las preferencias', 'error');
throw error; // Let the component handle the error display
}
};
const handleResetPreferences = () => {
const handleResetNotificationPreferences = () => {
if (profile) {
setPreferences({
notifications: {
inventory: { app: true, email: false, sms: true, frequency: 'immediate' },
sales: { app: true, email: true, sms: false, frequency: 'hourly' },
production: { app: true, email: false, sms: true, frequency: 'immediate' },
system: { app: true, email: true, sms: false, frequency: 'daily' },
marketing: { app: false, email: true, sms: false, frequency: 'weekly' }
},
global: {
doNotDisturb: false,
quietHours: { enabled: false, start: '22:00', end: '07:00' },
language: profile.language || 'es',
timezone: profile.timezone || 'Europe/Madrid',
soundEnabled: true,
vibrationEnabled: true
},
channels: {
email: profile.email || '',
phone: profile.phone || '',
slack: false,
webhook: ''
}
setNotificationPreferences({
email_enabled: true,
email_alerts: true,
email_marketing: false,
email_reports: true,
whatsapp_enabled: false,
whatsapp_alerts: false,
whatsapp_reports: false,
push_enabled: true,
push_alerts: true,
push_reports: false,
quiet_hours_start: '22:00',
quiet_hours_end: '08:00',
timezone: profile.timezone || 'Europe/Madrid',
digest_frequency: 'daily',
max_emails_per_day: 10,
language: profile.language || 'es'
});
}
setHasPreferencesChanges(false);
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'app':
return <Bell className="w-4 h-4" />;
case 'email':
return <Mail className="w-4 h-4" />;
case 'sms':
return <Smartphone className="w-4 h-4" />;
default:
return <MessageSquare className="w-4 h-4" />;
setHasPreferencesChanges(false);
}
};
@@ -784,209 +581,15 @@ const ProfilePage: React.FC = () => {
{/* Communication Preferences Tab */}
{activeTab === 'preferences' && (
<>
{/* Action Buttons */}
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={handleResetPreferences}>
<RotateCcw className="w-4 h-4 mr-2" />
Restaurar
</Button>
<Button onClick={handleSavePreferences} disabled={!hasPreferencesChanges}>
<Save className="w-4 h-4 mr-2" />
Guardar Cambios
</Button>
</div>
{/* Global Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Configuración General</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.doNotDisturb}
onChange={(e) => handleGlobalChange('doNotDisturb', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">No molestar</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Silencia todas las notificaciones</p>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.global.soundEnabled}
onChange={(e) => handleGlobalChange('soundEnabled', e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Sonidos</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Reproducir sonidos de notificación</p>
</div>
</div>
<div>
<label className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={preferences.global.quietHours.enabled}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
enabled: e.target.checked
})}
className="rounded border-[var(--border-secondary)]"
/>
<span className="text-sm font-medium text-[var(--text-secondary)]">Horas silenciosas</span>
</label>
{preferences.global.quietHours.enabled && (
<div className="flex space-x-4 ml-6">
<div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Desde</label>
<input
type="time"
value={preferences.global.quietHours.start}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
start: 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">Hasta</label>
<input
type="time"
value={preferences.global.quietHours.end}
onChange={(e) => handleGlobalChange('quietHours', {
...preferences.global.quietHours,
end: e.target.value
})}
className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm"
/>
</div>
</div>
)}
</div>
</div>
</Card>
{/* Channel Settings */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Canales de Comunicación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Email</label>
<input
type="email"
value={preferences.channels.email}
onChange={(e) => handleChannelChange('email', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="tu-email@ejemplo.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Teléfono (SMS)</label>
<input
type="tel"
value={preferences.channels.phone}
onChange={(e) => handleChannelChange('phone', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="+34 600 123 456"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Webhook URL</label>
<input
type="url"
value={preferences.channels.webhook}
onChange={(e) => handleChannelChange('webhook', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
placeholder="https://tu-webhook.com/notifications"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">URL para recibir notificaciones JSON</p>
</div>
</div>
</Card>
{/* Category Preferences */}
<div className="space-y-4">
{categories.map((category) => {
const categoryPrefs = preferences.notifications[category.id as keyof typeof preferences.notifications];
return (
<Card key={category.id} className="p-6">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">{category.name}</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">{category.description}</p>
<div className="space-y-4">
{/* Channel toggles */}
<div>
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Canales</h4>
<div className="flex space-x-6">
{['app', 'email', 'sms'].map((channel) => (
<label key={channel} className="flex items-center space-x-2">
<input
type="checkbox"
checked={categoryPrefs[channel as keyof typeof categoryPrefs] as boolean}
onChange={(e) => handleNotificationChange(category.id, channel, e.target.checked)}
className="rounded border-[var(--border-secondary)]"
/>
<div className="flex items-center space-x-1">
{getChannelIcon(channel)}
<span className="text-sm text-[var(--text-secondary)] capitalize">{channel}</span>
</div>
</label>
))}
</div>
</div>
{/* Frequency */}
<div>
<h4 className="text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia</h4>
<select
value={categoryPrefs.frequency}
onChange={(e) => handleFrequencyChange(category.id, e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md text-sm"
>
{frequencies.map((freq) => (
<option key={freq.value} value={freq.value}>
{freq.label}
</option>
))}
</select>
</div>
</div>
</div>
</div>
</Card>
);
})}
</div>
{/* Save Changes Banner */}
{hasPreferencesChanges && (
<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={handleResetPreferences}>
Descartar
</Button>
<Button size="sm" className="bg-blue-700 hover:bg-blue-800" onClick={handleSavePreferences}>
Guardar
</Button>
</div>
</div>
)}
</>
<CommunicationPreferences
userEmail={profileData.email}
userPhone={profileData.phone}
userLanguage={profileData.language}
userTimezone={profileData.timezone}
onSave={handleSaveNotificationPreferences}
onReset={handleResetNotificationPreferences}
hasChanges={hasPreferencesChanges}
/>
)}
{/* Subscription Tab */}