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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user