From 212f51cf9e065eb6fd617ffe706b71d6a68c7ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Consultor=C3=ADa=20Alcaraz=20Salazar?= Date: Wed, 9 Sep 2026 10:37:15 +0000 Subject: [PATCH] Login: mensaje amigable y ojo de password; agenda: click en celda libre para agendar (estilo Google Calendar) --- frontend/src/pages/Agenda.tsx | 61 +++++++++++++++++++++++++++++++---- frontend/src/pages/Login.tsx | 47 +++++++++++++++++++-------- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/Agenda.tsx b/frontend/src/pages/Agenda.tsx index f0b6c8d..4024362 100644 --- a/frontend/src/pages/Agenda.tsx +++ b/frontend/src/pages/Agenda.tsx @@ -301,6 +301,8 @@ const Agenda: FC = () => { const apt = dragApt; setDragApt(null); setDropTarget(null); + // Evita que el click posterior al drop abra el modal de nueva cita + justDroppedRef.current = true; if ((apt.doctor_id ?? null) === doctorId && aptSlot(apt.time) === slot) return; const nombreCol = doctorId ? (doctors.find((d) => d.id === doctorId)?.name || '') : 'Sin asignar'; const horaStr = horaFloatStr(slot); @@ -757,6 +759,42 @@ const Agenda: FC = () => { setCreateOpen(true); }; + // Nueva cita precargada desde una celda libre del grid (fecha/hora/médico + // del slot clicado). Se usa un solo click (el drag&drop se distingue con + // justDroppedRef: tras un drop real el click posterior se ignora). + const justDroppedRef = useRef(false); + const openCreateAt = (opts?: { date?: string; time?: string; doctorId?: number | null }) => { + setQuickMode(false); + const d = opts?.date ?? date; + const docId = medicoEmployeeId + ? String(medicoEmployeeId) + : (opts?.doctorId != null ? String(opts.doctorId) : doctorFilter); + const t = opts?.time ?? ''; + setForm((f) => ({ ...f, date: d, time: t, doctor_id: docId })); + setQuickForm({ + name: '', + phone: '', + doctor_id: docId, + date: d, + time: t, + notes: '', + }); + setCreateOpen(true); + }; + + const clickCeldaLibre = (e: React.MouseEvent, dateStr: string, slot: number | null, doctorId: number | null) => { + if (justDroppedRef.current) { + justDroppedRef.current = false; + return; + } + e.preventDefault(); + openCreateAt({ + date: dateStr, + time: slot != null ? horaFloatStr(slot) : '', + doctorId, + }); + }; + const submitQuick = async () => { if (!quickForm.name.trim() || !quickForm.phone.trim()) { toast.error('Nombre y teléfono son obligatorios'); @@ -1270,11 +1308,15 @@ const Agenda: FC = () => { Bloqueado{bloqueo.motivo ? ` — ${bloqueo.motivo}` : ''} ) : cell.length === 0 ? ( -
+
clickCeldaLibre(e, date, slot, col.doctorId)} + className={`cursor-pointer rounded-lg border py-1.5 text-center text-[10px] font-medium transition-colors ${ + quickFilter === 'libres' + ? 'bg-brand-mint border-emerald-700 text-theme-heading font-semibold' + : 'border-dashed border-theme-border-strong text-theme-muted hover:bg-theme-accent-bg' + }`} + title={`Nueva cita aquí — ${horaFloatStr(slot)}`} + > Libre
) : ( @@ -1377,7 +1419,8 @@ const Agenda: FC = () => {
) : cell.length === 0 ? (
clickCeldaLibre(e, date, slot, col.doctorId)} + className={`cursor-pointer rounded border py-1.5 transition-colors ${ quickFilter === 'libres' ? 'bg-brand-mint border-emerald-700' : 'border-dashed border-theme-border hover:bg-theme-accent-bg' @@ -1481,7 +1524,11 @@ const Agenda: FC = () => { {dayBloqueo.motivo || 'Bloqueado'}
) : dayApts.length === 0 ? ( -
+
clickCeldaLibre(e, d, null, col.doctorId)} + className="cursor-pointer rounded border border-dashed border-theme-border py-1.5 text-center text-[10px] text-theme-muted hover:bg-theme-accent-bg" + title="Nueva cita este día" + > Libre
) : ( diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 2a87032..8983305 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,7 +1,7 @@ import type { FC, FormEvent } from 'react'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { LogIn } from 'lucide-react'; +import { Eye, EyeOff, LogIn } from 'lucide-react'; import { Button, Input } from '../components/ui'; import { useAuth } from '../lib/auth'; import { useTheme } from '../lib/theme'; @@ -15,6 +15,7 @@ const Login: FC = () => { const navigate = useNavigate(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const isHomeNest = theme === 'homenest'; @@ -32,7 +33,13 @@ const Login: FC = () => { await login(username.trim(), password); navigate('/', { replace: true }); } catch (err) { - setError(err instanceof Error ? err.message : 'No se pudo iniciar sesión'); + // Mensajes amigables: nunca mostrar errores técnicos crudos. + const msg = err instanceof Error ? err.message : ''; + if (msg.includes('Credenciales inválidas') || msg.includes('401')) { + setError('Usuario o contraseña incorrectos'); + } else { + setError('No se pudo iniciar sesión. Intenta de nuevo.'); + } } finally { setSubmitting(false); } @@ -89,18 +96,30 @@ const Login: FC = () => { } labelClassName={isHomeNest ? 'text-theme-muted' : 'text-white/70'} /> - setPassword(e.target.value)} - autoComplete="current-password" - inputClassName={isHomeNest - ? 'bg-theme-bg border-theme-border-strong text-theme-heading placeholder:text-theme-muted/60 focus:border-theme-accent focus:ring-theme-accent/30' - : 'bg-theme-surface/10 border-white/10 text-white placeholder:text-white/40 focus:border-theme-accent focus:ring-theme-accent/30' - } - labelClassName={isHomeNest ? 'text-theme-muted' : 'text-white/70'} - /> +
+ setPassword(e.target.value)} + autoComplete="current-password" + inputClassName={`pr-10 ${isHomeNest + ? 'bg-theme-bg border-theme-border-strong text-theme-heading placeholder:text-theme-muted/60 focus:border-theme-accent focus:ring-theme-accent/30' + : 'bg-theme-surface/10 border-white/10 text-white placeholder:text-white/40 focus:border-theme-accent focus:ring-theme-accent/30' + }`} + labelClassName={isHomeNest ? 'text-theme-muted' : 'text-white/70'} + /> + +
{error &&

{error}

}