UX: motivo requerido, dock móvil, hamburguesa desktop, toaster Sileo, panel de estadísticas
Alumno: - Modal de solicitud: campo "Notas" renombrado a "Motivo del préstamo", ahora requerido, con hint sobre uso (para qué clase o proyecto). Admin: - Bandeja de solicitudes pendientes, activos y devoluciones ahora muestran el motivo del alumno (en tabla desktop y tarjetas móvil). - /admin/inventario se convirtió en "Estadísticas": grid de KPIs del día (solicitadas, aprobadas, rechazadas, devoluciones, en préstamo, vencidos) arriba del inventario existente. Los cambios de estado se cuentan desde audit_log, el rango del día se calcula automáticamente en timezone America/Tijuana con DST via Intl API (src/lib/date.ts). Rediseño móvil: - Sidebar desktop se puede colapsar con botón hamburguesa. Estado persiste en localStorage sin flash (script is:inline bloqueante). - Nav móvil reemplazado por dock píldora flotante con iconos SVG inline (heroicons outline). Respeta safe-area-inset-bottom. Notificaciones estilo Sileo: - Componente Toaster global (React island client:idle) que escucha CustomEvents. Toasts con backdrop-blur, animación de entrada translateY+opacity, auto-dismiss 3.8s, click para cerrar. Helper toastAfterReload deja pendiente en sessionStorage para sobrevivir location.reload() post-acción. Diagnóstico: - /api/whoami devuelve user+profile actuales para depurar sesión. - Endpoint devolver retorna un objeto `debug` en el 403 con email+rol vistos por el server (temporal, para trazar bug del 403). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast, toastAfterReload } from '@/lib/toast';
|
||||
|
||||
type Props = {
|
||||
material: {
|
||||
@@ -60,12 +61,19 @@ export default function SolicitarModal({ material }: Props) {
|
||||
throw new Error(json?.error ?? 'No se pudo enviar la solicitud');
|
||||
}
|
||||
setOk(true);
|
||||
toastAfterReload({
|
||||
kind: 'success',
|
||||
title: 'Solicitud enviada',
|
||||
description: `${material.nombre} · pendiente de aprobación`,
|
||||
});
|
||||
setTimeout(() => {
|
||||
dialogRef.current?.close();
|
||||
location.reload();
|
||||
}, 800);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error inesperado');
|
||||
const msg = err instanceof Error ? err.message : 'Error inesperado';
|
||||
setError(msg);
|
||||
toast({ kind: 'error', title: 'No se pudo enviar', description: msg });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -126,17 +134,22 @@ export default function SolicitarModal({ material }: Props) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label" htmlFor={`notas-${material.id}`}>Notas (opcional)</label>
|
||||
<label className="label" htmlFor={`notas-${material.id}`}>Motivo del préstamo</label>
|
||||
<textarea
|
||||
id={`notas-${material.id}`}
|
||||
className="input"
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
value={notas}
|
||||
required
|
||||
onChange={(e) => setNotas(e.target.value)}
|
||||
placeholder="Motivo, materia, fecha estimada de devolución…"
|
||||
placeholder="¿Para qué clase o proyecto lo necesitas?"
|
||||
style={{ minHeight: '96px' }}
|
||||
aria-describedby={`notas-hint-${material.id}`}
|
||||
/>
|
||||
<p id={`notas-hint-${material.id}`} className="text-xs opacity-60 mt-1">
|
||||
Ejemplo: Clase de Electrónica Analógica · Prof. Gómez · práctica 3.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user