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:
@@ -114,86 +114,65 @@ export default function UnidadesManager({ materialId }: { materialId: number })
|
||||
) : unidades.length === 0 ? (
|
||||
<p className="text-sm opacity-70">Sin unidades registradas.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead
|
||||
className="text-left"
|
||||
style={{ background: 'color-mix(in oklab, var(--color-ink) 4%, transparent)' }}
|
||||
>
|
||||
<tr>
|
||||
<th className="px-2 py-2 font-medium">Etiqueta</th>
|
||||
<th className="px-2 py-2 font-medium">Estado</th>
|
||||
<th className="px-2 py-2 font-medium">Notas</th>
|
||||
<th className="px-2 py-2 font-medium text-right">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{unidades.map((u) => {
|
||||
const bloqueada = u.estado === 'prestado';
|
||||
return (
|
||||
<tr
|
||||
key={u.id}
|
||||
className="border-t"
|
||||
style={{ borderColor: 'color-mix(in oklab, var(--color-ink) 12%, transparent)' }}
|
||||
>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
defaultValue={u.etiqueta}
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value.trim();
|
||||
if (v && v !== u.etiqueta) patch(u, { etiqueta: v });
|
||||
else e.target.value = u.etiqueta;
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<select
|
||||
className="input"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
value={bloqueada ? 'prestado' : u.estado}
|
||||
disabled={bloqueada}
|
||||
onChange={(e) => patch(u, { estado: e.target.value as Unidad['estado'] })}
|
||||
>
|
||||
{bloqueada && <option value="prestado">Prestado</option>}
|
||||
{ESTADOS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s[0].toUpperCase() + s.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
defaultValue={u.notas ?? ''}
|
||||
placeholder="Opcional…"
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value;
|
||||
if ((u.notas ?? '') !== v) patch(u, { notas: v || null });
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
disabled={bloqueada}
|
||||
onClick={() => eliminar(u)}
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col gap-2">
|
||||
{unidades.map((u) => {
|
||||
const bloqueada = u.estado === 'prestado';
|
||||
return (
|
||||
<div
|
||||
key={u.id}
|
||||
className="card p-3 grid grid-cols-1 sm:grid-cols-[1fr_auto_auto] gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
aria-label="Etiqueta"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
defaultValue={u.etiqueta}
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value.trim();
|
||||
if (v && v !== u.etiqueta) patch(u, { etiqueta: v });
|
||||
else e.target.value = u.etiqueta;
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
className="input"
|
||||
aria-label="Estado"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem', minWidth: '130px' }}
|
||||
value={bloqueada ? 'prestado' : u.estado}
|
||||
disabled={bloqueada}
|
||||
onChange={(e) => patch(u, { estado: e.target.value as Unidad['estado'] })}
|
||||
>
|
||||
{bloqueada && <option value="prestado">Prestado</option>}
|
||||
{ESTADOS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s[0].toUpperCase() + s.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
disabled={bloqueada}
|
||||
onClick={() => eliminar(u)}
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
className="input sm:col-span-3"
|
||||
aria-label="Notas"
|
||||
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
|
||||
defaultValue={u.notas ?? ''}
|
||||
placeholder="Notas (opcional)…"
|
||||
onBlur={(e) => {
|
||||
const v = e.target.value;
|
||||
if ((u.notas ?? '') !== v) patch(u, { notas: v || null });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user