feat: el cliente puede ver y modificar sus datos de contacto #6

Merged
jpbeldomenico merged 2 commits from feat/datos-de-contacto into main 2026-08-05 16:09:46 -03:00
Owner

El cliente no tenia forma de corregir su email ni su telefono despues del alta. Ahora hay una pantalla Mis datos (/cliente/perfil), enlazada desde el dashboard al lado de "Salir", que muestra nombre, email y telefono y permite editarlos.

Detalles que importan

  • El telefono usa el mismo campo con el +54 9 fijo que el registro, y se guarda normalizado en telefonoNorm, asi que si el cliente lo corrige el bot lo sigue reconociendo.
  • aLocalArgentino(): un cliente dado de alta por WhatsApp tiene el numero guardado como 5491141984844, y en el formulario tiene que verse 1141984844. Sin esto, editar sus datos le duplicaba el prefijo.
  • Unicidad: email y telefono se validan contra los demas clientes, excluyendo al propio (id: { not: sesion.sub }), asi guardar sin cambiar nada no da conflicto. Devuelve 409 con mensaje claro en vez de reventar contra el indice unico.
  • El email es la identidad del magic link: si lo cambia, se le avisa en el formulario que con ese va a entrar de ahora en mas.
  • El middleware protege /cliente/perfil como el resto del area del cliente.

Verificacion

Contra una copia de la base de produccion, con el dev server:

  • GET devuelve el telefono en formato local aunque este guardado con prefijo.
  • PATCH cambia apellido y telefono; en la base queda telefono=3515554444 y telefonoNorm=5493515554444.
  • Rechaza telefono con 0 adelante, email invalido y nombre vacio (400).
  • Email ya usado por otro cliente y telefono ya usado por otro cliente: 409 en ambos casos.

tsc y lint limpios.

🤖 Generated with Claude Code

El cliente no tenia forma de corregir su email ni su telefono despues del alta. Ahora hay una pantalla **Mis datos** (`/cliente/perfil`), enlazada desde el dashboard al lado de "Salir", que muestra nombre, email y telefono y permite editarlos. ## Detalles que importan - **El telefono usa el mismo campo con el `+54 9` fijo** que el registro, y se guarda normalizado en `telefonoNorm`, asi que si el cliente lo corrige el bot lo sigue reconociendo. - **`aLocalArgentino()`**: un cliente dado de alta por WhatsApp tiene el numero guardado como `5491141984844`, y en el formulario tiene que verse `1141984844`. Sin esto, editar sus datos le duplicaba el prefijo. - **Unicidad**: email y telefono se validan contra los demas clientes, excluyendo al propio (`id: { not: sesion.sub }`), asi guardar sin cambiar nada no da conflicto. Devuelve 409 con mensaje claro en vez de reventar contra el indice unico. - **El email es la identidad del magic link**: si lo cambia, se le avisa en el formulario que con ese va a entrar de ahora en mas. - El middleware protege `/cliente/perfil` como el resto del area del cliente. ## Verificacion Contra una copia de la base de produccion, con el dev server: - `GET` devuelve el telefono en formato local aunque este guardado con prefijo. - `PATCH` cambia apellido y telefono; en la base queda `telefono=3515554444` y `telefonoNorm=5493515554444`. - Rechaza telefono con 0 adelante, email invalido y nombre vacio (400). - Email ya usado por otro cliente y telefono ya usado por otro cliente: 409 en ambos casos. `tsc` y `lint` limpios. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jpbeldomenico added 1 commit 2026-08-05 15:55:49 -03:00
feat(cliente): ver y modificar los datos de contacto
Revisión con IA / ai-review (pull_request) Successful in 2m18s
62dab5e025
El cliente no tenia forma de corregir su email ni su telefono despues del alta.
Nueva pantalla /cliente/perfil ("Mis datos", enlazada desde el dashboard) que
muestra nombre, email y telefono, y permite editarlos.

- GET/PATCH /api/cliente/perfil, ambos con la sesion del cliente.
- El telefono usa el mismo campo con el +54 9 fijo que el registro, y se guarda
  normalizado en telefonoNorm para que el bot lo siga reconociendo.
- aLocalArgentino() saca el prefijo para mostrar: un cliente dado de alta por
  WhatsApp tiene el numero guardado como 5491141984844 y en el formulario tiene
  que verse 1141984844.
- Email y telefono se validan unicos contra los demas clientes (409), no contra
  uno mismo. Si cambia el email se avisa que con ese va a entrar de ahora en mas,
  porque es la identidad del magic link.
- El middleware protege /cliente/perfil como el resto del area del cliente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ai-bot reviewed 2026-08-05 15:57:24 -03:00
ai-bot left a comment
Collaborator

Inline review

