Quellcode durchsuchen

[hack] implement admin panel kinda

Matthew Trejo vor 2 Monaten
Ursprung
Commit
78c5f94680

+ 94 - 73
src/app/admin/page.tsx

@@ -6,12 +6,16 @@ import { useEffect } from "react"
 import AuthenticatedLayout from "@/components/AuthenticatedLayout"
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 import { Button } from "@/components/ui/button"
-import { Users, FileText, Activity, TrendingUp, UserCheck, AlertTriangle } from "lucide-react"
+import { Users, FileText, Activity, TrendingUp, Calendar } from "lucide-react"
 import Link from "next/link"
+import { useDashboard } from "@/hooks/useDashboard"
+import { Skeleton } from "@/components/ui/skeleton"
+import RecentActivityCard from "@/components/admin/RecentActivityCard"
 
 export default function AdminPage() {
   const { data: session, status } = useSession()
   const router = useRouter()
+  const stats = useDashboard()
 
   useEffect(() => {
     if (status === "unauthenticated") {
@@ -71,7 +75,11 @@ export default function AdminPage() {
                 </div>
                 <div>
                   <p className="text-sm font-medium text-gray-600">Pacientes</p>
-                  <p className="text-2xl font-bold text-gray-900">24</p>
+                  {stats.loading ? (
+                    <Skeleton className="h-8 w-12" />
+                  ) : (
+                    <p className="text-2xl font-bold text-gray-900">{stats.totalPatients}</p>
+                  )}
                 </div>
               </div>
             </CardContent>
@@ -85,7 +93,11 @@ export default function AdminPage() {
                 </div>
                 <div>
                   <p className="text-sm font-medium text-gray-600">Reportes</p>
-                  <p className="text-2xl font-bold text-gray-900">156</p>
+                  {stats.loading ? (
+                    <Skeleton className="h-8 w-12" />
+                  ) : (
+                    <p className="text-2xl font-bold text-gray-900">{stats.totalReports}</p>
+                  )}
                 </div>
               </div>
             </CardContent>
@@ -99,7 +111,11 @@ export default function AdminPage() {
                 </div>
                 <div>
                   <p className="text-sm font-medium text-gray-600">Consultas Hoy</p>
-                  <p className="text-2xl font-bold text-gray-900">12</p>
+                  {stats.loading ? (
+                    <Skeleton className="h-8 w-12" />
+                  ) : (
+                    <p className="text-2xl font-bold text-gray-900">{stats.consultsToday}</p>
+                  )}
                 </div>
               </div>
             </CardContent>
@@ -113,7 +129,11 @@ export default function AdminPage() {
                 </div>
                 <div>
                   <p className="text-sm font-medium text-gray-600">Crecimiento</p>
-                  <p className="text-2xl font-bold text-gray-900">+15%</p>
+                  {stats.loading ? (
+                    <Skeleton className="h-8 w-12" />
+                  ) : (
+                    <p className="text-2xl font-bold text-gray-900">{stats.reportsTrend}</p>
+                  )}
                 </div>
               </div>
             </CardContent>
@@ -121,7 +141,7 @@ export default function AdminPage() {
         </div>
 
         {/* Main Actions */}
-        <div className="grid md:grid-cols-2 gap-8 mb-8">
+        <div className="grid md:grid-cols-3 gap-8 mb-8">
           <Card className="hover:shadow-lg transition-shadow">
             <CardHeader>
               <CardTitle className="flex items-center">
@@ -136,16 +156,28 @@ export default function AdminPage() {
               </p>
               <div className="space-y-2">
                 <div className="flex items-center justify-between text-sm">
-                  <span>Pacientes activos</span>
-                  <span className="font-semibold">24</span>
+                  <span>Pacientes totales</span>
+                  {stats.loading ? (
+                    <Skeleton className="h-4 w-8" />
+                  ) : (
+                    <span className="font-semibold">{stats.totalPatients}</span>
+                  )}
                 </div>
                 <div className="flex items-center justify-between text-sm">
-                  <span>Nuevos este mes</span>
-                  <span className="font-semibold text-green-600">+8</span>
+                  <span>Pacientes activos</span>
+                  {stats.loading ? (
+                    <Skeleton className="h-4 w-8" />
+                  ) : (
+                    <span className="font-semibold text-green-600">{stats.activePatients}</span>
+                  )}
                 </div>
                 <div className="flex items-center justify-between text-sm">
-                  <span>Consultas pendientes</span>
-                  <span className="font-semibold text-orange-600">3</span>
+                  <span>Reportes esta semana</span>
+                  {stats.loading ? (
+                    <Skeleton className="h-4 w-8" />
+                  ) : (
+                    <span className="font-semibold text-orange-600">{stats.reportsThisWeek}</span>
+                  )}
                 </div>
               </div>
               <Link href="/admin/patients">
@@ -159,98 +191,87 @@ export default function AdminPage() {
           <Card className="hover:shadow-lg transition-shadow">
             <CardHeader>
               <CardTitle className="flex items-center">
-                <FileText className="w-5 h-5 mr-2 text-green-600" />
-                Reportes Médicos
+                <Calendar className="w-5 h-5 mr-2 text-purple-600" />
+                Gestión de Citas
               </CardTitle>
             </CardHeader>
             <CardContent>
               <p className="text-gray-600 mb-4">
-                Accede a todos los reportes médicos generados por el sistema 
-                y mantén un registro completo de las consultas.
+                Supervisa todas las citas del sistema, revisa su estado y
+                gestiona las solicitudes de pacientes.
               </p>
               <div className="space-y-2">
                 <div className="flex items-center justify-between text-sm">
-                  <span>Total de reportes</span>
-                  <span className="font-semibold">156</span>
+                  <span>Citas pendientes</span>
+                  <span className="font-semibold text-orange-600">-</span>
                 </div>
                 <div className="flex items-center justify-between text-sm">
-                  <span>Este mes</span>
-                  <span className="font-semibold text-green-600">+23</span>
+                  <span>Citas aprobadas</span>
+                  <span className="font-semibold text-green-600">-</span>
                 </div>
                 <div className="flex items-center justify-between text-sm">
-                  <span>Pendientes de revisión</span>
-                  <span className="font-semibold text-orange-600">7</span>
+                  <span>Completadas este mes</span>
+                  <span className="font-semibold">-</span>
                 </div>
               </div>
-              <Link href="/records">
+              <Link href="/appointments/doctor">
                 <Button className="w-full mt-4">
-                  Ver Todos los Reportes
+                  Ver Todas las Citas
                 </Button>
               </Link>
             </CardContent>
           </Card>
-        </div>
 
-        {/* Recent Activity */}
-        <div className="grid md:grid-cols-2 gap-8">
-          <Card>
+          <Card className="hover:shadow-lg transition-shadow">
             <CardHeader>
               <CardTitle className="flex items-center">
-                <UserCheck className="w-5 h-5 mr-2 text-blue-600" />
-                Actividad Reciente
+                <FileText className="w-5 h-5 mr-2 text-green-600" />
+                Reportes Médicos
               </CardTitle>
             </CardHeader>
             <CardContent>
-              <div className="space-y-4">
-                <div className="flex items-center space-x-3">
-                  <div className="w-2 h-2 bg-green-500 rounded-full"></div>
-                  <div className="flex-1">
-                    <p className="text-sm font-medium">Nuevo paciente registrado</p>
-                    <p className="text-xs text-gray-500">María González - hace 2 horas</p>
-                  </div>
+              <p className="text-gray-600 mb-4">
+                Accede a todos los reportes médicos generados por el sistema 
+                y mantén un registro completo de las consultas.
+              </p>
+              <div className="space-y-2">
+                <div className="flex items-center justify-between text-sm">
+                  <span>Total de reportes</span>
+                  {stats.loading ? (
+                    <Skeleton className="h-4 w-8" />
+                  ) : (
+                    <span className="font-semibold">{stats.totalReports}</span>
+                  )}
                 </div>
-                <div className="flex items-center space-x-3">
-                  <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
-                  <div className="flex-1">
-                    <p className="text-sm font-medium">Reporte médico generado</p>
-                    <p className="text-xs text-gray-500">Juan Pérez - hace 4 horas</p>
-                  </div>
+                <div className="flex items-center justify-between text-sm">
+                  <span>Esta semana</span>
+                  {stats.loading ? (
+                    <Skeleton className="h-4 w-8" />
+                  ) : (
+                    <span className="font-semibold text-green-600">{stats.reportsThisWeek}</span>
+                  )}
                 </div>
-                <div className="flex items-center space-x-3">
-                  <div className="w-2 h-2 bg-purple-500 rounded-full"></div>
-                  <div className="flex-1">
-                    <p className="text-sm font-medium">Consulta completada</p>
-                    <p className="text-xs text-gray-500">Ana López - hace 6 horas</p>
-                  </div>
+                <div className="flex items-center justify-between text-sm">
+                  <span>Crecimiento</span>
+                  {stats.loading ? (
+                    <Skeleton className="h-4 w-12" />
+                  ) : (
+                    <span className="font-semibold text-orange-600">{stats.reportsTrend}</span>
+                  )}
                 </div>
               </div>
+              <Link href="/records">
+                <Button className="w-full mt-4">
+                  Ver Todos los Reportes
+                </Button>
+              </Link>
             </CardContent>
           </Card>
+        </div>
 
-          <Card>
-            <CardHeader>
-              <CardTitle className="flex items-center">
-                <AlertTriangle className="w-5 h-5 mr-2 text-orange-600" />
-                Alertas del Sistema
-              </CardTitle>
-            </CardHeader>
-            <CardContent>
-              <div className="space-y-4">
-                <div className="p-3 bg-yellow-50 rounded-lg">
-                  <p className="text-sm font-medium text-yellow-800">3 reportes pendientes de revisión</p>
-                  <p className="text-xs text-yellow-600">Revisar antes del final del día</p>
-                </div>
-                <div className="p-3 bg-blue-50 rounded-lg">
-                  <p className="text-sm font-medium text-blue-800">2 nuevos pacientes este mes</p>
-                  <p className="text-xs text-blue-600">Crecimiento del 15% vs mes anterior</p>
-                </div>
-                <div className="p-3 bg-green-50 rounded-lg">
-                  <p className="text-sm font-medium text-green-800">Sistema funcionando correctamente</p>
-                  <p className="text-xs text-green-600">Todas las funciones operativas</p>
-                </div>
-              </div>
-            </CardContent>
-          </Card>
+        {/* Recent Activity */}
+        <div className="grid md:grid-cols-1 gap-8">
+          <RecentActivityCard limit={6} />
         </div>
       </div>
     </AuthenticatedLayout>

+ 152 - 0
src/app/api/admin/recent-activity/route.ts

@@ -0,0 +1,152 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/lib/auth"
+import { prisma } from "@/lib/prisma"
+
+interface ActivityEvent {
+  id: string
+  type: 'user_registered' | 'report_created' | 'appointment_created' | 'appointment_completed'
+  title: string
+  description: string
+  timestamp: Date
+  color: 'green' | 'blue' | 'purple' | 'orange'
+}
+
+export async function GET(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user?.id) {
+      return NextResponse.json(
+        { error: "No autorizado" },
+        { status: 401 }
+      )
+    }
+
+    if (session.user.role !== "ADMIN") {
+      return NextResponse.json(
+        { error: "Acceso denegado. Solo administradores." },
+        { status: 403 }
+      )
+    }
+
+    const { searchParams } = new URL(request.url)
+    const limit = parseInt(searchParams.get("limit") || "10")
+
+    // Obtener últimos usuarios registrados
+    const recentUsers = await prisma.user.findMany({
+      where: {
+        role: "PATIENT"
+      },
+      select: {
+        id: true,
+        name: true,
+        lastname: true,
+        createdAt: true
+      },
+      orderBy: {
+        createdAt: "desc"
+      },
+      take: 5
+    })
+
+    // Obtener últimos reportes creados
+    const recentReports = await prisma.record.findMany({
+      select: {
+        id: true,
+        createdAt: true,
+        chatType: true,
+        user: {
+          select: {
+            name: true,
+            lastname: true
+          }
+        }
+      },
+      orderBy: {
+        createdAt: "desc"
+      },
+      take: 5
+    })
+
+    // Obtener últimas citas completadas
+    const recentAppointments = await prisma.appointment.findMany({
+      where: {
+        estado: "COMPLETADA"
+      },
+      select: {
+        id: true,
+        createdAt: true,
+        updatedAt: true,
+        estado: true,
+        paciente: {
+          select: {
+            name: true,
+            lastname: true
+          }
+        }
+      },
+      orderBy: {
+        updatedAt: "desc"
+      },
+      take: 5
+    })
+
+    // Convertir a eventos unificados
+    const activities: ActivityEvent[] = []
+
+    // Agregar usuarios registrados
+    recentUsers.forEach(user => {
+      activities.push({
+        id: `user-${user.id}`,
+        type: 'user_registered',
+        title: 'Nuevo paciente registrado',
+        description: `${user.name} ${user.lastname}`,
+        timestamp: user.createdAt,
+        color: 'green'
+      })
+    })
+
+    // Agregar reportes
+    recentReports.forEach(report => {
+      activities.push({
+        id: `report-${report.id}`,
+        type: 'report_created',
+        title: report.chatType === 'PSYCHOLOGICAL' ? 'Reporte psicológico generado' : 'Reporte médico generado',
+        description: `${report.user.name} ${report.user.lastname}`,
+        timestamp: report.createdAt,
+        color: 'blue'
+      })
+    })
+
+    // Agregar citas completadas
+    recentAppointments.forEach(apt => {
+      activities.push({
+        id: `appointment-${apt.id}`,
+        type: 'appointment_completed',
+        title: 'Consulta completada',
+        description: `${apt.paciente.name} ${apt.paciente.lastname}`,
+        timestamp: apt.updatedAt,
+        color: 'purple'
+      })
+    })
+
+    // Ordenar todos los eventos por fecha (más reciente primero)
+    activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
+
+    // Limitar al número solicitado
+    const limitedActivities = activities.slice(0, limit)
+
+    return NextResponse.json({
+      activities: limitedActivities,
+      total: activities.length
+    })
+
+  } catch (error) {
+    console.error("Error in recent activity API:", error)
+    return NextResponse.json(
+      { error: "Error al obtener actividad reciente" },
+      { status: 500 }
+    )
+  }
+}

+ 46 - 8
src/app/api/appointments/route.ts

@@ -32,17 +32,55 @@ export async function GET(request: NextRequest) {
 
     let appointments;
 
-    // Si es médico, ver citas donde es médico o citas pendientes
-    if (user.role === "DOCTOR") {
+    // ADMIN: Ver todas las citas del sistema
+    if (user.role === "ADMIN") {
+      appointments = await prisma.appointment.findMany({
+        where: estado ? { estado: estado } : {},
+        include: {
+          paciente: {
+            select: {
+              id: true,
+              name: true,
+              lastname: true,
+              email: true,
+              profileImage: true,
+            },
+          },
+          medico: {
+            select: {
+              id: true,
+              name: true,
+              lastname: true,
+              email: true,
+              profileImage: true,
+            },
+          },
+        },
+        orderBy: [
+          { estado: "asc" },
+          { createdAt: "desc" },
+        ],
+      });
+    }
+    // Si es médico, ver sus citas y las pendientes sin asignar
+    else if (user.role === "DOCTOR") {
       appointments = await prisma.appointment.findMany({
         where: estado
-          ? {
-              OR: [
-                { medicoId: user.id, estado: estado },
-                { medicoId: null, estado: "PENDIENTE" as const },
-              ],
-            }
+          ? estado === "PENDIENTE"
+            ? {
+                // Para pendientes: solo las sin médico o las suyas
+                OR: [
+                  { medicoId: user.id, estado: "PENDIENTE" as const },
+                  { medicoId: null, estado: "PENDIENTE" as const },
+                ],
+              }
+            : {
+                // Para otros estados: solo sus citas
+                medicoId: user.id,
+                estado: estado,
+              }
           : {
+              // Sin filtro: sus citas + pendientes sin asignar
               OR: [
                 { medicoId: user.id },
                 { medicoId: null, estado: "PENDIENTE" as const },

+ 192 - 73
src/app/api/users/patients/route.ts

@@ -27,81 +27,161 @@ export async function GET(request: NextRequest) {
     const page = parseInt(searchParams.get("page") || "1")
     const limit = parseInt(searchParams.get("limit") || "10")
     const skip = (page - 1) * limit
-    const assignedOnly = searchParams.get("assignedOnly") === "true"
+    const myPatientsOnly = searchParams.get("myPatientsOnly") === "true"
 
-    // Construir la condición de búsqueda
-    const whereClause = {
-      role: "PATIENT" as const,
-      OR: [
-        {
-          name: {
-            contains: search,
-            mode: "insensitive" as const
-          }
+    // Si es un doctor y solicita solo sus pacientes asignados
+    if (session.user.role === "DOCTOR" && myPatientsOnly) {
+      // Obtener pacientes únicos que tienen citas APROBADA o COMPLETADA con este médico
+      const appointments = await prisma.appointment.findMany({
+        where: {
+          medicoId: session.user.id,
+          estado: {
+            in: ["APROBADA", "COMPLETADA"]
+          },
+          paciente: search ? {
+            OR: [
+              { name: { contains: search, mode: "insensitive" as const } },
+              { lastname: { contains: search, mode: "insensitive" as const } },
+              { email: { contains: search, mode: "insensitive" as const } },
+              { username: { contains: search, mode: "insensitive" as const } }
+            ]
+          } : undefined
         },
-        {
-          lastname: {
-            contains: search,
-            mode: "insensitive" as const
+        select: {
+          pacienteId: true,
+          createdAt: true,
+          estado: true,
+          fechaSolicitada: true,
+          paciente: {
+            select: {
+              id: true,
+              name: true,
+              lastname: true,
+              username: true,
+              email: true,
+              profileImage: true,
+              phone: true,
+              dateOfBirth: true,
+              gender: true,
+              allergies: true,
+              currentMedications: true,
+              medicalHistory: true,
+              createdAt: true,
+              _count: {
+                select: {
+                  records: true,
+                  patientAppointments: true
+                }
+              }
+            }
           }
         },
-        {
-          email: {
-            contains: search,
-            mode: "insensitive" as const
-          }
-        },
-        {
-          username: {
-            contains: search,
-            mode: "insensitive" as const
-          }
+        orderBy: {
+          updatedAt: "desc"
         }
-      ]
-    }
+      })
 
-    // Obtener todos los pacientes primero
-    const allPatients = await prisma.user.findMany({
-      where: whereClause,
-      select: {
-        id: true,
-        name: true,
-        lastname: true,
-        username: true,
-        email: true,
-        profileImage: true,
-        createdAt: true,
-        _count: {
-          select: {
-            records: true
+      // Agrupar por paciente y obtener la información más relevante
+      const patientMap = new Map()
+      appointments.forEach(apt => {
+        const patientId = apt.pacienteId
+        if (!patientMap.has(patientId)) {
+          patientMap.set(patientId, {
+            ...apt.paciente,
+            appointmentInfo: {
+              firstAppointment: apt.createdAt,
+              lastAppointment: apt.createdAt,
+              totalAppointments: 1,
+              completedAppointments: apt.estado === "COMPLETADA" ? 1 : 0
+            }
+          })
+        } else {
+          const existing = patientMap.get(patientId)
+          existing.appointmentInfo.totalAppointments += 1
+          if (apt.estado === "COMPLETADA") {
+            existing.appointmentInfo.completedAppointments += 1
+          }
+          // Actualizar primera y última cita
+          if (new Date(apt.createdAt) < new Date(existing.appointmentInfo.firstAppointment)) {
+            existing.appointmentInfo.firstAppointment = apt.createdAt
+          }
+          if (new Date(apt.createdAt) > new Date(existing.appointmentInfo.lastAppointment)) {
+            existing.appointmentInfo.lastAppointment = apt.createdAt
           }
         }
-      },
-      orderBy: {
-        createdAt: "desc"
-      }
-    })
+      })
+
+      // Convertir a array y aplicar paginación
+      const allPatients = Array.from(patientMap.values())
+      const total = allPatients.length
+      const paginatedPatients = allPatients.slice(skip, skip + limit)
 
-    // Filtrar por asignaciones si es necesario
-    let filteredPatients = allPatients
-    if (assignedOnly) {
-      // Por ahora, retornamos todos los pacientes
-      // En el futuro, cuando el cliente de Prisma esté actualizado, se puede implementar el filtro
-      filteredPatients = allPatients
+      return NextResponse.json({
+        patients: paginatedPatients,
+        pagination: {
+          page,
+          limit,
+          total,
+          totalPages: Math.ceil(total / limit)
+        }
+      })
     }
 
-    // Aplicar paginación
-    const total = filteredPatients.length
-    const patients = filteredPatients.slice(skip, skip + limit)
+    // Para admins o listado general: obtener todos los pacientes
+    const whereClause = {
+      role: "PATIENT" as const,
+      OR: search ? [
+        { name: { contains: search, mode: "insensitive" as const } },
+        { lastname: { contains: search, mode: "insensitive" as const } },
+        { email: { contains: search, mode: "insensitive" as const } },
+        { username: { contains: search, mode: "insensitive" as const } }
+      ] : undefined
+    }
 
-    // Agregar información de asignación (simulada por ahora)
-    const patientsWithAssignments = patients.map(patient => ({
-      ...patient,
-      assignedDoctor: [] // Por ahora vacío, se implementará cuando el cliente esté actualizado
-    }))
+    const [patients, total] = await Promise.all([
+      prisma.user.findMany({
+        where: whereClause,
+        select: {
+          id: true,
+          name: true,
+          lastname: true,
+          username: true,
+          email: true,
+          profileImage: true,
+          createdAt: true,
+          _count: {
+            select: {
+              records: true,
+              patientAppointments: true
+            }
+          },
+          assignedDoctor: {
+            where: { isActive: true },
+            select: {
+              doctorId: true,
+              assignedAt: true,
+              notes: true,
+              doctor: {
+                select: {
+                  id: true,
+                  name: true,
+                  lastname: true
+                }
+              }
+            }
+          }
+        },
+        orderBy: {
+          createdAt: "desc"
+        },
+        skip,
+        take: limit
+      }),
+      prisma.user.count({ where: whereClause })
+    ])
 
     return NextResponse.json({
-      patients: patientsWithAssignments,
+      patients,
       pagination: {
         page,
         limit,
@@ -139,11 +219,11 @@ export async function POST(request: NextRequest) {
     }
 
     const body = await request.json()
-    const { patientId, notes } = body
+    const { patientId, doctorId, notes } = body
 
-    if (!patientId) {
+    if (!patientId || !doctorId) {
       return NextResponse.json(
-        { error: "ID del paciente es requerido" },
+        { error: "ID del paciente y del doctor son requeridos" },
         { status: 400 }
       )
     }
@@ -163,18 +243,57 @@ export async function POST(request: NextRequest) {
       )
     }
 
-    // Por ahora, solo verificamos que el paciente existe
-    // La asignación se implementará cuando el cliente de Prisma esté actualizado
-    return NextResponse.json({
-      message: "Paciente verificado exitosamente",
-      patient: {
-        id: patient.id,
-        name: patient.name,
-        lastname: patient.lastname,
-        email: patient.email
+    // Verificar que el doctor existe y es un doctor
+    const doctor = await prisma.user.findFirst({
+      where: {
+        id: doctorId,
+        role: "DOCTOR"
       }
     })
 
+    if (!doctor) {
+      return NextResponse.json(
+        { error: "Doctor no encontrado" },
+        { status: 404 }
+      )
+    }
+
+    // Crear o actualizar la asignación
+    const assignment = await prisma.patientAssignment.upsert({
+      where: {
+        doctorId_patientId: {
+          doctorId,
+          patientId
+        }
+      },
+      update: {
+        isActive: true,
+        notes,
+        assignedAt: new Date()
+      },
+      create: {
+        doctorId,
+        patientId,
+        notes,
+        isActive: true
+      },
+      include: {
+        patient: {
+          select: {
+            id: true,
+            name: true,
+            lastname: true,
+            email: true
+          }
+        }
+      }
+    })
+
+    return NextResponse.json({
+      message: "Paciente asignado exitosamente",
+      assignment
+    })
+
   } catch (error) {
     console.error("Error asignando paciente:", error)
     return NextResponse.json(

+ 247 - 0
src/app/patients/page.tsx

@@ -0,0 +1,247 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useSession } from "next-auth/react"
+import { useRouter } from "next/navigation"
+import AuthenticatedLayout from "@/components/AuthenticatedLayout"
+import MyPatientCard from "@/components/patients/MyPatientCard"
+import PatientsSearch from "@/components/patients/PatientsSearch"
+import PageHeader from "@/components/ui/PageHeader"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Users, RefreshCw, AlertCircle } from "lucide-react"
+import { toast } from "sonner"
+
+interface Patient {
+  id: string
+  name: string
+  lastname: string
+  email: string | null
+  phone: string | null
+  dateOfBirth: string | null
+  gender: string | null
+  profileImage: string | null
+  allergies: string | null
+  currentMedications: string | null
+  medicalHistory: string | null
+  _count?: {
+    records: number
+    patientAppointments: number
+  }
+  appointmentInfo?: {
+    firstAppointment: string
+    lastAppointment: string
+    totalAppointments: number
+    completedAppointments: number
+  }
+}
+
+interface PatientsResponse {
+  patients: Patient[]
+  pagination: {
+    page: number
+    limit: number
+    total: number
+    totalPages: number
+  }
+}
+
+export default function MyPatientsPage() {
+  const { data: session, status } = useSession()
+  const router = useRouter()
+  const [patients, setPatients] = useState<Patient[]>([])
+  const [totalPatients, setTotalPatients] = useState(0)
+  const [loading, setLoading] = useState(true)
+  const [searchTerm, setSearchTerm] = useState("")
+  const [debouncedSearch, setDebouncedSearch] = useState("")
+
+  // Protección de ruta
+  useEffect(() => {
+    if (status === "unauthenticated") {
+      router.push("/auth/login")
+    }
+  }, [status, router])
+
+  useEffect(() => {
+    if (session && session.user.role !== "DOCTOR") {
+      router.push("/dashboard")
+    }
+  }, [session, router])
+
+  // Debounce para búsqueda
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      setDebouncedSearch(searchTerm)
+    }, 500)
+
+    return () => clearTimeout(timer)
+  }, [searchTerm])
+
+  // Cargar pacientes
+  const fetchPatients = async () => {
+    setLoading(true)
+    try {
+      const params = new URLSearchParams({
+        myPatientsOnly: "true",
+        search: debouncedSearch,
+        page: "1",
+        limit: "100"
+      })
+
+      const response = await fetch(`/api/users/patients?${params}`)
+
+      if (!response.ok) {
+        throw new Error("Error al cargar pacientes")
+      }
+
+      const data: PatientsResponse = await response.json()
+      setPatients(data.patients)
+      
+      // Si no hay búsqueda, actualizar el total
+      if (!debouncedSearch) {
+        setTotalPatients(data.patients.length)
+      }
+    } catch (error) {
+      console.error("Error fetching patients:", error)
+      toast.error("Error al cargar tus pacientes", {
+        description: "Por favor intenta de nuevo"
+      })
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  useEffect(() => {
+    if (session?.user?.role === "DOCTOR") {
+      fetchPatients()
+    }
+  }, [session, debouncedSearch])
+
+  const handleViewRecords = (patientId: string) => {
+    router.push(`/records?patientId=${patientId}`)
+  }
+
+  const handleViewAppointments = (patientId: string) => {
+    router.push(`/appointments/doctor?patientId=${patientId}`)
+  }
+
+  if (status === "loading" || !session) {
+    return (
+      <div className="min-h-screen flex items-center justify-center">
+        <div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
+      </div>
+    )
+  }
+
+  if (session.user.role !== "DOCTOR") {
+    return null
+  }
+
+  return (
+    <AuthenticatedLayout>
+      <div className="min-h-screen bg-gray-50 p-6">
+        <div className="max-w-7xl mx-auto space-y-6">
+          {/* Header */}
+          <PageHeader
+            title="Mis Pacientes"
+            description="Pacientes que has atendido o tienes citas aprobadas"
+            icon={Users}
+            stats={{
+              value: patients.length,
+              label: "Pacientes totales"
+            }}
+            actions={
+              <Button
+                onClick={fetchPatients}
+                variant="outline"
+                size="sm"
+                disabled={loading}
+              >
+                <RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
+                Actualizar
+              </Button>
+            }
+          />
+
+          {/* Estadísticas */}
+          <Card>
+            <CardHeader>
+              <CardTitle className="text-lg flex items-center gap-2">
+                <Users className="h-5 w-5 text-blue-600" />
+                Resumen
+              </CardTitle>
+            </CardHeader>
+            <CardContent>
+              <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+                <div className="bg-blue-50 rounded-lg p-4">
+                  <div className="text-2xl font-bold text-blue-600">
+                    {patients.length}
+                  </div>
+                  <div className="text-sm text-gray-600">Total Pacientes</div>
+                </div>
+                <div className="bg-green-50 rounded-lg p-4">
+                  <div className="text-2xl font-bold text-green-600">
+                    {patients.reduce((sum, p) => sum + (p.appointmentInfo?.completedAppointments || 0), 0)}
+                  </div>
+                  <div className="text-sm text-gray-600">Citas Completadas</div>
+                </div>
+                <div className="bg-amber-50 rounded-lg p-4">
+                  <div className="text-2xl font-bold text-amber-600">
+                    {patients.reduce((sum, p) => sum + (p.appointmentInfo?.totalAppointments || 0), 0)}
+                  </div>
+                  <div className="text-sm text-gray-600">Total de Citas</div>
+                </div>
+                <div className="bg-purple-50 rounded-lg p-4">
+                  <div className="text-2xl font-bold text-purple-600">
+                    {patients.reduce((sum, p) => sum + (p._count?.records || 0), 0)}
+                  </div>
+                  <div className="text-sm text-gray-600">Registros Médicos</div>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+
+          {/* Búsqueda */}
+          <PatientsSearch
+            searchTerm={searchTerm}
+            onSearchChange={setSearchTerm}
+            totalPatients={totalPatients}
+            filteredCount={patients.length}
+          />
+
+          {/* Lista de pacientes */}
+          {loading ? (
+            <div className="flex items-center justify-center py-12">
+              <div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
+            </div>
+          ) : patients.length === 0 ? (
+            <Card>
+              <CardContent className="py-12">
+                <div className="text-center text-gray-500">
+                  <AlertCircle className="h-12 w-12 mx-auto mb-4 text-gray-400" />
+                  <p className="text-lg font-medium">No tienes pacientes aún</p>
+                  <p className="text-sm mt-2">
+                    {searchTerm
+                      ? "No se encontraron pacientes con ese criterio de búsqueda"
+                      : "Los pacientes aparecerán aquí cuando apruebes o completes citas con ellos"}
+                  </p>
+                </div>
+              </CardContent>
+            </Card>
+          ) : (
+            <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+              {patients.map((patient) => (
+                <MyPatientCard
+                  key={patient.id}
+                  patient={patient}
+                  onViewRecords={handleViewRecords}
+                  onViewAppointments={handleViewAppointments}
+                />
+              ))}
+            </div>
+          )}
+        </div>
+      </div>
+    </AuthenticatedLayout>
+  )
+}

+ 8 - 2
src/components/Navigation.tsx

@@ -3,7 +3,7 @@
 import { useSession, signOut } from "next-auth/react"
 import Link from "next/link"
 import { Button } from "@/components/ui/button"
-import { User, LogOut, MessageSquare, FileText, Users, Settings } from "lucide-react"
+import { User, LogOut, MessageSquare, FileText, Users, Settings, Calendar } from "lucide-react"
 import { COLOR_PALETTE } from "@/utils/palette"
 
 export default function Navigation() {
@@ -78,7 +78,7 @@ export default function Navigation() {
 
               {session.user.role === "DOCTOR" && (
                 <>
-                  <Link href="/admin">
+                  <Link href="/patients">
                     <Button variant="ghost" size="sm">
                       <Users className="w-4 h-4 mr-2" />
                       Mis Pacientes
@@ -101,6 +101,12 @@ export default function Navigation() {
                       Administración
                     </Button>
                   </Link>
+                  <Link href="/appointments/doctor">
+                    <Button variant="ghost" size="sm">
+                      <Calendar className="w-4 h-4 mr-2" />
+                      Gestión de Citas
+                    </Button>
+                  </Link>
                   <Link href="/records">
                     <Button variant="ghost" size="sm">
                       <FileText className="w-4 h-4 mr-2" />

+ 113 - 0
src/components/admin/RecentActivityCard.tsx

@@ -0,0 +1,113 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Skeleton } from "@/components/ui/skeleton"
+import { UserCheck } from "lucide-react"
+import { useRecentActivity } from "@/hooks/useRecentActivity"
+import { formatDistanceToNow } from "date-fns"
+import { es } from "date-fns/locale"
+
+interface RecentActivityCardProps {
+  limit?: number
+}
+
+export default function RecentActivityCard({ limit = 6 }: RecentActivityCardProps) {
+  const { activities, loading, error } = useRecentActivity(limit)
+
+  const getColorClasses = (color: string) => {
+    switch (color) {
+      case 'green': return 'bg-green-500'
+      case 'blue': return 'bg-blue-500'
+      case 'purple': return 'bg-purple-500'
+      case 'orange': return 'bg-orange-500'
+      default: return 'bg-gray-500'
+    }
+  }
+
+  if (loading) {
+    return (
+      <Card>
+        <CardHeader>
+          <CardTitle className="flex items-center">
+            <UserCheck className="w-5 h-5 mr-2 text-blue-600" />
+            Actividad Reciente
+          </CardTitle>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            {[1, 2, 3, 4].map((i) => (
+              <div key={i} className="flex items-center space-x-3">
+                <Skeleton className="w-2 h-2 rounded-full" />
+                <div className="flex-1 space-y-2">
+                  <Skeleton className="h-4 w-48" />
+                  <Skeleton className="h-3 w-32" />
+                </div>
+              </div>
+            ))}
+          </div>
+        </CardContent>
+      </Card>
+    )
+  }
+
+  if (error) {
+    return (
+      <Card>
+        <CardHeader>
+          <CardTitle className="flex items-center">
+            <UserCheck className="w-5 h-5 mr-2 text-blue-600" />
+            Actividad Reciente
+          </CardTitle>
+        </CardHeader>
+        <CardContent>
+          <p className="text-sm text-red-600">Error al cargar actividad: {error}</p>
+        </CardContent>
+      </Card>
+    )
+  }
+
+  if (activities.length === 0) {
+    return (
+      <Card>
+        <CardHeader>
+          <CardTitle className="flex items-center">
+            <UserCheck className="w-5 h-5 mr-2 text-blue-600" />
+            Actividad Reciente
+          </CardTitle>
+        </CardHeader>
+        <CardContent>
+          <p className="text-sm text-gray-500">No hay actividad reciente</p>
+        </CardContent>
+      </Card>
+    )
+  }
+
+  return (
+    <Card>
+      <CardHeader>
+        <CardTitle className="flex items-center">
+          <UserCheck className="w-5 h-5 mr-2 text-blue-600" />
+          Actividad Reciente
+        </CardTitle>
+      </CardHeader>
+      <CardContent>
+        <div className="space-y-4">
+          {activities.map((activity) => (
+            <div key={activity.id} className="flex items-center space-x-3">
+              <div className={`w-2 h-2 rounded-full ${getColorClasses(activity.color)}`}></div>
+              <div className="flex-1">
+                <p className="text-sm font-medium">{activity.title}</p>
+                <p className="text-xs text-gray-500">
+                  {activity.description} - {formatDistanceToNow(new Date(activity.timestamp), { 
+                    addSuffix: true,
+                    locale: es 
+                  })}
+                </p>
+              </div>
+            </div>
+          ))}
+        </div>
+      </CardContent>
+    </Card>
+  )
+}

+ 3 - 3
src/components/dashboard/ActivitySummary.tsx

@@ -52,7 +52,7 @@ export default function ActivitySummary({ stats }: ActivitySummaryProps) {
       <CardContent>
         <div className="grid gap-4 md:grid-cols-3">
           <div className="flex items-center gap-4">
-            <div className="rounded-full bg-blue-100 dark:bg-blue-900/20 p-3">
+            <div className="rounded-full bg-blue-100 dark:bg-blue-500/20 p-3">
               <TrendingUp className="h-5 w-5 text-blue-600 dark:text-blue-400" />
             </div>
             <div>
@@ -63,7 +63,7 @@ export default function ActivitySummary({ stats }: ActivitySummaryProps) {
             </div>
           </div>
           <div className="flex items-center gap-4">
-            <div className="rounded-full bg-emerald-100 dark:bg-emerald-900/20 p-3">
+            <div className="rounded-full bg-emerald-100 dark:bg-emerald-500/20 p-3">
               <FileText className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
             </div>
             <div>
@@ -74,7 +74,7 @@ export default function ActivitySummary({ stats }: ActivitySummaryProps) {
             </div>
           </div>
           <div className="flex items-center gap-4">
-            <div className="rounded-full bg-amber-100 dark:bg-amber-900/20 p-3">
+            <div className="rounded-full bg-amber-100 dark:bg-amber-500/20 p-3">
               <Users className="h-5 w-5 text-amber-600 dark:text-amber-400" />
             </div>
             <div>

+ 1 - 1
src/components/dashboard/DashboardStats.tsx

@@ -84,7 +84,7 @@ export default function DashboardStats({ isPatient, isDoctor, isAdmin, stats }:
       <Card>
         <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
           <CardTitle className="text-sm font-medium">
-            {isDoctor || isAdmin ? "Pacientes" : "Citas"}
+            {isDoctor || isAdmin ? "Usuarios" : "Citas"}
           </CardTitle>
           {isDoctor || isAdmin ? (
             <Users className="h-4 w-4 text-muted-foreground" />

+ 4 - 4
src/components/dashboard/QuickActions.tsx

@@ -26,7 +26,7 @@ export default function QuickActions({ isAdmin, isDoctor, isPatient }: QuickActi
               title="Panel de Administración"
               description="Gestiona usuarios, doctores y configuración"
               href="/admin"
-              iconBg="bg-violet-100 dark:bg-violet-900/20"
+              iconBg="bg-violet-100 dark:bg-violet-500/20"
               iconColor="text-violet-600 dark:text-violet-400"
             />
             <QuickActionButton
@@ -34,7 +34,7 @@ export default function QuickActions({ isAdmin, isDoctor, isPatient }: QuickActi
               title="Todos los Reportes"
               description="Accede al historial completo del sistema"
               href="/records"
-              iconBg="bg-emerald-100 dark:bg-emerald-900/20"
+              iconBg="bg-emerald-100 dark:bg-emerald-500/20"
               iconColor="text-emerald-600 dark:text-emerald-400"
             />
             <QuickActionButton
@@ -42,7 +42,7 @@ export default function QuickActions({ isAdmin, isDoctor, isPatient }: QuickActi
               title="Gestión de Usuarios"
               description="Administra pacientes y personal médico"
               href="/admin/patients"
-              iconBg="bg-blue-100 dark:bg-blue-900/20"
+              iconBg="bg-blue-100 dark:bg-blue-500/20"
               iconColor="text-blue-600 dark:text-blue-400"
             />
           </>
@@ -52,7 +52,7 @@ export default function QuickActions({ isAdmin, isDoctor, isPatient }: QuickActi
               icon={Users}
               title="Mis Pacientes"
               description="Revisa y gestiona tus pacientes asignados"
-              href="/admin"
+              href="/patients"
               iconBg="bg-blue-100 dark:bg-blue-900/20"
               iconColor="text-blue-600 dark:text-blue-400"
             />

+ 211 - 0
src/components/patients/MyPatientCard.tsx

@@ -0,0 +1,211 @@
+"use client"
+
+import { Card, CardContent, CardHeader } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import {
+  User,
+  Mail,
+  Phone,
+  Calendar,
+  FileText,
+  Stethoscope,
+  AlertCircle,
+  Pill,
+  CalendarClock
+} from "lucide-react"
+import { format } from "date-fns"
+import { es } from "date-fns/locale"
+
+interface MyPatientCardProps {
+  patient: {
+    id: string
+    name: string
+    lastname: string
+    email: string | null
+    phone: string | null
+    dateOfBirth: string | null
+    gender: string | null
+    profileImage: string | null
+    allergies: string | null
+    currentMedications: string | null
+    medicalHistory: string | null
+    _count?: {
+      records: number
+      patientAppointments: number
+    }
+    appointmentInfo?: {
+      firstAppointment: string
+      lastAppointment: string
+      totalAppointments: number
+      completedAppointments: number
+    }
+  }
+  onViewRecords: (patientId: string) => void
+  onViewAppointments: (patientId: string) => void
+}
+
+export default function MyPatientCard({
+  patient,
+  onViewRecords,
+  onViewAppointments
+}: MyPatientCardProps) {
+  const getInitials = () => {
+    return `${patient.name[0] || ""}${patient.lastname[0] || ""}`.toUpperCase()
+  }
+
+  const getGenderLabel = (gender: string | null) => {
+    switch (gender) {
+      case "MALE": return "Masculino"
+      case "FEMALE": return "Femenino"
+      case "OTHER": return "Otro"
+      default: return "No especificado"
+    }
+  }
+
+  return (
+    <Card className="hover:shadow-lg transition-shadow">
+      <CardHeader className="pb-4">
+        <div className="flex items-start justify-between">
+          <div className="flex items-center gap-4">
+            <Avatar className="h-16 w-16">
+              <AvatarImage src={patient.profileImage || undefined} />
+              <AvatarFallback className="bg-blue-100 text-blue-600 text-lg font-semibold">
+                {getInitials()}
+              </AvatarFallback>
+            </Avatar>
+            <div>
+              <h3 className="text-lg font-semibold text-gray-900">
+                {patient.name} {patient.lastname}
+              </h3>
+              <div className="flex items-center gap-2 mt-1">
+                {patient.email && (
+                  <p className="text-sm text-gray-500 flex items-center gap-1">
+                    <Mail className="h-3 w-3" />
+                    {patient.email}
+                  </p>
+                )}
+              </div>
+            </div>
+          </div>
+          <div className="flex gap-2">
+            <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
+              {patient.appointmentInfo?.completedAppointments || 0} atendidas
+            </Badge>
+          </div>
+        </div>
+      </CardHeader>
+
+      <CardContent className="space-y-4">
+        {/* Información básica */}
+        <div className="grid grid-cols-2 gap-3 text-sm">
+          {patient.phone && (
+            <div className="flex items-center gap-2 text-gray-600">
+              <Phone className="h-4 w-4" />
+              <span>{patient.phone}</span>
+            </div>
+          )}
+          {patient.dateOfBirth && (
+            <div className="flex items-center gap-2 text-gray-600">
+              <Calendar className="h-4 w-4" />
+              <span>
+                {format(new Date(patient.dateOfBirth), "dd/MM/yyyy", { locale: es })}
+              </span>
+            </div>
+          )}
+          {patient.gender && (
+            <div className="flex items-center gap-2 text-gray-600">
+              <User className="h-4 w-4" />
+              <span>{getGenderLabel(patient.gender)}</span>
+            </div>
+          )}
+          {patient.appointmentInfo && (
+            <div className="flex items-center gap-2 text-gray-600 col-span-2">
+              <CalendarClock className="h-4 w-4" />
+              <span className="text-xs">
+                Primera cita: {format(new Date(patient.appointmentInfo.firstAppointment), "dd/MM/yyyy", { locale: es })}
+              </span>
+            </div>
+          )}
+        </div>
+
+        {/* Información médica resumida */}
+        {(patient.allergies || patient.currentMedications) && (
+          <div className="border-t pt-3 space-y-2">
+            {patient.allergies && (
+              <div className="flex items-start gap-2 text-sm">
+                <AlertCircle className="h-4 w-4 text-red-500 mt-0.5 flex-shrink-0" />
+                <div>
+                  <span className="font-medium text-gray-700">Alergias: </span>
+                  <span className="text-gray-600 line-clamp-2">{patient.allergies}</span>
+                </div>
+              </div>
+            )}
+            {patient.currentMedications && (
+              <div className="flex items-start gap-2 text-sm">
+                <Pill className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
+                <div>
+                  <span className="font-medium text-gray-700">Medicamentos: </span>
+                  <span className="text-gray-600 line-clamp-2">{patient.currentMedications}</span>
+                </div>
+              </div>
+            )}
+          </div>
+        )}
+
+        {/* Información de citas */}
+        {patient.appointmentInfo && (
+          <div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
+            <p className="text-sm font-medium text-blue-900 mb-2">Historial de Atención:</p>
+            <div className="grid grid-cols-2 gap-2 text-sm text-blue-700">
+              <div>
+                <span className="font-semibold">{patient.appointmentInfo.totalAppointments}</span> citas totales
+              </div>
+              <div>
+                <span className="font-semibold">{patient.appointmentInfo.completedAppointments}</span> completadas
+              </div>
+              <div className="col-span-2 text-xs text-blue-600 mt-1">
+                Última cita: {format(new Date(patient.appointmentInfo.lastAppointment), "dd/MM/yyyy", { locale: es })}
+              </div>
+            </div>
+          </div>
+        )}
+
+        {/* Estadísticas */}
+        <div className="flex gap-4 text-sm border-t pt-3">
+          <div className="flex items-center gap-2 text-gray-600">
+            <FileText className="h-4 w-4" />
+            <span>{patient._count?.records || 0} registros</span>
+          </div>
+          <div className="flex items-center gap-2 text-gray-600">
+            <Stethoscope className="h-4 w-4" />
+            <span>{patient._count?.patientAppointments || 0} citas</span>
+          </div>
+        </div>
+
+        {/* Acciones */}
+        <div className="flex gap-2 pt-2">
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={() => onViewRecords(patient.id)}
+            className="flex-1"
+          >
+            <FileText className="h-4 w-4 mr-2" />
+            Ver Historial
+          </Button>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={() => onViewAppointments(patient.id)}
+            className="flex-1"
+          >
+            <Calendar className="h-4 w-4 mr-2" />
+            Ver Citas
+          </Button>
+        </div>
+      </CardContent>
+    </Card>
+  )
+}

+ 130 - 0
src/components/patients/PatientsSearch.tsx

@@ -0,0 +1,130 @@
+"use client"
+
+import { Card, CardContent } from "@/components/ui/card"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Search, X, Filter } from "lucide-react"
+import { useState } from "react"
+
+interface PatientsSearchProps {
+  searchTerm: string
+  onSearchChange: (value: string) => void
+  totalPatients: number
+  filteredCount: number
+  showFilters?: boolean
+}
+
+export default function PatientsSearch({
+  searchTerm,
+  onSearchChange,
+  totalPatients,
+  filteredCount,
+  showFilters = false
+}: PatientsSearchProps) {
+  const [activeFilter, setActiveFilter] = useState<string>("all")
+
+  const handleClearSearch = () => {
+    onSearchChange("")
+  }
+
+  const isFiltering = searchTerm.length > 0
+
+  return (
+    <Card>
+      <CardContent className="pt-6 space-y-4">
+        {/* Barra de búsqueda principal */}
+        <div className="relative">
+          <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
+          <Input
+            placeholder="Buscar por nombre, apellido, email o teléfono..."
+            value={searchTerm}
+            onChange={(e) => onSearchChange(e.target.value)}
+            className="pl-10 pr-10 h-11"
+          />
+          {searchTerm && (
+            <button
+              onClick={handleClearSearch}
+              className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
+            >
+              <X className="h-4 w-4" />
+            </button>
+          )}
+        </div>
+
+        {/* Filtros rápidos y estadísticas */}
+        <div className="flex items-center justify-between flex-wrap gap-3">
+          <div className="flex items-center gap-2 flex-wrap">
+            {showFilters && (
+              <>
+                <span className="text-sm text-gray-600 font-medium">Filtros:</span>
+                <Badge
+                  variant={activeFilter === "all" ? "default" : "outline"}
+                  className="cursor-pointer hover:bg-primary/90"
+                  onClick={() => setActiveFilter("all")}
+                >
+                  Todos
+                </Badge>
+                <Badge
+                  variant={activeFilter === "recent" ? "default" : "outline"}
+                  className="cursor-pointer hover:bg-primary/90"
+                  onClick={() => setActiveFilter("recent")}
+                >
+                  Recientes
+                </Badge>
+                <Badge
+                  variant={activeFilter === "active" ? "default" : "outline"}
+                  className="cursor-pointer hover:bg-primary/90"
+                  onClick={() => setActiveFilter("active")}
+                >
+                  Activos
+                </Badge>
+              </>
+            )}
+          </div>
+
+          {/* Contador de resultados */}
+          <div className="flex items-center gap-2">
+            {isFiltering ? (
+              <div className="flex items-center gap-2">
+                <span className="text-sm text-gray-600">
+                  Mostrando <span className="font-semibold text-primary">{filteredCount}</span> de{" "}
+                  <span className="font-semibold">{totalPatients}</span> pacientes
+                </span>
+                {filteredCount === 0 && (
+                  <Badge variant="secondary" className="text-xs">
+                    Sin resultados
+                  </Badge>
+                )}
+              </div>
+            ) : (
+              <span className="text-sm text-gray-600">
+                <span className="font-semibold text-primary">{totalPatients}</span> pacientes en total
+              </span>
+            )}
+          </div>
+        </div>
+
+        {/* Sugerencias de búsqueda */}
+        {!isFiltering && totalPatients > 0 && (
+          <div className="pt-2 border-t">
+            <div className="flex items-center gap-2 text-xs text-gray-500">
+              <Filter className="h-3 w-3" />
+              <span>Tip: Puedes buscar por nombre, apellido, email o información médica</span>
+            </div>
+          </div>
+        )}
+
+        {/* Mensaje cuando no hay resultados */}
+        {isFiltering && filteredCount === 0 && (
+          <div className="pt-2 border-t">
+            <div className="text-sm text-amber-600 bg-amber-50 rounded-lg p-3 flex items-center gap-2">
+              <Search className="h-4 w-4" />
+              <span>No se encontraron pacientes con &quot;{searchTerm}&quot;</span>
+            </div>
+          </div>
+        )}
+      </CardContent>
+    </Card>
+  )
+}

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

@@ -96,6 +96,11 @@ export default function SidebarNavigation({ onItemClick, isCollapsed = false }:
               href: "/admin",
               icon: Users
             },
+            {
+              title: "Gestión de Citas",
+              href: "/appointments/doctor",
+              icon: Calendar
+            },
             {
               title: "Todos los Reportes",
               href: "/records",
@@ -126,7 +131,7 @@ export default function SidebarNavigation({ onItemClick, isCollapsed = false }:
           items: [
             {
               title: "Mis Pacientes",
-              href: "/admin",
+              href: "/patients",
               icon: Users
             },
             {

+ 60 - 0
src/components/ui/PageHeader.tsx

@@ -0,0 +1,60 @@
+import { ReactNode } from "react"
+import { LucideIcon } from "lucide-react"
+
+interface PageHeaderProps {
+  title: string
+  description?: string
+  icon?: LucideIcon
+  actions?: ReactNode
+  stats?: {
+    value: string | number
+    label: string
+  }
+}
+
+export default function PageHeader({ 
+  title, 
+  description, 
+  icon: Icon, 
+  actions,
+  stats 
+}: PageHeaderProps) {
+  return (
+    <div className="mb-6">
+      <div className="bg-card rounded-xl p-6 border shadow-sm">
+        <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
+          <div className="flex items-center space-x-3 flex-1 min-w-0">
+            {Icon && (
+              <div className="w-10 h-10 flex-shrink-0 bg-primary rounded-lg flex items-center justify-center shadow-sm">
+                <Icon className="w-5 h-5 text-primary-foreground" />
+              </div>
+            )}
+            <div className="min-w-0">
+              <h1 className="text-xl font-bold text-foreground">
+                {title}
+              </h1>
+              {description && (
+                <p className="text-sm text-muted-foreground">
+                  {description}
+                </p>
+              )}
+            </div>
+          </div>
+          <div className="flex items-center gap-3">
+            {stats && (
+              <div className="bg-muted rounded-lg p-3 shadow-sm border">
+                <div className="text-2xl font-bold text-primary">{stats.value}</div>
+                <div className="text-xs text-muted-foreground font-medium">{stats.label}</div>
+              </div>
+            )}
+            {actions && (
+              <div className="flex-shrink-0">
+                {actions}
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 48 - 0
src/hooks/useRecentActivity.ts

@@ -0,0 +1,48 @@
+import { useState, useEffect } from "react"
+
+export interface ActivityEvent {
+  id: string
+  type: 'user_registered' | 'report_created' | 'appointment_created' | 'appointment_completed'
+  title: string
+  description: string
+  timestamp: Date
+  color: 'green' | 'blue' | 'purple' | 'orange'
+}
+
+interface RecentActivityResponse {
+  activities: ActivityEvent[]
+  total: number
+}
+
+export function useRecentActivity(limit: number = 10) {
+  const [activities, setActivities] = useState<ActivityEvent[]>([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState<string | null>(null)
+
+  useEffect(() => {
+    const fetchActivity = async () => {
+      try {
+        setLoading(true)
+        setError(null)
+
+        const response = await fetch(`/api/admin/recent-activity?limit=${limit}`)
+
+        if (!response.ok) {
+          throw new Error("Error al cargar actividad reciente")
+        }
+
+        const data: RecentActivityResponse = await response.json()
+        setActivities(data.activities)
+      } catch (err) {
+        console.error("Error fetching recent activity:", err)
+        setError(err instanceof Error ? err.message : "Error desconocido")
+      } finally {
+        setLoading(false)
+      }
+    }
+
+    fetchActivity()
+  }, [limit])
+
+  return { activities, loading, error }
+}