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
+48 -9
View File
@@ -3,15 +3,35 @@ import Layout from './Layout.astro';
import Toaster from '@/components/Toaster.tsx';
import BadgeSolicitudes from '@/components/admin/BadgeSolicitudes.tsx';
import BrandMark from '@/components/BrandMark.astro';
import Avatar from '@/components/profile/Avatar.tsx';
import { avatarInfo } from '@/lib/avatar';
interface Props {
title?: string;
}
const { title } = Astro.props;
const user = Astro.locals.user;
const profile = Astro.locals.profile;
const isAdmin = profile?.rol === 'admin';
const googleAvatarUrl = user
? ((user.user_metadata as Record<string, unknown> | undefined)?.avatar_url as string | undefined) ||
((user.user_metadata as Record<string, unknown> | undefined)?.picture as string | undefined) ||
null
: null;
const avatar = profile
? avatarInfo({
email: profile.email,
nombre: profile.nombre,
fotoPath: profile.foto_path,
googleAvatarUrl,
})
: null;
const perfilIncompleto = profile?.rol === 'alumno' && (!profile.matricula || !profile.tutor_id);
let pendientesCount = 0;
if (isAdmin) {
const { count } = await Astro.locals.supabase
@@ -112,11 +132,24 @@ const activeCheck = (item: NavItem) => {
))}
</nav>
<div class="p-3" style="border-top: 2px solid var(--color-ink);">
<div class="px-3 py-2 text-sm">
<div class="font-medium truncate">{profile?.nombre ?? profile?.email}</div>
<div class="text-xs uppercase tracking-wide" style="color: var(--color-pencil);">{profile?.rol}</div>
</div>
<form method="POST" action="/api/auth/signout">
<a href="/perfil" class="flex items-center gap-3 px-2 py-2 rounded-[2px] hover:bg-[color:var(--color-chalk)] transition-colors">
{avatar && <Avatar client:load info={avatar} size={36} />}
<div class="min-w-0 flex-1">
<div class="text-sm font-medium truncate">{profile?.nombre ?? profile?.email}</div>
<div class="text-xs uppercase tracking-wide flex items-center gap-1.5" style="color: var(--color-pencil);">
<span>Ver perfil</span>
{perfilIncompleto && (
<span
aria-label="Perfil incompleto"
title="Perfil incompleto"
class="inline-block w-2 h-2 rounded-full motion-safe:animate-pulse"
style="background: var(--color-danger);"
></span>
)}
</div>
</div>
</a>
<form method="POST" action="/api/auth/signout" class="mt-1">
<button type="submit" class="w-full text-left px-3 py-2 rounded-[2px] text-sm uppercase tracking-wide hover:bg-[color:var(--color-chalk)] transition-colors flex items-center gap-2">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d={ICONS.logout} />
@@ -135,10 +168,16 @@ const activeCheck = (item: NavItem) => {
</div>
<span class="font-semibold text-sm uppercase tracking-wide">LabPréstamos</span>
</a>
<div class="text-xs truncate max-w-[45%] text-right">
<div class="font-medium truncate">{profile?.nombre ?? profile?.email}</div>
<div class="uppercase tracking-wide" style="color: var(--color-pencil);">{profile?.rol}</div>
</div>
<a href="/perfil" class="relative flex items-center gap-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-ink)] rounded-[2px]" aria-label="Mi perfil">
{avatar && <Avatar client:load info={avatar} size={36} />}
{perfilIncompleto && (
<span
aria-hidden="true"
class="absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full motion-safe:animate-pulse"
style="background: var(--color-danger); border: 1.5px solid var(--color-ink);"
></span>
)}
</a>
</header>
<main id="main" class="flex-1 p-4 md:p-8 pb-24 md:pb-8 md:ml-64 sidebar-aware transition-[margin] duration-200 ease-out">