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:
@@ -13,6 +13,43 @@ function tzOffset(date: Date, tz: string): string {
|
||||
return m?.[1] ?? '+00:00';
|
||||
}
|
||||
|
||||
function hourMX(now: Date = new Date()): number {
|
||||
const parts = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: MX_TZ,
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
}).formatToParts(now);
|
||||
const h = parseInt(parts.find((p) => p.type === 'hour')?.value ?? '0', 10);
|
||||
return h % 24; // "24" en algunos runtimes al filo de medianoche.
|
||||
}
|
||||
|
||||
export function saludoHora(
|
||||
now: Date = new Date()
|
||||
): 'Buenos días' | 'Buenas tardes' | 'Buenas noches' {
|
||||
const h = hourMX(now);
|
||||
if (h >= 5 && h < 12) return 'Buenos días';
|
||||
if (h >= 12 && h < 19) return 'Buenas tardes';
|
||||
return 'Buenas noches';
|
||||
}
|
||||
|
||||
// "Hoy a las 15:32" / "Ayer a las 10:00" / "Hace 3 días" / fecha absoluta.
|
||||
export function fmtFechaRelativa(input: string | Date, now: Date = new Date()): string {
|
||||
const d = typeof input === 'string' ? new Date(input) : input;
|
||||
const hoy = todayMX(now);
|
||||
const suDia = todayMX(d);
|
||||
const diffDays = Math.round((hoy.startUTC.getTime() - suDia.startUTC.getTime()) / 86400000);
|
||||
const hora = new Intl.DateTimeFormat('es-MX', {
|
||||
timeZone: MX_TZ,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(d);
|
||||
if (diffDays === 0) return `Hoy a las ${hora}`;
|
||||
if (diffDays === 1) return `Ayer a las ${hora}`;
|
||||
if (diffDays > 1 && diffDays < 7) return `Hace ${diffDays} días`;
|
||||
return new Intl.DateTimeFormat('es-MX', { dateStyle: 'medium', timeZone: MX_TZ }).format(d);
|
||||
}
|
||||
|
||||
export function todayMX(now: Date = new Date()): {
|
||||
label: string;
|
||||
startUTC: Date;
|
||||
|
||||
Reference in New Issue
Block a user