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:
2026-08-24 08:46:39 -07:00
parent ef40241ade
commit e159e8d297
28 changed files with 1533 additions and 218 deletions
+24 -1
View File
@@ -6,6 +6,29 @@ import BuscadorCatalogo from '@/components/alumno/BuscadorCatalogo.tsx';
const qInitial = Astro.url.searchParams.get('q') ?? '';
const supabase = Astro.locals.supabase;
const userId = Astro.locals.user?.id;
// Trae matricula + tutor_id directo (el middleware sólo hidrata columnas antiguas).
let matricula: string | null = null;
let tutorId: number | null = null;
if (userId) {
const { data: p } = await supabase
.from('profiles')
.select('matricula, tutor_id')
.eq('id', userId)
.maybeSingle();
matricula = p?.matricula ?? null;
tutorId = (p as { tutor_id?: number | null } | null)?.tutor_id ?? null;
}
let tutorNombre: string | null = null;
if (tutorId) {
const { data } = await supabase.from('maestros').select('nombre').eq('id', tutorId).maybeSingle();
tutorNombre = data?.nombre ?? null;
}
const perfilCompleto = !!(matricula && tutorId);
type Material = {
id: number;
nombre: string;
@@ -72,7 +95,7 @@ const categoriasFiltro = grupoList
Ningún material en esta categoría. Prueba con otra.
</p>
<CartProvider materiales={materiales} client:load />
<CartProvider materiales={materiales} tutorNombre={tutorNombre} perfilCompleto={perfilCompleto} client:load />
</>
)}
</div>