Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
62fe155
chore(deploy): separa entornos dev/prod (environments.md + .env.example)
mateopavoni Jun 8, 2026
3762b61
fix(paciente): ocultar navbar pública en área paciente + header propi…
mateopavoni Jun 2, 2026
37af156
fix(db): migration 0006 — usuarios invitados respetan rol del metadat…
mateopavoni Jun 2, 2026
5e4c15a
fix(db): permitir asignar rol desde service role / SQL editor (short-…
mateopavoni Jun 2, 2026
78408c3
docs(claude): documentar rol de invitados, migraciones 0005/0006 y re…
mateopavoni Jun 2, 2026
3371621
fix(auth): logout desde el cliente browser para que la UI refleje el …
mateopavoni Jun 2, 2026
bca4675
feat(auth): flujo olvidé mi contraseña — link en login + /auth/recupe…
mateopavoni Jun 2, 2026
5b9b0c3
feat(registro): placeholders en todos los campos del form (nombre, ap…
mateopavoni Jun 2, 2026
bce7093
fix(auth): bloquear submit de registro si las contraseñas no coincide…
mateopavoni Jun 5, 2026
29c7fdd
feat(auth): pedir nombre/apellido/teléfono al crear contraseña (invit…
mateopavoni Jun 5, 2026
f3c7ff6
perf(maps): cargar el iframe de Google Maps solo on-click (componente…
mateopavoni Jun 5, 2026
4855ab1
perf(ux): loading.tsx con skeleton en área paciente y admin — feedbac…
mateopavoni Jun 5, 2026
dbd8d14
fix(auth): logout con scope 'local' — limpia sesión al instante sin r…
mateopavoni Jun 5, 2026
cc141fa
feat(seguimiento): limpiar inputs tras guardar (hook useResetOnSucces…
mateopavoni Jun 5, 2026
85b8702
feat(seguimiento): placeholders en ficha, plan y evaluación funcional…
mateopavoni Jun 5, 2026
67079db
fix(staff): configurar profesional activa la cuenta y siembra agenda …
mateopavoni Jun 5, 2026
641a934
style: contener overflow horizontal en contenido del área paciente y …
mateopavoni Jun 5, 2026
b6ca41a
docs(claude): documentar lazy-map, loading skeletons, useResetOnSucce…
mateopavoni Jun 5, 2026
14cff99
fix(ux): navbar/subnav, auth links, agenda editable, recolor whatsapp…
mateopavoni Jun 6, 2026
6a98d6e
fix(auth): recuperar vuelve a login y el link del email abre Cambiar …
mateopavoni Jun 6, 2026
7c585da
fix(auth): recovery pasa por /auth/callback para intercambiar el code
mateopavoni Jun 6, 2026
539948a
docs: agregar MANUAL-PRUEBAS.md (checklist de testing manual)
mateopavoni Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,8 @@ Migración desde sitio PHP MVC propio (en `client-assets/vimet/vimet/`) que corr
| `app/auth/callback/route.ts` | Callback auth: intercambia `code` (PKCE) o `token_hash` (OTP), redirige a `next` |
| `app/auth/confirmar/page.tsx` | Confirma invite via `token_hash` o implicit flow; redirige a nueva-contrasena |
| `app/auth/nueva-contrasena/page.tsx` | Form para crear contraseña (invite + reset) |
| `components/hash-invite-handler.tsx` | Client component global: detecta hash invite y llama `setSession` + redirige |
| `app/auth/recuperar/page.tsx` | "Olvidé mi contraseña": pide email y dispara `resetPasswordForEmail` |
| `components/hash-invite-handler.tsx` | Client component global: detecta hash invite/recovery y llama `setSession` + redirige |
| `app/terminos/page.tsx` | Términos de servicio |
| `app/privacidad/page.tsx` | Política de privacidad |
| `app/(paciente)/layout.tsx` | Layout área paciente: auth gating + subnav |
Expand DownExpand Up@@ -74,7 +75,10 @@ Migración desde sitio PHP MVC propio (en `client-assets/vimet/vimet/`) que corr
| `app/(paciente)/mis-recursos/page.tsx` | Vista de recursos visibles para el paciente |
| `actions/recursos.ts` | CRUD recursos: crear (link/pdf/imagen/video), eliminar, toggle visibilidad |
| `actions/staff.ts` | Server Actions admin: `configurarProfesionalAction` (rol + servicios + horarios), `toggleActivoAction`, `cambiarPasswordAction` |
| `actions/horarios.ts` | Server Actions: `agregarHorarioAction` / `eliminarHorarioAction` — el profesional gestiona SU propia agenda (profesional_id = usuario logueado) |
| `app/admin/configuracion/page.tsx` | Configuración admin: configurar profesional + cambiar contraseña |
| `app/admin/horarios/page.tsx` | "Mis horarios": el profesional logueado edita sus franjas de atención por día |
| `components/horarios-editor.tsx` | Editor de agenda (lista por día + form para agregar/eliminar franjas) |
| `lib/supabase/admin.ts` | Cliente Supabase con service role key (bypass RLS, Admin Auth API) |
| `components/seguimiento/recurso-form.tsx` | Form admin para subir/linkear recursos (tipo selector dinámico) |
| `app/api/slots/route.ts` | GET slots disponibles (lo consume el wizard) |
Expand All@@ -92,6 +96,10 @@ Migración desde sitio PHP MVC propio (en `client-assets/vimet/vimet/`) que corr
| `components/seguimiento/*.tsx` | Forms del módulo (ficha, medición, eval, plan, feedback, evolución, objetivo) |
| `components/page-header.tsx` | Hero/header genérico de páginas internas |
| `components/whatsapp-fab.tsx` | FAB flotante de WhatsApp en páginas públicas |
| `components/lazy-map.tsx` | Mapa de Google que solo monta el iframe on-click (perf en home/contacto) |
| `components/section-skeleton.tsx` | Skeleton genérico para los `loading.tsx` de paciente/admin |
| `components/seguimiento/use-reset-on-success.ts` | Hook: resetea el form cuando la Server Action devuelve `ok` |
| `app/(paciente)/loading.tsx` · `app/admin/loading.tsx` · `app/admin/pacientes/[id]/loading.tsx` | Loading UI (Suspense) al navegar entre secciones |
| `components/footer.tsx` | Footer + CodeTlonBadge |
| `components/codetlon-badge.tsx` | Badge de marca CodeTlon en footer |
| `components/admin-sidebar.tsx` | Sidebar del admin |
Expand All@@ -105,7 +113,7 @@ Migración desde sitio PHP MVC propio (en `client-assets/vimet/vimet/`) que corr
| `lib/supabase/middleware.ts` | Helper para refresh de session en middleware |
| `lib/supabase/auth-helpers.ts` | `getUserAndProfile` / `requireAuth` / `requireStaff` |
| `middleware.ts` | Auth middleware: protege /mis-*, /feedback-semanal, /turnos/*, /admin/* |
| `actions/auth.ts` | Server Actions: login, register, logout |
| `actions/auth.ts` | Server Actions: login, register, logout, nuevaContrasena, recuperarContrasena |
| `actions/turnos.ts` | Server Actions: crear, cancelar, actualizar estado |
| `actions/contacto.ts` | Server Action: enviar email contacto |
| `actions/ficha.ts` | Upsert de ficha clínica |
Expand All@@ -119,6 +127,8 @@ Migración desde sitio PHP MVC propio (en `client-assets/vimet/vimet/`) que corr
| `supabase/migrations/0002_seed.sql` | Seed servicios + horarios |
| `supabase/migrations/0003_seguimiento.sql` | Tablas seguimiento + RLS + bucket `planes` |
| `supabase/migrations/0004_security_hardening.sql` | Triggers que bloquean cambios sensibles por parte del paciente (rol/activo, fecha/hora del turno, respuesta de feedback) + SELECT de profiles requiere auth |
| `supabase/migrations/0005_recursos.sql` | Tabla `recursos_paciente` + bucket `recursos` + `adjunto_path` en feedback |
| `supabase/migrations/0006_invited_role.sql` | `handle_new_user` respeta `rol` del metadata para invitados (`invited_at`) + relaja trigger de profiles para permitir asignar rol desde service role / SQL editor |
| `emails/contacto.tsx` | Template Resend del formulario contacto |

## Base de Datos (Supabase)
Expand All@@ -139,7 +149,7 @@ Migración desde sitio PHP MVC propio (en `client-assets/vimet/vimet/`) que corr
| `recursos_paciente` | tipo (link/pdf/imagen/video), categoria (5), titulo, descripcion, url, storage_path, `visible_paciente`, plan_id | staff CRUD + paciente lee solo los visibles |

**Roles** (campo `rol` en `profiles`): `paciente` (default), `nutricionista`, `entrenador`, `admin`.
**Trigger:** `on_auth_user_created` → inserta fila en `profiles` con rol `paciente`.
**Trigger:** `on_auth_user_created` → inserta fila en `profiles`. Rol por defecto `paciente`; si el usuario fue **invitado** (`auth.users.invited_at` no null) y trae `rol` válido en `user_metadata`, respeta ese rol (registro público siempre cae en `paciente`). El dashboard de Supabase no expone metadata al invitar → para staff, invitar y luego setear `rol` en `profiles` desde el Table/SQL editor.
**Storage bucket** `planes` (privado): paths `{paciente_id}/{filename}`. Staff todo, paciente solo lee su propia carpeta. Acceso vía signed URL (5 min TTL).
**Storage bucket** `recursos` (privado): paths `{paciente_id}/r/{ts}_{file}` (staff) y `{paciente_id}/f/{semana}_{ts}_{file}` (adjuntos de feedback, subidos por el paciente). Signed URLs con TTL 1 hora. Paciente puede insertar/borrar solo en subcarpeta `f/`; staff full access.
**`feedback_semanal.adjunto_path`**: columna opcional (text) para el archivo que el paciente adjunta a su feedback semanal. Se sube al bucket `recursos` desde `enviarFeedbackAction`. Al re-subir se borra el anterior del bucket antes de sobrescribir la columna.
Expand DownExpand Up@@ -184,8 +194,12 @@ Ver `.env.example` para el listado completo.
- Fechas: **nunca usar `new Date().toISOString().slice(0,10)` para representar "hoy"**, devuelve fecha en UTC y se corre un día cuando son >21:00 en Córdoba (server en Vercel es UTC). Usar siempre `hoyArgentina()` / `lunesDeSemanaArgentina()` de `lib/datetime.ts`. Mismo principio para `lib/booking/slots.ts` al calcular el mínimo de hora de hoy. Aplica server y client.
- Invite flow de Supabase usa implicit flow (hash fragment `#access_token=...&type=invite`). `@supabase/ssr`'s `createBrowserClient` **no** procesa el hash automáticamente — hay que parsear con `URLSearchParams` y llamar `setSession` explícitamente. Ver `components/hash-invite-handler.tsx`. Para PKCE invite (token_hash), ver `app/auth/confirmar/page.tsx`.
- Al actualizar un plan con nuevo PDF: la action borra automáticamente el PDF previo del bucket *después* de subir el nuevo (si falla el upload conservamos el viejo). No hay checkbox de "reemplazar"; cualquier upload reemplaza.
- Endurecimiento de RLS (migración `0004`): además de las policies, hay triggers BEFORE UPDATE que impiden al paciente (a) cambiar su `rol`/`activo` en `profiles`, (b) modificar fecha/hora/profesional/notas del profesional o cambiar `estado` a algo distinto de `cancelado` en `turnos`, (c) falsificar `respuesta_profesional` / `respondido_*` en `feedback_semanal`. `is_staff()` cortocircuita los tres triggers. Si se agregan columnas sensibles nuevas hay que sumarlas explícitamente al chequeo.
- Endurecimiento de RLS (migración `0004`): además de las policies, hay triggers BEFORE UPDATE que impiden al paciente (a) cambiar su `rol`/`activo` en `profiles`, (b) modificar fecha/hora/profesional/notas del profesional o cambiar `estado` a algo distinto de `cancelado` en `turnos`, (c) falsificar `respuesta_profesional` / `respondido_*` en `feedback_semanal`. `is_staff()` cortocircuita los tres triggers. Si se agregan columnas sensibles nuevas hay que sumarlas explícitamente al chequeo. El trigger de `profiles` (migración `0006`) también cortocircuita cuando `auth.uid()` es null (service role / SQL editor) para permitir asignar rol desde el backend; un paciente por la API siempre tiene `auth.uid()` seteado, así que el bloqueo de auto-escalación sigue intacto.
- `crearTurnoAction` revalida en el servidor que la fecha sea >= hoy en zona Córdoba: el `min` del input es defensa en profundidad, pero no se confía en él.
- `configurarProfesionalAction` (staff): además de asignar rol + linkear servicios, **activa la cuenta** (`activo=true`) y, si el profesional no tiene horarios (setup inicial sin profesional previo del que heredarlos), **siembra una agenda Lun–Vie 09–13/14–18 por defecto**. Sin esto el wizard de reserva no mostraba slots para ninguna fecha. El profesional puede después editar sus franjas desde `/admin/horarios` (cada uno gestiona su propia agenda; `actions/horarios.ts` filtra por `profesional_id = auth.uid()`).
- Pantallas de auth (`AuthShell`): link "Volver al sitio" arriba. El registro bloquea el submit si las contraseñas no coinciden (botón disabled + `onSubmit` preventDefault). `nueva-contrasena` (invite/reset) pide nombre/apellido/teléfono opcionales y `nuevaContrasenaAction` actualiza el perfil solo con los campos cargados.
- Logout (`LogoutButton`): `signOut({ scope: 'local' })` para no esperar el round-trip de revocación global (evita cuelgues con conexión lenta a Supabase).
- Forms de "agregar" del módulo seguimiento usan `useResetOnSuccess(state)` → limpian inputs al guardar. Los de edición (ficha, plan, feedback semanal, responder feedback) no resetean porque muestran datos existentes.

## Comandos Rápidos
```bash
Expand All@@ -205,4 +219,5 @@ npx playwright test # Tests E2E
| 2026-05-15 | dev | Endurecimiento de seguridad: migración `0004` con triggers que cierran auto-escalación de rol, modificación arbitraria de turnos por el paciente y falsificación de respuesta de feedback. SELECT de profiles ahora requiere sesión. `crearTurnoAction` revalida `fecha >= hoyArgentina()`. Register form valida match de password en cliente. CLAUDE.md actualizado con archivos faltantes. |
| 2026-05-23 | dev | Módulo de recursos multimedia: migración `0005` + bucket `recursos` (privado) + tabla `recursos_paciente` (link/pdf/imagen/video con categoría y visibilidad) + tab "Recursos" en admin + sección "Mis recursos" en área paciente + adjunto opcional en feedback semanal (imagen/PDF, 15 MB, signed URLs 1h). |
| 2026-06-02 | feat/configuracion-staff | `/admin/configuracion`: `configurarProfesionalAction` (asigna rol + linkea servicios/horarios en un paso), `cambiarPasswordAction`, `toggleActivoAction`. Registro deja `activo=false`; admin activa desde listado de pacientes. `lib/supabase/admin.ts` (service role client). |
| 2026-06-17 | — | Se agrega `MANUAL-PRUEBAS.md` (raíz): checklist de testing manual para auth, permisos paciente/admin, concurrencia de turnos, uploads, ficha/antropometría, feedback semanal y form de contacto. `/cambio` ahora suma secciones ahí cuando un cambio tiene riesgo real no cubierto por tests automáticos. |
| 2026-06-02 | feat/invite-flow | Flujo de invitación staff: `app/auth/confirmar/page.tsx` (implicit + PKCE), `app/auth/nueva-contrasena/page.tsx`, `nuevaContrasenaAction`, `app/auth/callback/route.ts`. `components/hash-invite-handler.tsx` detecta `#access_token&type=invite` en cualquier página y llama `setSession` manualmente (@supabase/ssr no procesa hash automático). Layout refactorizado: route group `app/(public)/` con Navbar+Footer; admin y auth sin navbar pública. `app/(paciente)/layout.tsx` incluye ahora Navbar+Footer propio. |
1 change: 1 addition & 0 deletions .claude/commands/cambio.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ A partir de ahora, en CADA prompt de la sesión:
- Implementás lo pedido leyendo solo los archivos necesarios.
- `git add` + `git commit` granular y convencional (`fix:`/`feat:`/...), **sin `Co-Authored-By: Claude`**.
- Si el cambio fue estructural (archivo/tabla/env nuevos), actualizás `.claude/CLAUDE.md` y `ARCHITECTURE.md` en el mismo commit.
- Si el cambio tiene riesgo real de bug que los tests automáticos no van a atrapar (UI con criterio humano, timing/concurrencia, credenciales externas, auth), sumá su sección a `MANUAL-PRUEBAS.md` en el mismo commit.
- **NO** `git push`, **NO** merge a main. Quedás en la rama esperando el próximo pedido.

Regla: si ya estás parado en una rama de trabajo (cualquier prefijo `feat/`/`fix/`/`perf/`/..., no `main`), seguís en la misma sesión — no abras otra rama salvo que el usuario lo pida.
16 changes: 13 additions & 3 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
# Supabase
# ===================================================================
# Vimet — Variables de entorno
# ===================================================================
# Copiar a .env.local y completar. NUNCA committear .env.local.
# Dos entornos (ver docs/environments.md):
# dev → Supabase vimet-dev (Vercel Preview)
# prod → Supabase vimet-prod (Vercel Production)
# En local apuntá SIEMPRE a vimet-dev, nunca a prod.
# ===================================================================

# Supabase — DISTINTO por entorno (dev=vimet-dev, prod=vimet-prod)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=

# Resend (formulario contacto público)
# Resend (formulario contacto público) — compartido entre entornos
RESEND_API_KEY=
RESEND_FROM_EMAIL=onboarding@resend.dev
COMPANY_EMAIL=hola@vimet.com

# App
# App — DISTINTO por entorno: apuntar al dominio del entorno (dev usa la URL de la Preview)
NEXT_PUBLIC_SITE_URL=http://localhost:3000
47 changes: 47 additions & 0 deletions MANUAL-PRUEBAS.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
# Manual de pruebas — VIMET

Checklist de testing manual para flujos con riesgo real de bug que los tests automáticos no atrapan
(criterio humano de UI, timing/concurrencia, credenciales externas, auth). Se completa a medida que
se construyen o tocan estas features.

## Auth — registro, login, invite, recuperar contraseña
- [ ] Registro de paciente nuevo (`signUp`) → llega el mail de confirmación, el link confirma y redirige bien.
- [ ] Invite de paciente desde admin → el link de invite (`token_hash` o implicit flow) deja crear la contraseña en `nueva-contrasena` y loguea.
- [ ] Reset de contraseña (olvidé mi contraseña) → mismo flujo de `nueva-contrasena`, no se mezcla con el de invite.
- [ ] `hash-invite-handler` detecta el hash en distintos navegadores (Chrome, Safari) y no rompe si se recarga la página a mitad del flujo.
- [ ] Login con credenciales inválidas muestra error claro, no rompe la página.
- [ ] Logout limpia la sesión y un paciente no puede volver atrás con el botón del navegador a una página autenticada.

## Permisos — paciente vs admin
- [ ] Un paciente logueado no puede acceder a `/admin/*` (ni por URL directa).
- [ ] Un paciente solo ve sus propios turnos/ficha/planes/recursos, nunca los de otro paciente (probar cambiando el `id` en la URL).
- [ ] Un profesional inactivo (`toggleActivoAction`) no puede seguir operando con una sesión ya abierta.

## Wizard de turnos — concurrencia de horarios
- [ ] Dos pestañas reservando el mismo slot al mismo tiempo → solo una reserva queda, la otra ve el slot ya ocupado (no double-booking).
- [ ] Un horario bloqueado (`bloqueos_horario`) no aparece como disponible en `/api/slots`.
- [ ] Cancelar un turno desde `mis-turnos` libera el slot para que otro paciente lo pueda tomar.
- [ ] Cambiar el estado de un turno desde `admin/turno/[id]` se refleja al instante en el calendario admin.

## Uploads y archivos — planes y recursos
- [ ] Subir un PDF de plan desde admin → el paciente lo puede descargar desde `mis-planes/[id]` (URL firmada del bucket privado `planes`, no expira antes de tiempo razonable).
- [ ] Subir un recurso multimedia (imagen/video/link) → toggle de visibilidad funciona: si está oculto, no aparece en `mis-recursos` del paciente.
- [ ] Subir un archivo de tamaño grande no rompe el form ni deja un registro huérfano en la tabla si falla el upload al bucket `recursos`.

## Ficha clínica, antropometría, evaluación funcional
- [ ] Editar la ficha clínica de un paciente guarda y se refleja igual al recargar (no se pierde con un refresh a medio completar).
- [ ] Agregar una medición antropométrica nueva actualiza el gráfico de evolución sin recargar la página.
- [ ] El score de evaluación funcional se calcula igual al guardar que al volver a abrir el registro (no hay drift entre cálculo en cliente vs servidor).

## Feedback semanal y objetivos
- [ ] El form de feedback semanal no permite enviar dos respuestas para la misma semana (o el histórico no se duplica).
- [ ] Responder una duda del feedback desde admin se ve reflejado para el paciente en su próxima visita a la página.
- [ ] Cambiar el estado de un objetivo se refleja correctamente agrupado por categoría/estado en ambas vistas (admin y paciente, si aplica).

## Formulario de contacto (Resend)
- [ ] Envío exitoso muestra confirmación y llega el mail a `COMPANY_EMAIL`.
- [ ] Falla de Resend (API key inválida/caída) no rompe la página — muestra error legible al usuario.

## Configuración admin
- [ ] Cambiar contraseña propia desde `admin/configuracion` desloguea o mantiene sesión de forma consistente (verificar cuál es el comportamiento esperado).
- [ ] Configurar un profesional (rol + servicios + horarios) no pisa configuraciones de otros profesionales ya guardadas.
Loading