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:
@@ -0,0 +1,119 @@
|
||||
---
|
||||
import AppLayout from '@/layouts/AppLayout.astro';
|
||||
import MaestroForm from '@/components/admin/maestros/MaestroForm.tsx';
|
||||
import EliminarMaestro from '@/components/admin/maestros/EliminarMaestro.tsx';
|
||||
|
||||
type MaestroRow = {
|
||||
id: number;
|
||||
nombre: string;
|
||||
activo: boolean;
|
||||
alumnos: { count: number }[];
|
||||
};
|
||||
|
||||
const { data, error } = await Astro.locals.supabase
|
||||
.from('maestros')
|
||||
.select('id, nombre, activo, alumnos:profiles!tutor_id(count)')
|
||||
.order('nombre');
|
||||
|
||||
const maestros = ((data ?? []) as unknown as MaestroRow[]).map((m) => ({
|
||||
id: m.id,
|
||||
nombre: m.nombre,
|
||||
activo: m.activo,
|
||||
count: m.alumnos?.[0]?.count ?? 0,
|
||||
}));
|
||||
---
|
||||
<AppLayout title="Maestros — Admin">
|
||||
<div class="max-w-4xl">
|
||||
<header class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">Maestros</h1>
|
||||
<p class="text-sm opacity-70 mt-1">Catálogo de tutores que los alumnos eligen en su perfil.</p>
|
||||
</div>
|
||||
<MaestroForm mode="create" client:load />
|
||||
</header>
|
||||
|
||||
<nav class="flex gap-2 mb-6 border-b" style="border-color: color-mix(in oklab, var(--color-ink) 10%, transparent);" aria-label="Sub-navegación de inventario">
|
||||
<a href="/admin/inventario" class="px-4 py-2 text-sm font-medium border-b-2 border-transparent opacity-70 transition-colors duration-150 hover:opacity-100">
|
||||
Materiales
|
||||
</a>
|
||||
<a href="/admin/inventario/categorias" class="px-4 py-2 text-sm font-medium border-b-2 border-transparent opacity-70 transition-colors duration-150 hover:opacity-100">
|
||||
Categorías
|
||||
</a>
|
||||
<a href="/admin/maestros" class="px-4 py-2 text-sm font-medium border-b-2" style="border-color: var(--color-primary); color: var(--color-primary);">
|
||||
Maestros
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{error && (
|
||||
<div class="card mb-6" role="alert" style="border-color: color-mix(in oklab, var(--color-danger) 30%, transparent);">
|
||||
<p class="text-sm" style="color: var(--color-danger-text);">
|
||||
No se pudieron cargar los maestros. Recarga la página.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && maestros.length === 0 && (
|
||||
<div class="card text-center">
|
||||
<h2 class="text-lg font-semibold mb-1">Sin maestros aún</h2>
|
||||
<p class="text-sm opacity-70">Agrega el primero para que los alumnos puedan elegirlo como tutor.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && maestros.length > 0 && (
|
||||
<>
|
||||
<div class="hidden md:block card p-0 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-left" style="background: color-mix(in oklab, var(--color-ink) 4%, transparent);">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-medium">Nombre</th>
|
||||
<th class="px-4 py-3 font-medium">Estado</th>
|
||||
<th class="px-4 py-3 font-medium text-right"># Alumnos</th>
|
||||
<th class="px-4 py-3 font-medium text-right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{maestros.map((m) => (
|
||||
<tr class="border-t" style="border-color: color-mix(in oklab, var(--color-ink) 8%, transparent);">
|
||||
<td class="px-4 py-3 font-medium">{m.nombre}</td>
|
||||
<td class="px-4 py-3">
|
||||
{m.activo ? (
|
||||
<span class="text-xs uppercase tracking-wide" style="color: var(--color-primary);">Activo</span>
|
||||
) : (
|
||||
<span class="text-xs uppercase tracking-wide opacity-60">Inactivo</span>
|
||||
)}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right" style="font-variant-numeric: tabular-nums;">
|
||||
{m.count > 0 ? m.count : <span class="opacity-60">0</span>}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<MaestroForm mode="edit" maestro={{ id: m.id, nombre: m.nombre, activo: m.activo }} client:load />
|
||||
<EliminarMaestro id={m.id} nombre={m.nombre} alumnosCount={m.count} client:load />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex flex-col gap-3">
|
||||
{maestros.map((m) => (
|
||||
<article class="card flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold">{m.nombre}</h3>
|
||||
<p class="text-xs opacity-70 mt-1">
|
||||
{m.activo ? 'Activo' : 'Inactivo'} · {m.count} alumno{m.count === 1 ? '' : 's'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 shrink-0">
|
||||
<MaestroForm mode="edit" maestro={{ id: m.id, nombre: m.nombre, activo: m.activo }} client:load />
|
||||
<EliminarMaestro id={m.id} nombre={m.nombre} alumnosCount={m.count} client:load />
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
Reference in New Issue
Block a user