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

@@ -1,6 +1,7 @@
import React, { useState, useMemo } from 'react';
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { statusColors } from '../../../../styles/colors';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { LoadingSpinner } from '../../../../components/shared';
import { PageHeader } from '../../../../components/layout';
@@ -82,13 +83,32 @@ const ProductionPage: React.FC = () => {
const isUrgent = priority === ProductionPriorityEnum.URGENT;
const isCritical = status === ProductionStatusEnum.FAILED || (status === ProductionStatusEnum.PENDING && isUrgent);
// Map production statuses to global status colors
const getStatusColorForProduction = (status: ProductionStatusEnum) => {
// Handle both uppercase (backend) and lowercase (frontend) status values
const normalizedStatus = status.toLowerCase();
switch (normalizedStatus) {
case 'pending':
return statusColors.pending.primary;
case 'in_progress':
return statusColors.inProgress.primary;
case 'completed':
return statusColors.completed.primary;
case 'cancelled':
case 'failed':
return statusColors.cancelled.primary;
case 'on_hold':
return statusColors.pending.primary;
case 'quality_check':
return statusColors.inProgress.primary;
default:
return statusColors.other.primary;
}
};
return {
color: getStatusColor(
status === ProductionStatusEnum.COMPLETED ? 'completed' :
status === ProductionStatusEnum.PENDING ? 'pending' :
status === ProductionStatusEnum.CANCELLED || status === ProductionStatusEnum.FAILED ? 'cancelled' :
'in_progress'
),
color: getStatusColorForProduction(status),
text: productionEnums.getProductionStatusLabel(status),
icon: Icon,
isCritical,

View File

@@ -0,0 +1,703 @@
import React, { useState, useEffect } from 'react';
import { Card } from '../../../../components/ui/Card';
import { Button } from '../../../../components/ui/Button';
import { Input } from '../../../../components/ui/Input';
import { Select } from '../../../../components/ui/Select';
import { Badge } from '../../../../components/ui/Badge';
import {
Bell,
Mail,
MessageSquare,
Smartphone,
Save,
RotateCcw,
Clock,
Globe,
Volume2,
VolumeX,
AlertTriangle,
TrendingUp,
Megaphone,
FileText,
Moon,
Sun,
Settings
} from 'lucide-react';
import { useToast } from '../../../../hooks/ui/useToast';
// Backend-aligned preference types
export interface NotificationPreferences {
// Email preferences
email_enabled: boolean;
email_alerts: boolean;
email_marketing: boolean;
email_reports: boolean;
// WhatsApp preferences
whatsapp_enabled: boolean;
whatsapp_alerts: boolean;
whatsapp_reports: boolean;
// Push notification preferences
push_enabled: boolean;
push_alerts: boolean;
push_reports: boolean;
// Timing preferences
quiet_hours_start: string;
quiet_hours_end: string;
timezone: string;
// Frequency preferences
digest_frequency: 'none' | 'daily' | 'weekly';
max_emails_per_day: number;
// Language preference
language: string;
}
interface CommunicationPreferencesProps {
userEmail?: string;
userPhone?: string;
userLanguage?: string;
userTimezone?: string;
onSave: (preferences: NotificationPreferences) => Promise<void>;
onReset: () => void;
hasChanges: boolean;
}
const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
userEmail = '',
userPhone = '',
userLanguage = 'es',
userTimezone = 'Europe/Madrid',
onSave,
onReset,
hasChanges
}) => {
const { addToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [preferences, setPreferences] = useState<NotificationPreferences>({
// Email preferences
email_enabled: true,
email_alerts: true,
email_marketing: false,
email_reports: true,
// WhatsApp preferences
whatsapp_enabled: false,
whatsapp_alerts: false,
whatsapp_reports: false,
// Push notification preferences
push_enabled: true,
push_alerts: true,
push_reports: false,
// Timing preferences
quiet_hours_start: '22:00',
quiet_hours_end: '08:00',
timezone: userTimezone,
// Frequency preferences
digest_frequency: 'daily',
max_emails_per_day: 10,
// Language preference
language: userLanguage
});
const [contactInfo, setContactInfo] = useState({
email: userEmail,
phone: userPhone
});
// Language options
const languageOptions = [
{ value: 'es', label: 'Español' },
{ value: 'en', label: 'English' }
];
// Timezone options
const timezoneOptions = [
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
{ value: 'Atlantic/Canary', label: 'Canarias (WET/WEST)' },
{ value: 'Europe/London', label: 'Londres (GMT/BST)' },
{ value: 'Europe/Paris', label: 'París (CET/CEST)' }
];
// Digest frequency options
const digestOptions = [
{ value: 'none', label: 'Sin resumen' },
{ value: 'daily', label: 'Resumen diario' },
{ value: 'weekly', label: 'Resumen semanal' }
];
// Max emails per day options
const maxEmailsOptions = [
{ value: 5, label: '5 emails/día' },
{ value: 10, label: '10 emails/día' },
{ value: 20, label: '20 emails/día' },
{ value: 50, label: '50 emails/día' },
{ value: 100, label: 'Sin límite' }
];
const handlePreferenceChange = (key: keyof NotificationPreferences, value: any) => {
setPreferences(prev => ({
...prev,
[key]: value
}));
};
const handleContactChange = (field: 'email' | 'phone', value: string) => {
setContactInfo(prev => ({
...prev,
[field]: value
}));
};
const handleSave = async () => {
try {
setIsLoading(true);
await onSave(preferences);
addToast('Preferencias guardadas correctamente', 'success');
} catch (error) {
addToast('Error al guardar las preferencias', 'error');
} finally {
setIsLoading(false);
}
};
const getChannelIcon = (channel: string) => {
switch (channel) {
case 'push':
return <Bell className="w-4 h-4 text-blue-500" />;
case 'email':
return <Mail className="w-4 h-4 text-green-500" />;
case 'whatsapp':
return <MessageSquare className="w-4 h-4 text-green-600" />;
case 'sms':
return <Smartphone className="w-4 h-4 text-purple-500" />;
default:
return <Bell className="w-4 h-4" />;
}
};
const getCategoryIcon = (category: string) => {
switch (category) {
case 'alerts':
return <AlertTriangle className="w-5 h-5 text-red-500" />;
case 'reports':
return <TrendingUp className="w-5 h-5 text-blue-500" />;
case 'marketing':
return <Megaphone className="w-5 h-5 text-purple-500" />;
default:
return <FileText className="w-5 h-5 text-gray-500" />;
}
};
const isQuietHoursEnabled = () => {
return preferences.quiet_hours_start !== preferences.quiet_hours_end;
};
return (
<div className="space-y-6">
{/* Action Buttons */}
<div className="flex justify-end gap-3">
<Button
variant="outline"
onClick={onReset}
disabled={!hasChanges || isLoading}
className="flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
Restaurar
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={!hasChanges || isLoading}
isLoading={isLoading}
loadingText="Guardando..."
className="flex items-center gap-2"
>
<Save className="w-4 h-4" />
Guardar Cambios
</Button>
</div>
{/* Contact Information */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-500/10 rounded-lg">
<Settings className="w-5 h-5 text-blue-500" />
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
Información de Contacto
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Configura tus canales de comunicación
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input
label="Email"
type="email"
value={contactInfo.email}
onChange={(e) => handleContactChange('email', e.target.value)}
leftIcon={<Mail className="w-4 h-4" />}
placeholder="tu-email@ejemplo.com"
/>
<Input
label="Teléfono (WhatsApp)"
type="tel"
value={contactInfo.phone}
onChange={(e) => handleContactChange('phone', e.target.value)}
leftIcon={<MessageSquare className="w-4 h-4" />}
placeholder="+34 600 123 456"
/>
</div>
</Card>
{/* Global Settings */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-500/10 rounded-lg">
<Globe className="w-5 h-5 text-purple-500" />
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
Configuración General
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Idioma, zona horaria y configuración de silencio
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Select
label="Idioma"
options={languageOptions}
value={preferences.language}
onChange={(value) => handlePreferenceChange('language', value)}
leftIcon={<Globe className="w-4 h-4" />}
/>
<Select
label="Zona Horaria"
options={timezoneOptions}
value={preferences.timezone}
onChange={(value) => handlePreferenceChange('timezone', value)}
leftIcon={<Clock className="w-4 h-4" />}
/>
<Select
label="Frecuencia de Resumen"
options={digestOptions}
value={preferences.digest_frequency}
onChange={(value) => handlePreferenceChange('digest_frequency', value)}
leftIcon={<FileText className="w-4 h-4" />}
/>
</div>
{/* Quiet Hours */}
<div className="mt-6 p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center gap-3 mb-4">
{isQuietHoursEnabled() ? (
<Moon className="w-5 h-5 text-indigo-500" />
) : (
<Sun className="w-5 h-5 text-yellow-500" />
)}
<div>
<h4 className="font-medium text-[var(--text-primary)]">Horas Silenciosas</h4>
<p className="text-sm text-[var(--text-secondary)]">
Sin notificaciones durante este periodo
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Desde
</label>
<input
type="time"
value={preferences.quiet_hours_start}
onChange={(e) => handlePreferenceChange('quiet_hours_start', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Hasta
</label>
<input
type="time"
value={preferences.quiet_hours_end}
onChange={(e) => handlePreferenceChange('quiet_hours_end', e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
</div>
</Card>
{/* Notification Categories */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Alerts */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
{getCategoryIcon('alerts')}
<div>
<h3 className="font-semibold text-[var(--text-primary)]">Alertas</h3>
<p className="text-sm text-[var(--text-secondary)]">
Notificaciones urgentes e importantes
</p>
</div>
</div>
<div className="space-y-4">
{/* Push Alerts */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('push')}
<span className="text-sm font-medium text-[var(--text-primary)]">
App Push
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.push_alerts}
onChange={(e) => handlePreferenceChange('push_alerts', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
{/* Email Alerts */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('email')}
<span className="text-sm font-medium text-[var(--text-primary)]">
Email
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.email_alerts}
onChange={(e) => handlePreferenceChange('email_alerts', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
{/* WhatsApp Alerts */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('whatsapp')}
<span className="text-sm font-medium text-[var(--text-primary)]">
WhatsApp
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.whatsapp_alerts}
onChange={(e) => handlePreferenceChange('whatsapp_alerts', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
</div>
</Card>
{/* Reports */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
{getCategoryIcon('reports')}
<div>
<h3 className="font-semibold text-[var(--text-primary)]">Reportes</h3>
<p className="text-sm text-[var(--text-secondary)]">
Informes y análisis del negocio
</p>
</div>
</div>
<div className="space-y-4">
{/* Push Reports */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('push')}
<span className="text-sm font-medium text-[var(--text-primary)]">
App Push
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.push_reports}
onChange={(e) => handlePreferenceChange('push_reports', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
{/* Email Reports */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('email')}
<span className="text-sm font-medium text-[var(--text-primary)]">
Email
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.email_reports}
onChange={(e) => handlePreferenceChange('email_reports', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
{/* WhatsApp Reports */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('whatsapp')}
<span className="text-sm font-medium text-[var(--text-primary)]">
WhatsApp
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.whatsapp_reports}
onChange={(e) => handlePreferenceChange('whatsapp_reports', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
</div>
</Card>
{/* Marketing */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
{getCategoryIcon('marketing')}
<div>
<h3 className="font-semibold text-[var(--text-primary)]">Marketing</h3>
<p className="text-sm text-[var(--text-secondary)]">
Promociones y campañas
</p>
</div>
</div>
<div className="space-y-4">
{/* Email Marketing */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getChannelIcon('email')}
<span className="text-sm font-medium text-[var(--text-primary)]">
Email
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.email_marketing}
onChange={(e) => handlePreferenceChange('email_marketing', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
<div className="text-xs text-[var(--text-tertiary)]">
Las preferencias de marketing se aplicarán solo a contenido promocional opcional
</div>
</div>
</Card>
</div>
{/* Master Channel Controls */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-orange-500/10 rounded-lg">
<Volume2 className="w-5 h-5 text-orange-500" />
</div>
<div>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
Control de Canales
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Activar o desactivar canales de comunicación completamente
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Email Master Control */}
<div className="p-4 border border-[var(--border-primary)] rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
{getChannelIcon('email')}
<span className="font-medium text-[var(--text-primary)]">Email</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.email_enabled}
onChange={(e) => handlePreferenceChange('email_enabled', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
<div className="mb-3">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
Límite diario
</label>
<select
value={preferences.max_emails_per_day}
onChange={(e) => handlePreferenceChange('max_emails_per_day', parseInt(e.target.value))}
disabled={!preferences.email_enabled}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:opacity-50"
>
{maxEmailsOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<p className="text-xs text-[var(--text-tertiary)]">
{preferences.email_enabled ? 'Activado' : 'Desactivado'}
</p>
</div>
{/* Push Master Control */}
<div className="p-4 border border-[var(--border-primary)] rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
{getChannelIcon('push')}
<span className="font-medium text-[var(--text-primary)]">Push</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.push_enabled}
onChange={(e) => handlePreferenceChange('push_enabled', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
<p className="text-xs text-[var(--text-tertiary)]">
{preferences.push_enabled ? 'Notificaciones push activadas' : 'Notificaciones push desactivadas'}
</p>
</div>
{/* WhatsApp Master Control */}
<div className="p-4 border border-[var(--border-primary)] rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
{getChannelIcon('whatsapp')}
<span className="font-medium text-[var(--text-primary)]">WhatsApp</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={preferences.whatsapp_enabled}
onChange={(e) => handlePreferenceChange('whatsapp_enabled', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-[var(--bg-tertiary)] peer-focus:outline-none 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
{contactInfo.phone ? (
<p className="text-xs text-[var(--text-secondary)]">
{contactInfo.phone}
</p>
) : (
<p className="text-xs text-[var(--color-warning)]">
Configura tu teléfono arriba
</p>
)}
<p className="text-xs text-[var(--text-tertiary)] mt-1">
{preferences.whatsapp_enabled ? 'Activado' : 'Desactivado'}
</p>
</div>
</div>
</Card>
{/* Status Summary */}
<Card className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)]">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Badge
variant={preferences.email_enabled || preferences.push_enabled || preferences.whatsapp_enabled ? 'success' : 'warning'}
size="sm"
>
{preferences.email_enabled || preferences.push_enabled || preferences.whatsapp_enabled ? 'Activo' : 'Limitado'}
</Badge>
<span className="text-sm text-[var(--text-secondary)]">
Estado de notificaciones
</span>
</div>
<div className="text-xs text-[var(--text-tertiary)]">
{[
preferences.email_enabled && 'Email',
preferences.push_enabled && 'Push',
preferences.whatsapp_enabled && 'WhatsApp'
].filter(Boolean).join(' • ') || 'Sin canales activos'}
</div>
</div>
</Card>
{/* Save Changes Banner */}
{hasChanges && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-[var(--color-primary)] text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-4 z-50">
<span className="text-sm font-medium">Tienes cambios sin guardar</span>
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
className="bg-white text-[var(--color-primary)] hover:bg-gray-100 border-white"
onClick={onReset}
disabled={isLoading}
>
Descartar
</Button>
<Button
size="sm"
variant="secondary"
className="bg-[var(--color-primary-dark)] hover:bg-[var(--color-primary-darker)] text-white border-transparent"
onClick={handleSave}
disabled={isLoading}
isLoading={isLoading}
>
Guardar
</Button>
</div>
</div>
)}
</div>
);
};
export default CommunicationPreferences;

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 */}