Files
labre-web/src/lib/date.ts
T
LakG e159e8d297 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)
2026-08-24 08:46:39 -07:00

77 lines
2.5 KiB
TypeScript

// Helpers de fecha con timezone MX. Respeta DST automáticamente
// leyendo el offset real vigente para la fecha dada.
const MX_TZ = 'America/Tijuana';
function tzOffset(date: Date, tz: string): string {
const parts = new Intl.DateTimeFormat('en', {
timeZone: tz,
timeZoneName: 'longOffset',
}).formatToParts(date);
const s = parts.find((p) => p.type === 'timeZoneName')?.value ?? 'GMT+00:00';
const m = s.match(/GMT([+-]\d{2}:\d{2})/);
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;
endUTC: Date;
isoDate: string;
} {
const isoDate = new Intl.DateTimeFormat('en-CA', {
timeZone: MX_TZ,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(now); // YYYY-MM-DD
const offset = tzOffset(now, MX_TZ);
const startUTC = new Date(`${isoDate}T00:00:00${offset}`);
const endUTC = new Date(startUTC.getTime() + 24 * 60 * 60 * 1000);
const label = new Intl.DateTimeFormat('es-MX', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: MX_TZ,
}).format(now);
return { label, startUTC, endUTC, isoDate };
}