Inline review
@@ -0,0 +48,4 @@
// El email es con lo que se entra al sistema y el teléfono es con lo que el bot
// reconoce al cliente: los dos tienen que seguir siendo únicos.
const emailTomado = await prisma.cliente.findFirst({
Collaborator

Condición de carrera: la verificación de unicidad de email/teléfono y el update no son atómicos. Dos requests simultáneos pueden pasar la verificación y fallar en el update (o peor, si no hay constraint única en BD, duplicar datos). Usá una transacción de Prisma o confia en el constraint único y atrapá el error P2002.

#ai-review-inline

Condición de carrera: la verificación de unicidad de email/teléfono y el update no son atómicos. Dos requests simultáneos pueden pasar la verificación y fallar en el update (o peor, si no hay constraint única en BD, duplicar datos). Usá una transacción de Prisma o confia en el constraint único y atrapá el error P2002. #ai-review-inline
@@ -0,0 +68,4 @@
nombre: nombre.trim(),
apellido: apellido.trim(),
email: emailNuevo,
telefono: aLocalArgentino(telefonoNorm),
Collaborator

Guardás el teléfono en formato local en el campo telefono y en formato WhatsApp en telefonoNorm. El GET lee telefonoNorm primero y hace fallback a telefono. Si telefonoNorm existe, telefono es redundante. Confirmá que el schema tenga @@unique([telefonoNorm]) y @@unique([email]) para que la BD sea la fuente de verdad.

#ai-review-inline

Guardás el teléfono en formato local en el campo `telefono` y en formato WhatsApp en `telefonoNorm`. El GET lee `telefonoNorm` primero y hace fallback a `telefono`. Si `telefonoNorm` existe, `telefono` es redundante. Confirmá que el schema tenga `@@unique([telefonoNorm])` y `@@unique([email])` para que la BD sea la fuente de verdad. #ai-review-inline
@@ -0,0 +28,4 @@
const [guardado, setGuardado] = useState(false);
useEffect(() => {
fetch("/api/cliente/perfil")
Collaborator

El fetch inicial no maneja errores de red: si falla la request (p.ej. sin conexión), el .then no se ejecuta y loading queda en true para siempre. Agregá .catch(() => { setLoading(false); setError('Error de conexión'); }) o usá try/catch con async/await.

  useEffect(() => {
    async function cargar() {
      try {
        const r = await fetch('/api/cliente/perfil');
        if (r.status === 401) { router.push('/cliente'); return; }
        const data = await r.json();
        setPerfil(data);
        setForm(data);
      } catch {
        setError('Error de conexión');
      } finally {
        setLoading(false);
      }
    }
    cargar();
  }, [router]);

#ai-review-inline

El fetch inicial no maneja errores de red: si falla la request (p.ej. sin conexión), el `.then` no se ejecuta y `loading` queda en `true` para siempre. Agregá `.catch(() => { setLoading(false); setError('Error de conexión'); })` o usá try/catch con async/await. ```suggestion useEffect(() => { async function cargar() { try { const r = await fetch('/api/cliente/perfil'); if (r.status === 401) { router.push('/cliente'); return; } const data = await r.json(); setPerfil(data); setForm(data); } catch { setError('Error de conexión'); } finally { setLoading(false); } } cargar(); }, [router]); ``` #ai-review-inline
ai-bot reviewed 2026-08-05 15:57:24 -03:00
ai-bot left a comment
Collaborator

Inline review

Inline review
ai-bot reviewed 2026-08-05 15:57:24 -03:00
ai-bot left a comment
Collaborator

Inline review

Inline review
ai-bot reviewed 2026-08-05 15:57:24 -03:00
ai-bot left a comment
Collaborator

Inline review

Inline review
@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { verificarSesion } from "@/lib/auth-admin";
Collaborator

Importás verificarSesion desde auth-admin.ts para validar la cookie cliente_session. Verificá que esa función valide correctamente el rol de cliente y no solo la existencia de la sesión de admin. Si reutiliza lógica de admin, podría haber escalada de privilegios.

#ai-review-inline

Importás `verificarSesion` desde `auth-admin.ts` para validar la cookie `cliente_session`. Verificá que esa función valide correctamente el rol de cliente y no solo la existencia de la sesión de admin. Si reutiliza lógica de admin, podría haber escalada de privilegios. #ai-review-inline
@@ -0,0 +69,4 @@
}
setGuardando(true);
setError("");
const res = await fetch("/api/cliente/perfil", {
Collaborator

El PATCH no maneja 401 (sesión expirada durante la edición). El usuario ve un error genérico y no sabe que debe reloguearse. Agregá if (res.status === 401) { router.push('/cliente'); return; } antes de leer res.json().

    const res = await fetch('/api/cliente/perfil', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form),
    });
    if (res.status === 401) { router.push('/cliente'); return; }
    const data = await res.json();

#ai-review-inline

El PATCH no maneja 401 (sesión expirada durante la edición). El usuario ve un error genérico y no sabe que debe reloguearse. Agregá `if (res.status === 401) { router.push('/cliente'); return; }` antes de leer `res.json()`. ```suggestion const res = await fetch('/api/cliente/perfil', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }); if (res.status === 401) { router.push('/cliente'); return; } const data = await res.json(); ``` #ai-review-inline
ai-bot reviewed 2026-08-05 15:57:24 -03:00
ai-bot left a comment
Collaborator

Inline review

Inline review
Collaborator

Qué cambia
Se agrega la funcionalidad completa para que el cliente logueado vea y edite su perfil (nombre, apellido, email, teléfono). Incluye: endpoint GET/PATCH /api/cliente/perfil con validación de unicidad de email y teléfono, página /cliente/perfil con formulario controlado y feedback, link "Mis datos" en el dashboard, y protección de la ruta en el middleware. También se suma aLocalArgentino en lib/telefono.ts para mostrar el número sin el prefijo +54 9.

Riesgos

  • Race condition en unicidad: el check findFirst + update no es atómico; dos requests simultáneos con el mismo email/teléfono pueden pasar la validación y fallar en la BD con error P2002 no manejado.
  • Inconsistencia de formatos en telefono: el campo telefono a veces guarda formato local y a veces WhatsApp; el código confía en telefonoNorm como fuente de verdad, pero si hay datos legacy mezclados, aLocalArgentino podría devolver valores inesperados.
  • Enumeración de cuentas: el error 409 "Ese email/teléfono ya está registrado" permite saber si un dato existe en el sistema.

Antes de aprobar

  1. Confirmar que email y telefonoNorm tienen @@unique en el schema de Prisma.
  2. Probar concurrencia: abrir dos pestañas, poner el mismo email/teléfono y guardar casi a la vez; verificar que el segundo muestra error amigable (no 500).
  3. Revisar datos existentes: correr script que normalice telefono a formato local y telefonoNorm a WhatsApp para evitar sorpresas.
  4. Evaluar si el 409 debe ser genérico ("No se pudo actualizar") para no filtrar existencia de cuentas.

#ai-review-summary

**Qué cambia** Se agrega la funcionalidad completa para que el cliente logueado vea y edite su perfil (nombre, apellido, email, teléfono). Incluye: endpoint `GET/PATCH /api/cliente/perfil` con validación de unicidad de email y teléfono, página `/cliente/perfil` con formulario controlado y feedback, link "Mis datos" en el dashboard, y protección de la ruta en el middleware. También se suma `aLocalArgentino` en `lib/telefono.ts` para mostrar el número sin el prefijo +54 9. **Riesgos** - **Race condition en unicidad**: el check `findFirst` + `update` no es atómico; dos requests simultáneos con el mismo email/teléfono pueden pasar la validación y fallar en la BD con error P2002 no manejado. - **Inconsistencia de formatos en `telefono`**: el campo `telefono` a veces guarda formato local y a veces WhatsApp; el código confía en `telefonoNorm` como fuente de verdad, pero si hay datos legacy mezclados, `aLocalArgentino` podría devolver valores inesperados. - **Enumeración de cuentas**: el error 409 "Ese email/teléfono ya está registrado" permite saber si un dato existe en el sistema. **Antes de aprobar** 1. Confirmar que `email` y `telefonoNorm` tienen `@@unique` en el schema de Prisma. 2. Probar concurrencia: abrir dos pestañas, poner el mismo email/teléfono y guardar casi a la vez; verificar que el segundo muestra error amigable (no 500). 3. Revisar datos existentes: correr script que normalice `telefono` a formato local y `telefonoNorm` a WhatsApp para evitar sorpresas. 4. Evaluar si el 409 debe ser genérico ("No se pudo actualizar") para no filtrar existencia de cuentas. #ai-review-summary
jpbeldomenico added 1 commit 2026-08-05 16:09:45 -03:00
fix(perfil): detalles senalados en la revision
Revisión con IA / ai-review (pull_request) Successful in 24s
ce7063514c
- verificarSesion acepta el rol esperado. Las tres sesiones (admin, cliente,
  jefe) se firman con el mismo secreto, asi que un token de jefe puesto en la
  cookie cliente_session pasaba la verificacion de firma; ahora el rol se
  comprueba. Verificado: con un JWT de jefe, /api/cliente/perfil da 401.
- El update traduce el P2002 de los indices unicos a un 409 legible, por si otra
  request toma el mismo email o telefono entre la comprobacion y el update.
- La pantalla ya no queda en "Cargando..." para siempre si falla el pedido.
- Si la sesion vence mientras edita, el PATCH manda a entrar de nuevo en vez de
  mostrar un error suelto.

Queda como mejora aparte pasarle el rol esperado al resto de las rutas del area
del cliente, que hoy solo validan la firma.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jpbeldomenico merged commit 97f9ccec28 into main 2026-08-05 16:09:46 -03:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jpbeldomenico/taller_automotriz#6