UX: motivo requerido, dock móvil, hamburguesa desktop, toaster Sileo, panel de estadísticas
Alumno: - Modal de solicitud: campo "Notas" renombrado a "Motivo del préstamo", ahora requerido, con hint sobre uso (para qué clase o proyecto). Admin: - Bandeja de solicitudes pendientes, activos y devoluciones ahora muestran el motivo del alumno (en tabla desktop y tarjetas móvil). - /admin/inventario se convirtió en "Estadísticas": grid de KPIs del día (solicitadas, aprobadas, rechazadas, devoluciones, en préstamo, vencidos) arriba del inventario existente. Los cambios de estado se cuentan desde audit_log, el rango del día se calcula automáticamente en timezone America/Tijuana con DST via Intl API (src/lib/date.ts). Rediseño móvil: - Sidebar desktop se puede colapsar con botón hamburguesa. Estado persiste en localStorage sin flash (script is:inline bloqueante). - Nav móvil reemplazado por dock píldora flotante con iconos SVG inline (heroicons outline). Respeta safe-area-inset-bottom. Notificaciones estilo Sileo: - Componente Toaster global (React island client:idle) que escucha CustomEvents. Toasts con backdrop-blur, animación de entrada translateY+opacity, auto-dismiss 3.8s, click para cerrar. Helper toastAfterReload deja pendiente en sessionStorage para sobrevivir location.reload() post-acción. Diagnóstico: - /api/whoami devuelve user+profile actuales para depurar sesión. - Endpoint devolver retorna un objeto `debug` en el 403 con email+rol vistos por el server (temporal, para trazar bug del 403). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// 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';
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
import { createServerClient, createBrowserClient, type CookieOptionsWithName } from '@supabase/ssr';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { AstroCookies } from 'astro';
|
||||
|
||||
const SCHEMA = 'prestamos';
|
||||
|
||||
const SUPABASE_URL = process.env.PUBLIC_SUPABASE_URL ?? import.meta.env.PUBLIC_SUPABASE_URL;
|
||||
const SUPABASE_ANON_KEY = process.env.PUBLIC_SUPABASE_ANON_KEY ?? import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
|
||||
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY ?? import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
// ponytail: solo para preview local sin login. Bypass RLS con service_role.
|
||||
// Su uso queda tras un flag de DEV en el middleware.
|
||||
export function serviceClient() {
|
||||
return createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, {
|
||||
db: { schema: SCHEMA },
|
||||
auth: { persistSession: false, autoRefreshToken: false },
|
||||
});
|
||||
}
|
||||
|
||||
const cookieOptions: CookieOptionsWithName = {
|
||||
name: 'sb',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export type ToastKind = 'success' | 'error' | 'info';
|
||||
|
||||
export type ToastPayload = {
|
||||
title: string;
|
||||
description?: string;
|
||||
kind?: ToastKind;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export function toast(payload: ToastPayload) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new CustomEvent<ToastPayload>('labre:toast', { detail: payload }));
|
||||
}
|
||||
|
||||
// Deja un toast pendiente que sobrevive un location.reload().
|
||||
export function toastAfterReload(payload: ToastPayload) {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const q = JSON.parse(sessionStorage.getItem('labre:toast:queue') ?? '[]');
|
||||
q.push(payload);
|
||||
sessionStorage.setItem('labre:toast:queue', JSON.stringify(q));
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user