Perfil + onboarding + home reformulado + fixes
Perfil de usuario: - /perfil con avatar, matrícula, semestre, tutor, foto de perfil - /onboarding opcional (skippeable) para primer login - Avatar con prioridad: foto propia > Google OAuth > iniciales sobre color HSL - Endpoint PATCH /api/profile con validación y guard self-only - Middleware carga semestre, tutor_id, foto_path - Sidebar footer con Avatar + link a perfil CRUD admin de maestros: - /admin/maestros con tabla desktop / cards mobile - MaestroForm + EliminarMaestro (mismo patrón que categorias) - Endpoints POST/PATCH/DELETE con guard admin + 23503 traducido - Subnav de inventario ahora incluye tab Maestros Home reformulado: - Alumno: saludo por hora + CTA "¿Qué vas a pedir hoy?" + card de último préstamo activo - Admin: saludo + 2 KPIs de alerta (Pendientes, Vencidos) + últimas 2 pendientes + actividad reciente (tabla desktop, cards mobile — fix del bug 6) - Helper saludoHora() y fmtFechaRelativa() en date.ts con offset MX Checkout con tutor auto: - SolicitudCart: input maestro → textarea con placeholder = nombre del tutor - Guard perfil incompleto: CTA "Completa tu perfil" si !matricula || !tutor_id - RPC crear_solicitud: fallback automático al nombre del tutor si textarea vacío Fixes: - Bug upload imagen: Dockerfile ARG + docker-compose build.args pasan PUBLIC_* al bundle client de Vite - Grid inventario mobile: acciones en grid 2-col con prop compact en botones - UnidadesManager mobile: tabla reemplazada por cards, select estado con min-width 130px - Chart donut: Legend horizontal debajo (sin labels internos cortadas) - Chart barras: YAxis width fijo 100 + tickFormatter que trunca >14 chars - Panel admin mobile: tabla ahora tiene versión cards mobile (parte del rediseño home)
This commit is contained in:
@@ -39,10 +39,18 @@ export function useCart() {
|
||||
|
||||
const clamp = (n: number, max: number) => Math.max(1, Math.min(max, n));
|
||||
|
||||
export default function CartProvider({ materiales }: { materiales: Material[] }) {
|
||||
export default function CartProvider({
|
||||
materiales,
|
||||
tutorNombre = null,
|
||||
perfilCompleto = true,
|
||||
}: {
|
||||
materiales: Material[];
|
||||
tutorNombre?: string | null;
|
||||
perfilCompleto?: boolean;
|
||||
}) {
|
||||
const [cart, setCart] = useState<Map<number, CartItem>>(new Map());
|
||||
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||||
const firstFieldRef = useRef<HTMLInputElement | null>(null);
|
||||
const firstFieldRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const [maestroResponsable, setMaestroResponsable] = useState('');
|
||||
const [notas, setNotas] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -129,7 +137,7 @@ export default function CartProvider({ materiales }: { materiales: Material[] })
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (loading || items.length === 0) return;
|
||||
if (loading || items.length === 0 || !perfilCompleto) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -151,10 +159,13 @@ export default function CartProvider({ materiales }: { materiales: Material[] })
|
||||
throw new Error(json?.error ?? 'No se pudo enviar la solicitud');
|
||||
}
|
||||
setOk(true);
|
||||
const usedFallback = maestroResponsable.trim() === '' && !!tutorNombre;
|
||||
toastAfterReload({
|
||||
kind: 'success',
|
||||
title: 'Solicitud enviada',
|
||||
description: `${items.length} material${items.length === 1 ? '' : 'es'} · pendiente de aprobación`,
|
||||
description: usedFallback
|
||||
? `${items.length} material${items.length === 1 ? '' : 'es'} · maestro: ${tutorNombre} (tu tutor)`
|
||||
: `${items.length} material${items.length === 1 ? '' : 'es'} · pendiente de aprobación`,
|
||||
});
|
||||
setTimeout(() => {
|
||||
dialogRef.current?.close();
|
||||
@@ -308,16 +319,27 @@ export default function CartProvider({ materiales }: { materiales: Material[] })
|
||||
|
||||
<div>
|
||||
<label className="label" htmlFor="maestro-responsable">Maestro responsable</label>
|
||||
<input
|
||||
<textarea
|
||||
ref={firstFieldRef}
|
||||
id="maestro-responsable"
|
||||
className="input"
|
||||
type="text"
|
||||
required
|
||||
rows={2}
|
||||
maxLength={200}
|
||||
value={maestroResponsable}
|
||||
onChange={(e) => setMaestroResponsable(e.target.value)}
|
||||
placeholder={tutorNombre ?? 'Escribe el nombre del maestro responsable'}
|
||||
aria-describedby="maestro-hint"
|
||||
style={{ minHeight: '64px' }}
|
||||
/>
|
||||
{tutorNombre ? (
|
||||
<p id="maestro-hint" className="text-xs opacity-70 mt-1">
|
||||
Si dejas esto vacío, se usará tu tutor: <strong>{tutorNombre}</strong>.
|
||||
</p>
|
||||
) : (
|
||||
<p id="maestro-hint" className="text-xs opacity-60 mt-1">
|
||||
Debes escribir un maestro; no tienes tutor guardado.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -349,14 +371,21 @@ export default function CartProvider({ materiales }: { materiales: Material[] })
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<button type="button" className="btn btn-ghost" onClick={closeCheckout} disabled={loading}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading || ok || items.length === 0}>
|
||||
{loading ? 'Enviando…' : 'Enviar solicitud'}
|
||||
</button>
|
||||
</div>
|
||||
{!perfilCompleto ? (
|
||||
<div className="card p-3">
|
||||
<p className="text-sm mb-3">Necesitas completar tu perfil (matrícula + tutor) para crear vales.</p>
|
||||
<a href="/perfil" className="btn btn-primary">Completar perfil</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<button type="button" className="btn btn-ghost" onClick={closeCheckout} disabled={loading}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading || ok || items.length === 0}>
|
||||
{loading ? 'Enviando…' : 'Enviar solicitud'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</dialog>
|
||||
</CartContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user