Răsfoiți Sursa

ux improvements p2

Matthew Trejo 2 luni în urmă
părinte
comite
ae770fc915

+ 37 - 7
src/app/appointments/page.tsx

@@ -8,12 +8,16 @@ import { AppointmentsHeader } from "@/components/appointments/AppointmentsHeader
 import { AppointmentsStats } from "@/components/appointments/AppointmentsStats";
 import { AppointmentsFilter, type AppointmentFilter } from "@/components/appointments/AppointmentsFilter";
 import { AppointmentsGrid } from "@/components/appointments/AppointmentsGrid";
+import { Pagination } from "@/components/ui/pagination";
 import { useAppointments } from "@/hooks/useAppointments";
 import { Loader2 } from "lucide-react";
 
+const ITEMS_PER_PAGE = 5;
+
 export default function AppointmentsPage() {
   const { data: session, status } = useSession();
   const [currentFilter, setCurrentFilter] = useState<AppointmentFilter>("all");
+  const [currentPage, setCurrentPage] = useState(1);
 
   const {
     appointments,
@@ -42,6 +46,20 @@ export default function AppointmentsPage() {
     return appointments;
   }, [appointments, currentFilter]);
 
+  // Calcular paginación
+  const totalPages = Math.ceil(filteredAppointments.length / ITEMS_PER_PAGE);
+  const paginatedAppointments = useMemo(() => {
+    const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
+    const endIndex = startIndex + ITEMS_PER_PAGE;
+    return filteredAppointments.slice(startIndex, endIndex);
+  }, [filteredAppointments, currentPage]);
+
+  // Resetear a la página 1 cuando cambia el filtro
+  const handleFilterChange = (filter: AppointmentFilter) => {
+    setCurrentFilter(filter);
+    setCurrentPage(1);
+  };
+
   const filterMessages: Record<AppointmentFilter, string> = {
     all: "No tienes citas registradas. Usa el chatbot para obtener recomendaciones médicas y agendar citas.",
     pending: "No tienes citas pendientes de aprobación.",
@@ -88,7 +106,7 @@ export default function AppointmentsPage() {
         <div className="flex items-center justify-between">
           <AppointmentsFilter
             currentFilter={currentFilter}
-            onFilterChange={setCurrentFilter}
+            onFilterChange={handleFilterChange}
             counts={{
               all: stats.total,
               pending: stats.pending,
@@ -105,12 +123,24 @@ export default function AppointmentsPage() {
             <Loader2 className="h-8 w-8 animate-spin" />
           </div>
         ) : (
-          <AppointmentsGrid
-            appointments={filteredAppointments}
-            userRole="PATIENT"
-            onCancel={cancelAppointment}
-            emptyMessage={filterMessages[currentFilter]}
-          />
+          <>
+            <AppointmentsGrid
+              appointments={paginatedAppointments}
+              userRole="PATIENT"
+              onCancel={cancelAppointment}
+              emptyMessage={filterMessages[currentFilter]}
+            />
+
+            {/* Pagination */}
+            {totalPages > 1 && (
+              <Pagination
+                currentPage={currentPage}
+                totalPages={totalPages}
+                onPageChange={setCurrentPage}
+                className="mt-6"
+              />
+            )}
+          </>
         )}
       </div>
     </AuthenticatedLayout>

+ 0 - 9
src/app/auth/login/page.tsx

@@ -131,15 +131,6 @@ export default function LoginPage() {
               </Button>
             </div>
           </form>
-          
-          <div className="mt-6 text-center">
-            <p className="text-sm text-gray-600">
-              ¿No tienes una cuenta?{" "}
-              <Link href="/auth/register" className="text-blue-600 hover:underline">
-                Regístrate aquí
-              </Link>
-            </p>
-          </div>
 
           <div className="mt-6 p-4 bg-blue-50 rounded-lg">
             <div className="flex items-center">

+ 17 - 1
src/components/appointments/AppointmentCard.tsx

@@ -7,6 +7,7 @@ import { format } from "date-fns";
 import { es } from "date-fns/locale";
 import Link from "next/link";
 import type { Appointment } from "@/types/appointments";
+import { canJoinMeeting } from "@/utils/appointments";
 
 interface AppointmentCardProps {
   appointment: Appointment;
@@ -26,6 +27,7 @@ export const AppointmentCard = ({
   const hasFecha = appointment.fechaSolicitada !== null;
   const fecha = hasFecha ? new Date(appointment.fechaSolicitada!) : null;
   const otherUser = userRole === "PATIENT" ? appointment.medico : appointment.paciente;
+  const meetingStatus = canJoinMeeting(appointment.fechaSolicitada);
   
   return (
     <Card className="hover:shadow-md transition-shadow">
@@ -115,7 +117,7 @@ export const AppointmentCard = ({
             </Button>
           )}
 
-          {appointment.estado === "APROBADA" && (
+          {appointment.estado === "APROBADA" && meetingStatus.canJoin && (
             <Button asChild className="flex-1" size="sm">
               <Link href={`/appointments/${appointment.id}/meet`}>
                 Unirse a la consulta
@@ -123,6 +125,20 @@ export const AppointmentCard = ({
             </Button>
           )}
 
+          {appointment.estado === "APROBADA" && !meetingStatus.canJoin && meetingStatus.reason && (
+            <Button
+              disabled
+              variant="outline"
+              className="flex-1"
+              size="sm"
+              title={meetingStatus.reason}
+            >
+              {meetingStatus.minutesUntil !== undefined 
+                ? `Disponible en ${Math.floor(meetingStatus.minutesUntil / 60)}h ${meetingStatus.minutesUntil % 60}m`
+                : meetingStatus.reason}
+            </Button>
+          )}
+
           <Button asChild variant="outline" size="sm">
             <Link href={`/appointments/${appointment.id}`}>
               Ver detalles

+ 58 - 59
src/components/chatbot/ChatInterface.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
 import { useSession } from "next-auth/react";
 import { useChat, MAX_MESSAGES } from "@/hooks/useChat";
 import { useChatEffects } from "@/hooks/useChatEffects";
@@ -25,6 +25,7 @@ interface ChatInterfaceProps {
 
 export const ChatInterface = ({ chatType }: ChatInterfaceProps) => {
   const { data: session } = useSession();
+  const chatContainerRef = useRef<HTMLDivElement>(null);
   const [showReportModal, setShowReportModal] = useState(false);
   const [showResetModal, setShowResetModal] = useState(false);
   const [showLastMessageToast, setShowLastMessageToast] = useState(false);
@@ -158,9 +159,9 @@ export const ChatInterface = ({ chatType }: ChatInterfaceProps) => {
   }
 
   return (
-    <div className="min-h-screen flex items-center justify-center p-6">
-      <div className="w-full max-w-6xl mx-auto">
-        <div className="bg-card rounded-xl shadow-lg border border-border">
+    <div className="h-screen flex items-center justify-center p-6 overflow-hidden">
+      <div className="w-full max-w-6xl mx-auto h-full flex items-center">
+        <div className="bg-card rounded-xl shadow-lg border border-border w-full h-[90vh] flex flex-col">
           {/* Header */}
           <ChatHeader
             remainingMessages={remainingMessages}
@@ -193,68 +194,66 @@ export const ChatInterface = ({ chatType }: ChatInterfaceProps) => {
             />
           )}
 
-          <div className="flex flex-col min-h-[70vh]">
-            {/* Chat Content */}
-            <div className="flex-1 overflow-hidden">
-              <div className="h-full overflow-y-auto p-6 space-y-6">
-                {/* Completed Banner */}
-                {showCompletedBanner && (
-                  <CompletedBanner
-                    isGeneratingReport={isGeneratingReport}
-                    onDismiss={dismissCompletedBanner}
-                  />
-                )}
-
-                {/* Welcome Message */}
-                {messages.length === 0 && (
-                  <WelcomeMessage maxMessages={MAX_MESSAGES} chatType={chatType} />
-                )}
-
-                {/* Suggested Prompts - solo para chat médico */}
-                {chatType === "medical" && showSuggestions && messages.length === 0 && (
-                  <SuggestedPrompts
-                    onSuggestionClick={handleSuggestionClick}
-                    isLoading={isLoading}
-                  />
-                )}
+          {/* Chat Content - con scroll interno */}
+          <div ref={chatContainerRef} className="flex-1 overflow-y-auto p-6 space-y-6">
+            {/* Completed Banner */}
+            {showCompletedBanner && (
+              <CompletedBanner
+                isGeneratingReport={isGeneratingReport}
+                onDismiss={dismissCompletedBanner}
+              />
+            )}
 
-                {/* Messages */}
-                <ChatMessages 
-                  messages={messages} 
-                  isLoading={isLoading} 
-                  showDynamicSuggestions={showDynamicSuggestions}
-                />
-                
-                {/* Dynamic Suggestions */}
-                {showDynamicSuggestions && currentSuggestions.length > 0 && !isLoading && (
-                  <DynamicSuggestions
-                    suggestions={currentSuggestions}
-                    onSuggestionClick={handleSuggestionClick}
-                    isLoading={isLoading}
-                  />
-                )}
-              </div>
-            </div>
+            {/* Welcome Message */}
+            {messages.length === 0 && (
+              <WelcomeMessage maxMessages={MAX_MESSAGES} chatType={chatType} />
+            )}
 
-            {/* Input Area */}
-            {(chatType === "psychological" || messageCount < MAX_MESSAGES) && (
-              <ChatInput
-                inputMessage={inputMessage}
-                setInputMessage={setInputMessage}
-                onSendMessage={handleSendMessage}
-                isLimitReached={chatType === "medical" && messageCount >= MAX_MESSAGES}
-                isLoading={isLoading || inputDisabledForSuggestions}
-                chatType={chatType}
-                onEndConversation={chatType === "psychological" ? handleResetClick : undefined}
-                hasMessages={messages.length > 0}
+            {/* Suggested Prompts - solo para chat médico */}
+            {chatType === "medical" && showSuggestions && messages.length === 0 && (
+              <SuggestedPrompts
+                onSuggestionClick={handleSuggestionClick}
+                isLoading={isLoading}
               />
             )}
 
-            {/* Reset Button - solo para médico */}
-            {chatType === "medical" && messageCount >= MAX_MESSAGES && (
-              <ResetButton onReset={handleResetWithReportAndModal} />
+            {/* Messages */}
+            <ChatMessages 
+              messages={messages} 
+              isLoading={isLoading} 
+              showDynamicSuggestions={showDynamicSuggestions}
+              chatContainerRef={chatContainerRef}
+            />
+            
+            {/* Dynamic Suggestions */}
+            {showDynamicSuggestions && currentSuggestions.length > 0 && !isLoading && (
+              <DynamicSuggestions
+                suggestions={currentSuggestions}
+                onSuggestionClick={handleSuggestionClick}
+                isLoading={isLoading}
+                chatContainerRef={chatContainerRef}
+              />
             )}
           </div>
+
+          {/* Input Area */}
+          {(chatType === "psychological" || messageCount < MAX_MESSAGES) && (
+            <ChatInput
+              inputMessage={inputMessage}
+              setInputMessage={setInputMessage}
+              onSendMessage={handleSendMessage}
+              isLimitReached={chatType === "medical" && messageCount >= MAX_MESSAGES}
+              isLoading={isLoading || inputDisabledForSuggestions}
+              chatType={chatType}
+              onEndConversation={chatType === "psychological" ? handleResetClick : undefined}
+              hasMessages={messages.length > 0}
+            />
+          )}
+
+          {/* Reset Button - solo para médico */}
+          {chatType === "medical" && messageCount >= MAX_MESSAGES && (
+            <ResetButton onReset={handleResetWithReportAndModal} />
+          )}
         </div>
       </div>
 

+ 9 - 6
src/components/chatbot/ChatMessages.tsx

@@ -1,4 +1,4 @@
-import { useRef, useEffect, useCallback } from "react";
+import { useRef, useEffect, useCallback, RefObject } from "react";
 import { Message } from "./types";
 import { ChatMessage } from "./ChatMessage";
 import { DynamicLoader } from "./DynamicLoader";
@@ -7,24 +7,27 @@ interface ChatMessagesProps {
   messages: Message[];
   isLoading: boolean;
   showDynamicSuggestions?: boolean;
+  chatContainerRef?: RefObject<HTMLDivElement | null>;
 }
 
 export const ChatMessages = ({ 
   messages, 
   isLoading, 
-  showDynamicSuggestions
+  showDynamicSuggestions,
+  chatContainerRef
 }: ChatMessagesProps) => {
   const messagesEndRef = useRef<HTMLDivElement>(null);
 
   // Scroll suave cuando se agregan nuevos mensajes
   const scrollToBottom = useCallback(() => {
-    if (messagesEndRef.current) {
-      messagesEndRef.current.scrollIntoView({
+    if (chatContainerRef?.current) {
+      // Hacer scroll del contenedor específico, no de la ventana
+      chatContainerRef.current.scrollTo({
+        top: chatContainerRef.current.scrollHeight,
         behavior: "smooth",
-        block: "end",
       });
     }
-  }, []);
+  }, [chatContainerRef]);
 
   // Hacer scroll cuando se agregan nuevos mensajes o aparecen sugerencias dinámicas
   useEffect(() => {

+ 11 - 6
src/components/chatbot/DynamicSuggestions.tsx

@@ -2,13 +2,14 @@ import { Button } from "@/components/ui/button";
 import { Sparkles } from "lucide-react";
 import { SuggestedPrompt } from "./types";
 import { cn } from "@/lib/utils";
-import { useEffect } from "react";
+import { useEffect, RefObject } from "react";
 
 interface DynamicSuggestionsProps {
   suggestions: SuggestedPrompt[];
   onSuggestionClick: (prompt: string) => void;
   isLoading: boolean;
   className?: string;
+  chatContainerRef?: RefObject<HTMLDivElement | null>;
 }
 
 export const DynamicSuggestions = ({
@@ -16,14 +17,18 @@ export const DynamicSuggestions = ({
   onSuggestionClick,
   isLoading,
   className,
+  chatContainerRef,
 }: DynamicSuggestionsProps) => {
   // Scroll automático cuando aparecen las sugerencias
   useEffect(() => {
     const scrollToBottom = () => {
-      window.scrollTo({
-        top: document.documentElement.scrollHeight,
-        behavior: 'smooth'
-      });
+      if (chatContainerRef?.current) {
+        // Hacer scroll del contenedor específico, no de la ventana
+        chatContainerRef.current.scrollTo({
+          top: chatContainerRef.current.scrollHeight,
+          behavior: 'smooth'
+        });
+      }
     };
     
     // Múltiples intentos de scroll para asegurar que funcione
@@ -37,7 +42,7 @@ export const DynamicSuggestions = ({
     return () => {
       timeouts.forEach(timeout => clearTimeout(timeout));
     };
-  }, []);
+  }, [chatContainerRef]);
 
   if (!suggestions || suggestions.length === 0) {
     return null;