Add alerts ssytems to the frontend
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user