Kaynağa Gözat

fixed notifications because wtf was that before

Matthew Trejo 2 ay önce
ebeveyn
işleme
ca9afa5c49

+ 1 - 1
src/app/api/account/update/route.ts

@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
     }
 
     // Preparar datos de actualización
-    const updateData: any = {}
+    const updateData: Record<string, unknown> = {}
 
     // Actualizar email
     if (email) {

+ 11 - 11
src/app/appointments/[id]/page.tsx

@@ -35,7 +35,7 @@ import {
 } from "lucide-react";
 import { format } from "date-fns";
 import { es } from "date-fns/locale";
-import { toast } from "sonner";
+import { notifications } from "@/lib/notifications";
 import type { Appointment } from "@/types/appointments";
 import { canJoinMeeting, getAppointmentTimeStatus } from "@/utils/appointments";
 
@@ -74,7 +74,7 @@ export default function AppointmentDetailPage({ params }: PageProps) {
         const data: Appointment = await response.json();
         setAppointment(data);
       } catch (error) {
-        toast.error("Error al cargar la cita");
+        notifications.appointments.loadError();
         console.error(error);
       } finally {
         setLoading(false);
@@ -148,9 +148,9 @@ export default function AppointmentDetailPage({ params }: PageProps) {
       const updated: Appointment = await response.json();
       setAppointment(updated);
       setApproveDialog(false);
-      toast.success("Cita aprobada exitosamente");
+      notifications.appointments.approved();
     } catch (error) {
-      toast.error(error instanceof Error ? error.message : "Error al aprobar la cita");
+      notifications.appointments.approveError(error instanceof Error ? error.message : undefined);
       console.error(error);
     } finally {
       setActionLoading(false);
@@ -174,9 +174,9 @@ export default function AppointmentDetailPage({ params }: PageProps) {
       setAppointment(updated);
       setRejectDialog(false);
       setMotivoRechazo("");
-      toast.success("Cita rechazada");
+      notifications.appointments.rejected();
     } catch (error) {
-      toast.error("Error al rechazar la cita");
+      notifications.appointments.rejectError();
       console.error(error);
     } finally {
       setActionLoading(false);
@@ -192,10 +192,10 @@ export default function AppointmentDetailPage({ params }: PageProps) {
 
       if (!response.ok) throw new Error("Error al cancelar la cita");
 
-      toast.success("Cita cancelada exitosamente");
+      notifications.appointments.cancelled();
       router.push("/appointments");
     } catch (error) {
-      toast.error("Error al cancelar la cita");
+      notifications.appointments.cancelError();
       console.error(error);
       setActionLoading(false);
     }
@@ -218,7 +218,7 @@ export default function AppointmentDetailPage({ params }: PageProps) {
       // Redirigir a la sala de Jitsi
       router.push(`/appointments/${appointment.id}/meet`);
     } catch (error) {
-      toast.error(error instanceof Error ? error.message : "Error al iniciar videollamada");
+      notifications.appointments.videocallError(error instanceof Error ? error.message : undefined);
       console.error(error);
     } finally {
       setActionLoading(false);
@@ -236,9 +236,9 @@ export default function AppointmentDetailPage({ params }: PageProps) {
 
       const updated: Appointment = await response.json();
       setAppointment(updated);
-      toast.success("Cita marcada como completada");
+      notifications.appointments.completed();
     } catch (error) {
-      toast.error("Error al completar la cita");
+      notifications.appointments.completeError();
       console.error(error);
     } finally {
       setActionLoading(false);

+ 4 - 4
src/app/auth/login/page.tsx

@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input"
 import { Label } from "@/components/ui/label"
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 import { User, Lock, AlertCircle } from "lucide-react"
-import { toast } from "sonner"
+import { notifications } from "@/lib/notifications"
 
 export default function LoginPage() {
   const [username, setUsername] = useState("")
@@ -58,14 +58,14 @@ export default function LoginPage() {
       })
 
       if (result?.error) {
-        toast.error("Credenciales inválidas")
+        notifications.auth.loginError()
       } else {
-        toast.success("Inicio de sesión exitoso")
+        notifications.auth.loginSuccess()
         router.push("/dashboard")
       }
     } catch (error) {
       console.error("Error en login:", error)
-      toast.error("Error al iniciar sesión")
+      notifications.auth.loginGeneralError()
     } finally {
       setLoading(false)
     }

+ 6 - 6
src/app/auth/register/page.tsx

@@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"
 import { Label } from "@/components/ui/label"
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 import { User, Lock, Mail, UserPlus, AlertCircle } from "lucide-react"
-import { toast } from "sonner"
+import { notifications } from "@/lib/notifications"
 
 export default function RegisterPage() {
   const [formData, setFormData] = useState({
@@ -32,12 +32,12 @@ export default function RegisterPage() {
     e.preventDefault()
     
     if (formData.password !== formData.confirmPassword) {
-      toast.error("Las contraseñas no coinciden")
+      notifications.auth.passwordMismatch()
       return
     }
 
     if (formData.password.length < 6) {
-      toast.error("La contraseña debe tener al menos 6 caracteres")
+      notifications.auth.passwordTooShort()
       return
     }
 
@@ -60,14 +60,14 @@ export default function RegisterPage() {
       const data = await response.json()
 
       if (response.ok) {
-        toast.success("Cuenta creada exitosamente")
+        notifications.auth.registerSuccess()
         router.push("/auth/login")
       } else {
-        toast.error(data.error || "Error al crear la cuenta")
+        notifications.auth.registerError(data.error)
       }
     } catch (error) {
       console.error("Error en registro:", error)
-      toast.error("Error al crear la cuenta")
+      notifications.auth.registerError()
     } finally {
       setLoading(false)
     }

+ 147 - 0
src/app/toast-demo/README.md

@@ -0,0 +1,147 @@
+# 🎮 Toast Demo Page
+
+## Descripción
+
+Página interactiva de demostración del sistema centralizado de notificaciones (toasts) de Ani Assistant.
+
+## 🔒 Acceso
+
+**Solo disponible para usuarios con rol `ADMIN`**
+
+- **URL**: `/toast-demo`
+- **Ubicación en sidebar**: Administración → Toast Demo
+
+## ✨ Funcionalidades
+
+### 1. Módulos de Notificaciones
+
+La página organiza todas las notificaciones disponibles por categorías:
+
+#### 📅 Appointments
+- Cita creada, aprobada, rechazada, cancelada, completada
+- Errores: al cargar, crear, aprobar, iniciar videollamada
+
+#### 👤 Profile
+- Perfil actualizado, imagen eliminada
+- Errores: actualización, imagen inválida, imagen muy grande
+
+#### 🔐 Authentication
+- Login/registro exitoso
+- Errores: credenciales inválidas, contraseñas, validación
+
+#### 📄 Records
+- Generación, guardado, descarga de reportes
+- Copiado al portapapeles
+- Errores en generación
+
+#### ❤️ Patients
+- Asignación/desasignación de pacientes
+- Actualización de pacientes
+- Errores en gestión
+
+#### 💬 Chat
+- Nueva consulta iniciada
+- Advertencia de última consulta
+
+#### ⚠️ Validation
+- Campo requerido
+- Formato inválido
+
+### 2. Notificaciones Genéricas
+
+Sección para crear notificaciones personalizadas con:
+- Mensaje personalizable
+- Descripción opcional
+- 4 tipos: Success, Error, Info, Warning
+
+### 3. Helpers Avanzados
+
+Demostración de helpers para operaciones asíncronas:
+
+#### `withLoadingToast`
+```typescript
+await withLoadingToast(
+  operacionAsincrona(),
+  "Procesando...",
+  "¡Completado!"
+)
+```
+
+#### `promiseToast`
+```typescript
+promiseToast.execute(operacionAsincrona(), {
+  loading: "Cargando...",
+  success: "Éxito",
+  error: "Error"
+})
+```
+
+## 🎨 Diseño
+
+- Layout responsivo con grid de 2 columnas (1 en móvil)
+- Iconos visuales para cada tipo de notificación
+- Colores diferenciados por estado
+- Cards organizadas por módulo
+- Separadores visuales entre acciones exitosas y errores
+
+## 🛠️ Uso para Desarrollo
+
+### Durante Testing
+1. Verifica que todas las notificaciones se muestren correctamente
+2. Prueba la consistencia del tono y lenguaje
+3. Valida los tiempos de duración
+
+### Para Nuevos Desarrolladores
+1. Familiarízate con los tipos de notificaciones disponibles
+2. Aprende la sintaxis del sistema centralizado
+3. Ve ejemplos en vivo de cada módulo
+
+### Para Demostración
+1. Muestra las capacidades del sistema a stakeholders
+2. Demuestra la consistencia UX
+3. Presenta los diferentes estados y variantes
+
+## 📝 Agregar Nuevas Notificaciones
+
+Si agregas notificaciones al sistema, actualiza esta página:
+
+1. Abre `src/app/toast-demo/page.tsx`
+2. Encuentra el módulo correspondiente o crea uno nuevo
+3. Agrega un botón con el pattern:
+
+```tsx
+<Button 
+  onClick={() => notifications.tuModulo.tuAccion()} 
+  variant="outline" 
+  className="w-full justify-start"
+>
+  <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+  Descripción de la Acción
+</Button>
+```
+
+## 🔐 Seguridad
+
+La página tiene protección a nivel de componente:
+
+```typescript
+if (!session || session.user.role !== "ADMIN") {
+  redirect("/dashboard")
+}
+```
+
+Solo usuarios autenticados con rol `ADMIN` pueden acceder.
+
+## 📚 Referencias
+
+- Sistema centralizado: `src/lib/notifications.ts`
+- Documentación completa: `docs/TOAST_REFACTORING.md`
+- Biblioteca de toasts: [Sonner](https://sonner.emilkowal.ski/)
+
+## 🎯 Próximas Mejoras
+
+- [ ] Filtro por tipo de notificación
+- [ ] Historial de toasts mostrados
+- [ ] Configuración de duración personalizada
+- [ ] Tema oscuro/claro preview
+- [ ] Export de configuraciones

+ 603 - 0
src/app/toast-demo/page.tsx

@@ -0,0 +1,603 @@
+"use client"
+
+import { useState } from "react"
+import { useSession } from "next-auth/react"
+import { redirect } from "next/navigation"
+import AuthenticatedLayout from "@/components/AuthenticatedLayout"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Separator } from "@/components/ui/separator"
+import { Badge } from "@/components/ui/badge"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { 
+  CheckCircle2, 
+  XCircle, 
+  Info, 
+  AlertTriangle,
+  Loader2,
+  Calendar,
+  User,
+  FileText,
+  Heart,
+  Settings,
+  Sparkles
+} from "lucide-react"
+import { notifications, withLoadingToast, promiseToast } from "@/lib/notifications"
+
+export default function ToastDemoPage() {
+  const { data: session, status } = useSession()
+  const [customMessage, setCustomMessage] = useState("")
+  const [customDescription, setCustomDescription] = useState("")
+  const [isLoading, setIsLoading] = useState(false)
+
+  if (status === "loading") {
+    return (
+      <AuthenticatedLayout>
+        <div className="flex items-center justify-center min-h-screen">
+          <Loader2 className="h-8 w-8 animate-spin text-primary" />
+        </div>
+      </AuthenticatedLayout>
+    )
+  }
+
+  if (!session || session.user.role !== "ADMIN") {
+    redirect("/dashboard")
+  }
+
+  // Simulación de operación async
+  const mockAsyncOperation = () => {
+    return new Promise((resolve) => {
+      setTimeout(() => resolve("Operación completada"), 2000)
+    })
+  }
+
+  const mockFailingOperation = () => {
+    return new Promise((_, reject) => {
+      setTimeout(() => reject(new Error("Operación falló")), 2000)
+    })
+  }
+
+  const handleWithLoadingToast = async () => {
+    setIsLoading(true)
+    try {
+      await withLoadingToast(
+        mockAsyncOperation(),
+        "Procesando...",
+        "¡Operación completada con éxito!"
+      )
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  const handlePromiseToast = () => {
+    promiseToast.execute(mockAsyncOperation(), {
+      loading: "Cargando datos...",
+      success: "Datos cargados correctamente",
+      error: "Error al cargar datos"
+    })
+  }
+
+  return (
+    <AuthenticatedLayout>
+      <div className="container mx-auto px-4 py-8 max-w-6xl">
+        {/* Header */}
+        <div className="mb-8">
+          <div className="flex items-center gap-3 mb-2">
+            <Sparkles className="h-8 w-8 text-primary" />
+            <h1 className="text-3xl font-bold">Toast Demo</h1>
+            <Badge variant="outline" className="ml-auto">Admin Only</Badge>
+          </div>
+          <p className="text-muted-foreground">
+            Demostración del sistema centralizado de notificaciones. Prueba todos los tipos de toasts disponibles.
+          </p>
+        </div>
+
+        <div className="grid gap-6 md:grid-cols-2">
+          {/* Appointments Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <Calendar className="h-5 w-5 text-primary" />
+                <CardTitle>Appointments</CardTitle>
+              </div>
+              <CardDescription>Notificaciones relacionadas con citas</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.appointments.created()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Cita Creada
+              </Button>
+              <Button 
+                onClick={() => notifications.appointments.approved()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Cita Aprobada
+              </Button>
+              <Button 
+                onClick={() => notifications.appointments.rejected()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Cita Rechazada
+              </Button>
+              <Button 
+                onClick={() => notifications.appointments.cancelled()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Cita Cancelada
+              </Button>
+              <Button 
+                onClick={() => notifications.appointments.completed()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Cita Completada
+              </Button>
+              <Separator />
+              <Button 
+                onClick={() => notifications.appointments.loadError()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error al Cargar
+              </Button>
+              <Button 
+                onClick={() => notifications.appointments.createError("Fecha no disponible")} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error al Crear (custom)
+              </Button>
+              <Button 
+                onClick={() => notifications.appointments.videocallError("Cámara no disponible")} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error Videollamada
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Profile Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <User className="h-5 w-5 text-primary" />
+                <CardTitle>Profile</CardTitle>
+              </div>
+              <CardDescription>Notificaciones de perfil de usuario</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.profile.updated()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Perfil Actualizado
+              </Button>
+              <Button 
+                onClick={() => notifications.profile.imageRemoved()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Imagen Eliminada
+              </Button>
+              <Separator />
+              <Button 
+                onClick={() => notifications.profile.updateError("Conexión perdida")} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error al Actualizar
+              </Button>
+              <Button 
+                onClick={() => notifications.profile.invalidImage()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Imagen Inválida
+              </Button>
+              <Button 
+                onClick={() => notifications.profile.imageTooLarge()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Imagen Muy Grande
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Auth Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <Settings className="h-5 w-5 text-primary" />
+                <CardTitle>Authentication</CardTitle>
+              </div>
+              <CardDescription>Notificaciones de autenticación</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.auth.loginSuccess()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Login Exitoso
+              </Button>
+              <Button 
+                onClick={() => notifications.auth.registerSuccess()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Registro Exitoso
+              </Button>
+              <Separator />
+              <Button 
+                onClick={() => notifications.auth.loginError()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error de Login
+              </Button>
+              <Button 
+                onClick={() => notifications.auth.passwordMismatch()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Contraseñas No Coinciden
+              </Button>
+              <Button 
+                onClick={() => notifications.auth.passwordTooShort()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Contraseña Muy Corta
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Records Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <FileText className="h-5 w-5 text-primary" />
+                <CardTitle>Records</CardTitle>
+              </div>
+              <CardDescription>Notificaciones de reportes médicos</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.records.generating()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <Info className="h-4 w-4 mr-2 text-blue-500" />
+                Generando Reporte
+              </Button>
+              <Button 
+                onClick={() => notifications.records.generated()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Reporte Generado
+              </Button>
+              <Button 
+                onClick={() => notifications.records.saved()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Reporte Guardado
+              </Button>
+              <Button 
+                onClick={() => notifications.records.downloaded()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Reporte Descargado
+              </Button>
+              <Button 
+                onClick={() => notifications.records.copied()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Copiado al Portapapeles
+              </Button>
+              <Separator />
+              <Button 
+                onClick={() => notifications.records.generateError()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error al Generar
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Patients Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <Heart className="h-5 w-5 text-primary" />
+                <CardTitle>Patients</CardTitle>
+              </div>
+              <CardDescription>Notificaciones de gestión de pacientes</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.patients.assigned()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Paciente Asignado
+              </Button>
+              <Button 
+                onClick={() => notifications.patients.unassigned()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Paciente Desasignado
+              </Button>
+              <Button 
+                onClick={() => notifications.patients.updated()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Paciente Actualizado
+              </Button>
+              <Separator />
+              <Button 
+                onClick={() => notifications.patients.loadError()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error al Cargar
+              </Button>
+              <Button 
+                onClick={() => notifications.patients.assignError("Doctor no disponible")} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Error al Asignar
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Chat Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <Info className="h-5 w-5 text-primary" />
+                <CardTitle>Chat</CardTitle>
+              </div>
+              <CardDescription>Notificaciones del chatbot</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.chat.newConsultation()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                Nueva Consulta
+              </Button>
+              <Button 
+                onClick={() => notifications.chat.lastMessageWarning()} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <AlertTriangle className="h-4 w-4 mr-2 text-yellow-500" />
+                Última Consulta (Warning)
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Validation Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <AlertTriangle className="h-5 w-5 text-primary" />
+                <CardTitle>Validation</CardTitle>
+              </div>
+              <CardDescription>Notificaciones de validación</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-2">
+              <Button 
+                onClick={() => notifications.validation.requiredField("Nombre")} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Campo Requerido
+              </Button>
+              <Button 
+                onClick={() => notifications.validation.invalidFormat("Email")} 
+                variant="outline" 
+                className="w-full justify-start"
+              >
+                <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                Formato Inválido
+              </Button>
+            </CardContent>
+          </Card>
+
+          {/* Generic Toasts */}
+          <Card>
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <Sparkles className="h-5 w-5 text-primary" />
+                <CardTitle>Generic</CardTitle>
+              </div>
+              <CardDescription>Notificaciones genéricas personalizables</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <div className="space-y-2">
+                <Label htmlFor="customMessage">Mensaje</Label>
+                <Input
+                  id="customMessage"
+                  placeholder="Ej: Operación exitosa"
+                  value={customMessage}
+                  onChange={(e) => setCustomMessage(e.target.value)}
+                />
+              </div>
+              <div className="space-y-2">
+                <Label htmlFor="customDescription">Descripción (opcional)</Label>
+                <Input
+                  id="customDescription"
+                  placeholder="Ej: Los cambios se guardaron"
+                  value={customDescription}
+                  onChange={(e) => setCustomDescription(e.target.value)}
+                />
+              </div>
+              <div className="grid grid-cols-2 gap-2">
+                <Button 
+                  onClick={() => notifications.generic.success(
+                    customMessage || "Éxito",
+                    customDescription || undefined
+                  )} 
+                  variant="outline"
+                  className="w-full"
+                >
+                  <CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
+                  Success
+                </Button>
+                <Button 
+                  onClick={() => notifications.generic.error(
+                    customMessage || "Error",
+                    customDescription || undefined
+                  )} 
+                  variant="outline"
+                  className="w-full"
+                >
+                  <XCircle className="h-4 w-4 mr-2 text-red-500" />
+                  Error
+                </Button>
+                <Button 
+                  onClick={() => notifications.generic.info(
+                    customMessage || "Información",
+                    customDescription || undefined
+                  )} 
+                  variant="outline"
+                  className="w-full"
+                >
+                  <Info className="h-4 w-4 mr-2 text-blue-500" />
+                  Info
+                </Button>
+                <Button 
+                  onClick={() => notifications.generic.warning(
+                    customMessage || "Advertencia",
+                    customDescription || undefined
+                  )} 
+                  variant="outline"
+                  className="w-full"
+                >
+                  <AlertTriangle className="h-4 w-4 mr-2 text-yellow-500" />
+                  Warning
+                </Button>
+              </div>
+            </CardContent>
+          </Card>
+
+          {/* Advanced Helpers */}
+          <Card className="md:col-span-2">
+            <CardHeader>
+              <div className="flex items-center gap-2">
+                <Loader2 className="h-5 w-5 text-primary" />
+                <CardTitle>Advanced Helpers</CardTitle>
+              </div>
+              <CardDescription>Helpers avanzados para operaciones asíncronas</CardDescription>
+            </CardHeader>
+            <CardContent>
+              <div className="grid gap-4 md:grid-cols-2">
+                <div className="space-y-2">
+                  <h4 className="font-semibold">withLoadingToast</h4>
+                  <p className="text-sm text-muted-foreground mb-2">
+                    Muestra un toast de loading que se convierte en success al completar
+                  </p>
+                  <Button 
+                    onClick={handleWithLoadingToast}
+                    disabled={isLoading}
+                    className="w-full"
+                  >
+                    {isLoading ? (
+                      <>
+                        <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+                        Procesando...
+                      </>
+                    ) : (
+                      "Probar withLoadingToast"
+                    )}
+                  </Button>
+                </div>
+                <div className="space-y-2">
+                  <h4 className="font-semibold">promiseToast</h4>
+                  <p className="text-sm text-muted-foreground mb-2">
+                    Ejecuta una promesa con estados loading/success/error automáticos
+                  </p>
+                  <Button 
+                    onClick={handlePromiseToast}
+                    variant="outline"
+                    className="w-full"
+                  >
+                    Probar promiseToast
+                  </Button>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* Footer Info */}
+        <Card className="mt-6 border-dashed">
+          <CardContent className="pt-6">
+            <div className="flex items-start gap-3">
+              <Info className="h-5 w-5 text-blue-500 mt-0.5" />
+              <div className="space-y-1">
+                <p className="text-sm font-medium">Sobre este sistema</p>
+                <p className="text-sm text-muted-foreground">
+                  Este sistema de notificaciones está centralizado en <code className="bg-muted px-1 py-0.5 rounded text-xs">src/lib/notifications.ts</code>.
+                  Todas las notificaciones son type-safe, consistentes y fáciles de mantener. 
+                  Para más información, consulta la documentación en <code className="bg-muted px-1 py-0.5 rounded text-xs">docs/TOAST_REFACTORING.md</code>.
+                </p>
+              </div>
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    </AuthenticatedLayout>
+  )
+}

+ 3 - 3
src/components/account/ProfileImageSection.tsx

@@ -4,7 +4,7 @@ import Image from "next/image"
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 import { Label } from "@/components/ui/label"
 import { User, Camera, Upload, Trash2 } from "lucide-react"
-import { toast } from "sonner"
+import { notifications } from "@/lib/notifications"
 
 interface ProfileImageSectionProps {
   profileImage: string | null
@@ -24,13 +24,13 @@ export default function ProfileImageSection({
     if (file) {
       // Validar tipo de archivo
       if (!file.type.startsWith('image/')) {
-        toast.error("Por favor selecciona un archivo de imagen")
+        notifications.profile.invalidImage()
         return
       }
       
       // Validar tamaño (máximo 5MB)
       if (file.size > 5 * 1024 * 1024) {
-        toast.error("La imagen debe ser menor a 5MB")
+        notifications.profile.imageTooLarge()
         return
       }
 

+ 5 - 18
src/components/chatbot/AppointmentModalFromChat.tsx

@@ -13,7 +13,7 @@ import {
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import { Calendar, AlertCircle } from "lucide-react";
-import { useToast } from "@/hooks/use-toast";
+import { notifications } from "@/lib/notifications";
 
 interface AppointmentModalFromChatProps {
   open: boolean;
@@ -28,17 +28,12 @@ export const AppointmentModalFromChat = ({
 }: AppointmentModalFromChatProps) => {
   const [motivoConsulta, setMotivoConsulta] = useState("");
   const [isSubmitting, setIsSubmitting] = useState(false);
-  const { toast } = useToast();
 
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault();
 
     if (!motivoConsulta.trim()) {
-      toast({
-        title: "Campo requerido",
-        description: "Por favor describe el motivo de tu consulta",
-        variant: "destructive",
-      });
+      notifications.validation.requiredField("motivo de consulta");
       return;
     }
 
@@ -58,22 +53,14 @@ export const AppointmentModalFromChat = ({
         throw new Error(error.error || "Error al crear cita");
       }
 
-      toast({
-        title: "¡Cita solicitada!",
-        description: "Un médico revisará tu solicitud y te asignará una fecha pronto.",
-      });
+      notifications.appointments.created();
 
       setMotivoConsulta("");
       onClose();
       onSuccess?.();
     } catch (error) {
-      const message =
-        error instanceof Error ? error.message : "No se pudo crear la cita";
-      toast({
-        title: "Error",
-        description: message,
-        variant: "destructive",
-      });
+      const message = error instanceof Error ? error.message : undefined;
+      notifications.appointments.createError(message);
     } finally {
       setIsSubmitting(false);
     }

+ 3 - 3
src/components/chatbot/ReportModal.tsx

@@ -6,7 +6,7 @@ import {
   DialogTitle,
 } from "@/components/ui/dialog";
 import { FileText, Copy, Download } from "lucide-react";
-import { toast } from "sonner";
+import { notifications } from "@/lib/notifications";
 
 interface ReportModalProps {
   isOpen: boolean;
@@ -21,7 +21,7 @@ export const ReportModal = ({
 }: ReportModalProps) => {
   const copyReport = () => {
     navigator.clipboard.writeText(generatedReport);
-    toast.success("Reporte copiado al portapapeles");
+    notifications.records.copied();
   };
 
   const downloadReport = () => {
@@ -34,7 +34,7 @@ export const ReportModal = ({
     a.click();
     document.body.removeChild(a);
     URL.revokeObjectURL(url);
-    toast.success("Reporte descargado");
+    notifications.records.downloaded();
   };
 
   return (

+ 7 - 1
src/components/sidebar/SidebarNavigation.tsx

@@ -11,7 +11,8 @@ import {
   ChevronDown,
   ChevronRight,
   Home,
-  Calendar
+  Calendar,
+  Sparkles
 } from "lucide-react"
 import { COLOR_PALETTE } from "@/utils/palette"
 import { useAppointmentsBadge } from "@/hooks/useAppointmentsBadge"
@@ -95,6 +96,11 @@ export default function SidebarNavigation({ onItemClick, isCollapsed = false }:
               title: "Todos los Reportes",
               href: "/records",
               icon: FileText
+            },
+            {
+              title: "Toast Demo",
+              href: "/toast-demo",
+              icon: Sparkles
             }
           ]
         }

+ 0 - 25
src/hooks/use-toast.ts

@@ -1,25 +0,0 @@
-import { toast as sonnerToast } from "sonner";
-
-export const useToast = () => {
-  return {
-    toast: ({
-      title,
-      description,
-      variant,
-    }: {
-      title: string;
-      description?: string;
-      variant?: "default" | "destructive";
-    }) => {
-      if (variant === "destructive") {
-        sonnerToast.error(title, {
-          description,
-        });
-      } else {
-        sonnerToast.success(title, {
-          description,
-        });
-      }
-    },
-  };
-};

+ 9 - 9
src/hooks/useAccountForm.ts

@@ -1,7 +1,7 @@
 import { useState, useEffect, useCallback } from "react"
 import { useSession } from "next-auth/react"
 import { useRouter } from "next/navigation"
-import { toast } from "sonner"
+import { notifications } from "@/lib/notifications"
 import { useProfileImageContext } from "@/contexts/ProfileImageContext"
 
 interface FormData {
@@ -154,14 +154,14 @@ export function useAccountForm() {
           profileImage: null
         })
         
-        toast.success("Imagen de perfil eliminada correctamente")
+        notifications.profile.imageRemoved()
       } else {
         const data = await response.json()
-        toast.error(data.error || "Error al eliminar la imagen")
+        notifications.profile.imageRemoveError(data.error)
       }
     } catch (error) {
       console.error("Error eliminando imagen:", error)
-      toast.error("Error al eliminar la imagen de perfil")
+      notifications.profile.imageRemoveError()
     } finally {
       setLoadingImage(false)
     }
@@ -174,13 +174,13 @@ export function useAccountForm() {
     try {
       // Validaciones
       if (formData.newPassword && formData.newPassword !== formData.confirmPassword) {
-        toast.error("Las contraseñas no coinciden")
+        notifications.auth.passwordMismatch()
         setLoading(false)
         return
       }
       
       if (formData.newPassword && formData.newPassword.length < 6) {
-        toast.error("La contraseña debe tener al menos 6 caracteres")
+        notifications.auth.passwordTooShort()
         setLoading(false)
         return
       }
@@ -241,7 +241,7 @@ export function useAccountForm() {
       const data = await response.json()
 
       if (response.ok) {
-        toast.success("Perfil actualizado correctamente.")
+        notifications.profile.updated()
         
         // Limpiar contraseñas
         setFormData(prev => ({
@@ -280,11 +280,11 @@ export function useAccountForm() {
           }, 500)
         }
       } else {
-        toast.error(data.error || "Error al actualizar el perfil")
+        notifications.profile.updateError(data.error)
       }
     } catch (error) {
       console.error("Error updating profile:", error)
-      toast.error("Error al actualizar el perfil")
+      notifications.profile.updateError()
     } finally {
       setLoading(false)
     }

+ 12 - 45
src/hooks/useAppointments.ts

@@ -1,13 +1,12 @@
 "use client";
 
 import { useState, useEffect } from "react";
-import { useToast } from "@/hooks/use-toast";
+import { notifications } from "@/lib/notifications";
 import type { Appointment, CreateAppointmentInput } from "@/types/appointments";
 
 export const useAppointments = () => {
   const [appointments, setAppointments] = useState<Appointment[]>([]);
   const [isLoading, setIsLoading] = useState(true);
-  const { toast } = useToast();
 
   const fetchAppointments = async (estado?: string) => {
     try {
@@ -23,11 +22,7 @@ export const useAppointments = () => {
       setAppointments(data);
     } catch (error) {
       console.error("Error fetching appointments:", error);
-      toast({
-        title: "Error",
-        description: "No se pudieron cargar las citas",
-        variant: "destructive",
-      });
+      notifications.appointments.loadError();
     } finally {
       setIsLoading(false);
     }
@@ -48,20 +43,13 @@ export const useAppointments = () => {
 
       const newAppointment: Appointment = await response.json();
       
-      toast({
-        title: "¡Cita solicitada!",
-        description: "Un médico revisará tu solicitud pronto",
-      });
+      notifications.appointments.created();
 
       await fetchAppointments();
       return newAppointment;
     } catch (error) {
-      const message = error instanceof Error ? error.message : "No se pudo crear la cita";
-      toast({
-        title: "Error",
-        description: message,
-        variant: "destructive",
-      });
+      const message = error instanceof Error ? error.message : undefined;
+      notifications.appointments.createError(message);
       throw error;
     }
   };
@@ -82,19 +70,12 @@ export const useAppointments = () => {
         throw new Error(error.error || "Error al aprobar cita");
       }
 
-      toast({
-        title: "Cita aprobada",
-        description: "El paciente ha sido notificado",
-      });
+      notifications.appointments.approved();
 
       await fetchAppointments();
     } catch (error) {
-      const message = error instanceof Error ? error.message : "No se pudo aprobar la cita";
-      toast({
-        title: "Error",
-        description: message,
-        variant: "destructive",
-      });
+      const message = error instanceof Error ? error.message : undefined;
+      notifications.appointments.approveError(message);
       throw error;
     }
   };
@@ -109,18 +90,11 @@ export const useAppointments = () => {
 
       if (!response.ok) throw new Error("Error al rechazar cita");
 
-      toast({
-        title: "Cita rechazada",
-        description: "El paciente ha sido notificado",
-      });
+      notifications.appointments.rejected();
 
       await fetchAppointments();
     } catch (error) {
-      toast({
-        title: "Error",
-        description: "No se pudo rechazar la cita",
-        variant: "destructive",
-      });
+      notifications.appointments.rejectError();
     }
   };
 
@@ -132,18 +106,11 @@ export const useAppointments = () => {
 
       if (!response.ok) throw new Error("Error al cancelar cita");
 
-      toast({
-        title: "Cita cancelada",
-        description: "La cita ha sido cancelada exitosamente",
-      });
+      notifications.appointments.cancelled();
 
       await fetchAppointments();
     } catch (error) {
-      toast({
-        title: "Error",
-        description: "No se pudo cancelar la cita",
-        variant: "destructive",
-      });
+      notifications.appointments.cancelError();
     }
   };
 

+ 9 - 11
src/hooks/useChat.ts

@@ -1,5 +1,5 @@
 import { useState, useEffect, useCallback } from "react";
-import { toast } from "sonner";
+import { notifications } from "@/lib/notifications";
 import { generateReportFromMessages } from "@/utils/reports";
 import { Message, ChatState, ChatResponse, SuggestedPrompt } from "@/components/chatbot/types";
 
@@ -80,9 +80,7 @@ export const useChat = () => {
 
     try {
       // Mostrar toast de generación
-      toast.info("Generando reporte médico...", {
-        duration: 3000,
-      });
+      notifications.records.generating();
 
       // Generar reporte
       const report = generateReportFromMessages(messages);
@@ -102,13 +100,13 @@ export const useChat = () => {
       });
 
       if (response.ok) {
-        toast.success("Reporte médico generado y guardado exitosamente");
+        notifications.records.saved();
       } else {
-        toast.error("Error al guardar el reporte");
+        notifications.records.saveError();
       }
     } catch (error) {
       console.error("Error generando reporte:", error);
-      toast.error("Error al generar el reporte");
+      notifications.records.generateError();
     } finally {
       setIsGeneratingReport(false);
     }
@@ -355,7 +353,7 @@ export const useChat = () => {
     setInputDisabledForSuggestions(false);
     // Limpiar localStorage
     localStorage.removeItem("chatState");
-    toast.success("Nueva consulta iniciada");
+    notifications.chat.newConsultation();
   };
 
   const dismissCompletedBanner = () => {
@@ -385,14 +383,14 @@ export const useChat = () => {
       });
 
       if (response.ok) {
-        toast.success("Reporte guardado. Iniciando nueva consulta...");
+        notifications.records.savedNewConsultation();
         resetChat();
       } else {
-        toast.error("Error al guardar el reporte");
+        notifications.records.saveError();
       }
     } catch (error) {
       console.error("Error al generar reporte:", error);
-      toast.error("Error al generar el reporte");
+      notifications.records.generateError();
     }
   };
 

+ 2 - 6
src/hooks/useChatEffects.ts

@@ -1,5 +1,5 @@
 import { useEffect } from "react";
-import { toast } from "sonner";
+import { notifications } from "@/lib/notifications";
 import { Message } from "@/components/chatbot/types";
 
 interface UseChatEffectsProps {
@@ -19,11 +19,7 @@ export const useChatEffects = ({
   useEffect(() => {
     if (isLastMessage && messages.length > 0 && !showLastMessageToast) {
       setShowLastMessageToast(true);
-      toast.warning("⚠️ Última consulta disponible", {
-        description:
-          "Después de esta consulta se generará automáticamente el reporte médico.",
-        duration: 4000,
-      });
+      notifications.chat.lastMessageWarning();
     }
   }, [isLastMessage, messages.length, showLastMessageToast, setShowLastMessageToast]);
 }; 

+ 8 - 8
src/hooks/usePatients.ts

@@ -1,5 +1,5 @@
 import { useState, useEffect, useCallback } from "react"
-import { toast } from "sonner"
+import { notifications } from "@/lib/notifications"
 import { Patient, PatientsResponse } from "@/types/patients"
 
 export function usePatients() {
@@ -35,7 +35,7 @@ export function usePatients() {
       setPagination(data.pagination)
     } catch (error) {
       console.error("Error fetching patients:", error)
-      toast.error("Error al cargar los pacientes")
+      notifications.patients.loadError()
     } finally {
       setLoading(false)
     }
@@ -56,11 +56,11 @@ export function usePatients() {
         throw new Error(error.error || "Error al asignar paciente")
       }
 
-      toast.success("Paciente asignado exitosamente")
+      notifications.patients.assigned()
       fetchPatients() // Recargar la lista
     } catch (error) {
       console.error("Error assigning patient:", error)
-      toast.error(error instanceof Error ? error.message : "Error al asignar paciente")
+      notifications.patients.assignError(error instanceof Error ? error.message : undefined)
     }
   }, [fetchPatients])
 
@@ -75,11 +75,11 @@ export function usePatients() {
         throw new Error(error.error || "Error al desasignar paciente")
       }
 
-      toast.success("Paciente desasignado exitosamente")
+      notifications.patients.unassigned()
       fetchPatients() // Recargar la lista
     } catch (error) {
       console.error("Error unassigning patient:", error)
-      toast.error(error instanceof Error ? error.message : "Error al desasignar paciente")
+      notifications.patients.unassignError(error instanceof Error ? error.message : undefined)
     }
   }, [fetchPatients])
 
@@ -98,11 +98,11 @@ export function usePatients() {
         throw new Error(error.error || "Error al actualizar paciente")
       }
 
-      toast.success("Paciente actualizado exitosamente")
+      notifications.patients.updated()
       fetchPatients() // Recargar la lista
     } catch (error) {
       console.error("Error updating patient:", error)
-      toast.error(error instanceof Error ? error.message : "Error al actualizar paciente")
+      notifications.patients.updateError(error instanceof Error ? error.message : undefined)
     }
   }, [fetchPatients])
 

+ 8 - 8
src/hooks/useRecords.ts

@@ -1,6 +1,6 @@
 import { useState, useEffect, useCallback } from "react"
 import { useSession } from "next-auth/react"
-import { toast } from "sonner"
+import { notifications } from "@/lib/notifications"
 import { downloadReactPDF } from "@/utils/pdf"
 import { generateTXTReport } from "@/utils/reports"
 import { Record } from "@/components/records/types"
@@ -65,11 +65,11 @@ export function useRecords() {
         setFilteredRecords(data.records || [])
       } else {
         console.error("Error response:", response.status, response.statusText)
-        toast.error("Error al cargar los reportes")
+        notifications.records.loadError()
       }
     } catch (error) {
       console.error("Error fetching records:", error)
-      toast.error("Error al cargar los reportes")
+      notifications.records.loadError()
     } finally {
       setLoading(false)
     }
@@ -165,10 +165,10 @@ export function useRecords() {
       a.click()
       document.body.removeChild(a)
       URL.revokeObjectURL(url)
-      toast.success("Reporte TXT descargado")
+      notifications.records.txtDownloaded()
     } catch (error) {
       console.error("Error generando reporte TXT:", error)
-      toast.error("Error al generar el reporte TXT")
+      notifications.records.txtError()
     }
   }
 
@@ -179,10 +179,10 @@ export function useRecords() {
     try {
       console.log("Iniciando generación de PDF con React-PDF...")
       await downloadReactPDF(record)
-      toast.success("PDF generado y descargado exitosamente")
+      notifications.records.pdfDownloaded()
     } catch (error) {
       console.error("Error generando PDF:", error)
-      toast.error("Error al generar el PDF")
+      notifications.records.pdfError()
     } finally {
       setGeneratingPDF(false)
     }
@@ -190,7 +190,7 @@ export function useRecords() {
 
   const copyToClipboard = (text: string) => {
     navigator.clipboard.writeText(text)
-    toast.success("Reporte copiado al portapapeles")
+    notifications.records.copied()
   }
 
   const clearFilters = () => {

+ 341 - 0
src/lib/notifications.ts

@@ -0,0 +1,341 @@
+/**
+ * Sistema centralizado de notificaciones
+ * Proporciona mensajes consistentes y tipados para toda la aplicación
+ */
+
+import { toast } from "sonner"
+
+/**
+ * Notificaciones predefinidas organizadas por módulo
+ * Uso: notifications.appointments.created()
+ */
+export const notifications = {
+  // ==================== APPOINTMENTS ====================
+  appointments: {
+    created: () =>
+      toast.success("¡Cita solicitada!", {
+        description: "Un médico revisará tu solicitud pronto",
+      }),
+    
+    approved: () =>
+      toast.success("Cita aprobada", {
+        description: "El paciente ha sido notificado",
+      }),
+    
+    rejected: () =>
+      toast.success("Cita rechazada", {
+        description: "El paciente ha sido notificado",
+      }),
+    
+    cancelled: () =>
+      toast.success("Cita cancelada", {
+        description: "La cita ha sido cancelada exitosamente",
+      }),
+    
+    completed: () =>
+      toast.success("Cita completada", {
+        description: "Cita marcada como completada",
+      }),
+    
+    loadError: () =>
+      toast.error("Error al cargar las citas", {
+        description: "Por favor intenta de nuevo",
+      }),
+    
+    createError: (message?: string) =>
+      toast.error("Error al crear cita", {
+        description: message || "No se pudo crear la cita",
+      }),
+    
+    approveError: (message?: string) =>
+      toast.error("Error al aprobar cita", {
+        description: message || "No se pudo aprobar la cita",
+      }),
+    
+    rejectError: () =>
+      toast.error("Error al rechazar cita", {
+        description: "No se pudo rechazar la cita",
+      }),
+    
+    cancelError: () =>
+      toast.error("Error al cancelar cita", {
+        description: "No se pudo cancelar la cita",
+      }),
+    
+    completeError: () =>
+      toast.error("Error al completar cita", {
+        description: "No se pudo marcar la cita como completada",
+      }),
+    
+    videocallError: (message?: string) =>
+      toast.error("Error al iniciar videollamada", {
+        description: message || "No se pudo iniciar la videollamada",
+      }),
+  },
+
+  // ==================== PROFILE ====================
+  profile: {
+    updated: () =>
+      toast.success("Perfil actualizado", {
+        description: "Tus cambios han sido guardados correctamente",
+      }),
+    
+    updateError: (message?: string) =>
+      toast.error("Error al actualizar perfil", {
+        description: message || "No se pudo actualizar el perfil",
+      }),
+    
+    imageRemoved: () =>
+      toast.success("Imagen eliminada", {
+        description: "Tu foto de perfil ha sido eliminada correctamente",
+      }),
+    
+    imageRemoveError: (message?: string) =>
+      toast.error("Error al eliminar imagen", {
+        description: message || "No se pudo eliminar la imagen de perfil",
+      }),
+    
+    invalidImage: () =>
+      toast.error("Archivo inválido", {
+        description: "Por favor selecciona un archivo de imagen válido",
+      }),
+    
+    imageTooLarge: () =>
+      toast.error("Imagen muy grande", {
+        description: "La imagen debe ser menor a 5MB",
+      }),
+  },
+
+  // ==================== AUTHENTICATION ====================
+  auth: {
+    loginSuccess: () =>
+      toast.success("¡Bienvenido!", {
+        description: "Inicio de sesión exitoso",
+      }),
+    
+    loginError: () =>
+      toast.error("Error de autenticación", {
+        description: "Credenciales inválidas",
+      }),
+    
+    loginGeneralError: () =>
+      toast.error("Error al iniciar sesión", {
+        description: "Por favor intenta de nuevo",
+      }),
+    
+    registerSuccess: () =>
+      toast.success("¡Cuenta creada!", {
+        description: "Tu cuenta ha sido creada exitosamente",
+      }),
+    
+    registerError: (message?: string) =>
+      toast.error("Error al registrar", {
+        description: message || "No se pudo crear la cuenta",
+      }),
+    
+    passwordMismatch: () =>
+      toast.error("Contraseñas no coinciden", {
+        description: "Las contraseñas ingresadas no son iguales",
+      }),
+    
+    passwordTooShort: () =>
+      toast.error("Contraseña muy corta", {
+        description: "La contraseña debe tener al menos 6 caracteres",
+      }),
+  },
+
+  // ==================== RECORDS / REPORTS ====================
+  records: {
+    generating: () =>
+      toast.info("Generando reporte médico...", {
+        description: "Esto puede tomar unos segundos",
+      }),
+    
+    generated: () =>
+      toast.success("Reporte generado", {
+        description: "El reporte médico ha sido generado exitosamente",
+      }),
+    
+    saved: () =>
+      toast.success("Reporte guardado", {
+        description: "El reporte ha sido guardado exitosamente",
+      }),
+    
+    savedNewConsultation: () =>
+      toast.success("Reporte guardado", {
+        description: "Iniciando nueva consulta...",
+      }),
+    
+    downloaded: () =>
+      toast.success("Reporte descargado", {
+        description: "El archivo ha sido descargado exitosamente",
+      }),
+    
+    txtDownloaded: () =>
+      toast.success("Reporte TXT descargado"),
+    
+    pdfDownloaded: () =>
+      toast.success("PDF generado", {
+        description: "PDF generado y descargado exitosamente",
+      }),
+    
+    copied: () =>
+      toast.success("Copiado al portapapeles", {
+        description: "Reporte copiado exitosamente",
+      }),
+    
+    loadError: () =>
+      toast.error("Error al cargar reportes", {
+        description: "No se pudieron cargar los reportes médicos",
+      }),
+    
+    generateError: () =>
+      toast.error("Error al generar reporte", {
+        description: "No se pudo generar el reporte médico",
+      }),
+    
+    saveError: () =>
+      toast.error("Error al guardar reporte", {
+        description: "No se pudo guardar el reporte",
+      }),
+    
+    pdfError: () =>
+      toast.error("Error al generar PDF", {
+        description: "No se pudo generar el archivo PDF",
+      }),
+    
+    txtError: () =>
+      toast.error("Error al generar TXT", {
+        description: "No se pudo generar el reporte TXT",
+      }),
+  },
+
+  // ==================== PATIENTS ====================
+  patients: {
+    assigned: () =>
+      toast.success("Paciente asignado", {
+        description: "El paciente ha sido asignado exitosamente",
+      }),
+    
+    unassigned: () =>
+      toast.success("Paciente desasignado", {
+        description: "El paciente ha sido desasignado exitosamente",
+      }),
+    
+    updated: () =>
+      toast.success("Paciente actualizado", {
+        description: "Los datos del paciente han sido actualizados",
+      }),
+    
+    loadError: () =>
+      toast.error("Error al cargar pacientes", {
+        description: "No se pudieron cargar los pacientes",
+      }),
+    
+    assignError: (message?: string) =>
+      toast.error("Error al asignar paciente", {
+        description: message || "No se pudo asignar el paciente",
+      }),
+    
+    unassignError: (message?: string) =>
+      toast.error("Error al desasignar paciente", {
+        description: message || "No se pudo desasignar el paciente",
+      }),
+    
+    updateError: (message?: string) =>
+      toast.error("Error al actualizar paciente", {
+        description: message || "No se pudo actualizar el paciente",
+      }),
+  },
+
+  // ==================== CHAT ====================
+  chat: {
+    newConsultation: () =>
+      toast.success("Nueva consulta iniciada", {
+        description: "Puedes comenzar a consultar con el asistente",
+      }),
+    
+    lastMessageWarning: () =>
+      toast.warning("⚠️ Última consulta disponible", {
+        description:
+          "Has alcanzado el límite de consultas. La conversación se guardará después del próximo mensaje.",
+        duration: 5000,
+      }),
+  },
+
+  // ==================== VALIDATION ====================
+  validation: {
+    requiredField: (fieldName: string) =>
+      toast.error("Campo requerido", {
+        description: `Por favor completa el campo: ${fieldName}`,
+      }),
+    
+    invalidFormat: (fieldName: string) =>
+      toast.error("Formato inválido", {
+        description: `El formato de ${fieldName} no es válido`,
+      }),
+  },
+
+  // ==================== GENERIC ====================
+  generic: {
+    success: (message: string, description?: string) =>
+      toast.success(message, { description }),
+    
+    error: (message: string, description?: string) =>
+      toast.error(message, { description }),
+    
+    info: (message: string, description?: string) =>
+      toast.info(message, { description }),
+    
+    warning: (message: string, description?: string) =>
+      toast.warning(message, { description }),
+    
+    loading: (message: string, description?: string) =>
+      toast.loading(message, { description }),
+  },
+} as const
+
+/**
+ * Helper para manejar toasts de loading con promesas
+ * Uso: await withLoadingToast(promise, 'Guardando...', 'Guardado!')
+ */
+export async function withLoadingToast<T>(
+  promise: Promise<T>,
+  loadingMessage: string,
+  successMessage?: string
+): Promise<T> {
+  const toastId = toast.loading(loadingMessage)
+  
+  try {
+    const result = await promise
+    if (successMessage) {
+      toast.success(successMessage, { id: toastId })
+    } else {
+      toast.dismiss(toastId)
+    }
+    return result
+  } catch (error) {
+    toast.error("Ocurrió un error", { id: toastId })
+    throw error
+  }
+}
+
+/**
+ * Helper para crear toasts con promise
+ * Útil para operaciones asíncronas
+ */
+export const promiseToast = {
+  execute: <T>(
+    promise: Promise<T>,
+    messages: {
+      loading: string
+      success: string
+      error?: string
+    }
+  ) =>
+    toast.promise(promise, {
+      loading: messages.loading,
+      success: messages.success,
+      error: messages.error || "Ocurrió un error",
+    }),
+}