diff --git a/src/app/api/cliente/perfil/route.ts b/src/app/api/cliente/perfil/route.ts new file mode 100644 index 0000000..16d6887 --- /dev/null +++ b/src/app/api/cliente/perfil/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { verificarSesion } from "@/lib/auth-admin"; +import { aFormatoWhatsapp, aLocalArgentino, esLocalArgentinoValido } from "@/lib/telefono"; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +// Datos de contacto del cliente logueado. El teléfono se devuelve en formato local +// (sin el +54 9) porque el formulario lo muestra como prefijo fijo. +export async function GET() { + const sesion = await verificarSesion("cliente_session"); + if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + + const cliente = await prisma.cliente.findUnique({ + where: { id: sesion.sub }, + select: { nombre: true, apellido: true, email: true, telefono: true, telefonoNorm: true }, + }); + if (!cliente) return NextResponse.json({ error: "Cliente no encontrado" }, { status: 404 }); + + return NextResponse.json({ + nombre: cliente.nombre, + apellido: cliente.apellido, + email: cliente.email ?? "", + telefono: aLocalArgentino(cliente.telefonoNorm ?? cliente.telefono ?? ""), + }); +} + +export async function PATCH(req: NextRequest) { + const sesion = await verificarSesion("cliente_session"); + if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 }); + + const { nombre, apellido, email, telefono } = await req.json(); + + if (!nombre?.trim() || !apellido?.trim()) + return NextResponse.json({ error: "Nombre y apellido son obligatorios" }, { status: 400 }); + + if (!email?.trim() || !EMAIL_RE.test(email.trim())) + return NextResponse.json({ error: "El email no es válido" }, { status: 400 }); + + if (!esLocalArgentinoValido(telefono ?? "")) + return NextResponse.json( + { error: "El teléfono debe tener 10 dígitos, sin el 0 de la característica ni el 15" }, + { status: 400 } + ); + + const emailNuevo = email.trim().toLowerCase(); + const telefonoNorm = aFormatoWhatsapp(telefono); + + // 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({ + where: { email: emailNuevo, id: { not: sesion.sub } }, + select: { id: true }, + }); + if (emailTomado) + return NextResponse.json({ error: "Ese email ya está registrado" }, { status: 409 }); + + const telefonoTomado = await prisma.cliente.findFirst({ + where: { telefonoNorm, id: { not: sesion.sub } }, + select: { id: true }, + }); + if (telefonoTomado) + return NextResponse.json({ error: "Ese teléfono ya está registrado" }, { status: 409 }); + + const cliente = await prisma.cliente.update({ + where: { id: sesion.sub }, + data: { + nombre: nombre.trim(), + apellido: apellido.trim(), + email: emailNuevo, + telefono: aLocalArgentino(telefonoNorm), + telefonoNorm, + }, + select: { nombre: true, apellido: true, email: true, telefonoNorm: true }, + }); + + return NextResponse.json({ + ok: true, + nombre: cliente.nombre, + apellido: cliente.apellido, + email: cliente.email ?? "", + telefono: aLocalArgentino(cliente.telefonoNorm ?? ""), + }); +} diff --git a/src/app/cliente/dashboard/page.tsx b/src/app/cliente/dashboard/page.tsx index b0b59a4..353237d 100644 --- a/src/app/cliente/dashboard/page.tsx +++ b/src/app/cliente/dashboard/page.tsx @@ -68,9 +68,14 @@ export default function DashboardPage() {

Mis autos

- +
+ + Mis datos + + +
{loading ? ( diff --git a/src/app/cliente/perfil/page.tsx b/src/app/cliente/perfil/page.tsx new file mode 100644 index 0000000..1c12079 --- /dev/null +++ b/src/app/cliente/perfil/page.tsx @@ -0,0 +1,157 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Button from "@/components/ui/Button"; +import Card from "@/components/ui/Card"; +import Input from "@/components/ui/Input"; +import { esLocalArgentinoValido } from "@/lib/telefono"; + +interface Perfil { + nombre: string; + apellido: string; + email: string; + telefono: string; +} + +const VACIO: Perfil = { nombre: "", apellido: "", email: "", telefono: "" }; + +export default function PerfilPage() { + const router = useRouter(); + const [perfil, setPerfil] = useState(VACIO); + const [form, setForm] = useState(VACIO); + const [loading, setLoading] = useState(true); + const [editando, setEditando] = useState(false); + const [guardando, setGuardando] = useState(false); + const [error, setError] = useState(""); + const [telefonoError, setTelefonoError] = useState(""); + const [guardado, setGuardado] = useState(false); + + useEffect(() => { + fetch("/api/cliente/perfil") + .then((r) => { + if (r.status === 401) { router.push("/cliente"); return null; } + return r.json(); + }) + .then((data) => { + if (!data) return; + setPerfil(data); + setForm(data); + setLoading(false); + }); + }, [router]); + + function set(campo: keyof Perfil) { + return (e: React.ChangeEvent) => + setForm((f) => ({ ...f, [campo]: e.target.value })); + } + + function handleTelefono(e: React.ChangeEvent) { + const soloDigitos = e.target.value.replace(/\D/g, "").slice(0, 10); + setForm((f) => ({ ...f, telefono: soloDigitos })); + if (soloDigitos.startsWith("0")) setTelefonoError("Sin el 0 de la característica"); + else if (soloDigitos.startsWith("15")) setTelefonoError("Sin el 15, poné la característica"); + else setTelefonoError(""); + } + + function cancelar() { + setForm(perfil); + setEditando(false); + setError(""); + setTelefonoError(""); + } + + async function guardar(e: React.FormEvent) { + e.preventDefault(); + if (!esLocalArgentinoValido(form.telefono)) { + setTelefonoError("Son 10 dígitos: característica sin el 0 y número sin el 15"); + return; + } + setGuardando(true); + setError(""); + const res = await fetch("/api/cliente/perfil", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + const data = await res.json(); + setGuardando(false); + if (!res.ok) { setError(data.error || "No se pudieron guardar los cambios"); return; } + setPerfil(data); + setForm(data); + setEditando(false); + setGuardado(true); + setTimeout(() => setGuardado(false), 4000); + } + + if (loading) return
Cargando...
; + + const emailCambio = editando && form.email.trim().toLowerCase() !== perfil.email.toLowerCase(); + + return ( +
+
+ +

Mis datos

+
+ + + {!editando ? ( +
+ + + + {guardado &&

✅ Datos actualizados

} + +
+ ) : ( +
+
+ + +
+ + + {error &&

{error}

} +
+ + +
+
+ )} +
+
+ ); +} + +function Dato({ etiqueta, valor }: { etiqueta: string; valor: string }) { + return ( +
+

{etiqueta}

+

{valor}

+
+ ); +} diff --git a/src/lib/telefono.ts b/src/lib/telefono.ts index a24e94a..c046cb7 100644 --- a/src/lib/telefono.ts +++ b/src/lib/telefono.ts @@ -29,3 +29,13 @@ export function esLocalArgentinoValido(local: string): boolean { export function aFormatoWhatsapp(local: string): string { return PREFIJO_WHATSAPP_AR + normalizarTelefono(local); } + +/** + * Inverso del anterior: 5491141984844 → 1141984844. Sirve para mostrar el número + * en el formulario (donde el +54 9 es un prefijo fijo), venga de un alta web o de + * una por WhatsApp, que lo guarda con el prefijo puesto. + */ +export function aLocalArgentino(valor: string): string { + const d = normalizarTelefono(valor); + return d.startsWith(PREFIJO_WHATSAPP_AR) ? d.slice(PREFIJO_WHATSAPP_AR.length) : d; +} diff --git a/src/middleware.ts b/src/middleware.ts index 1fd0c06..4321925 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -19,6 +19,7 @@ export async function middleware(req: NextRequest) { if (pathname.startsWith("/cliente/dashboard") || pathname.startsWith("/cliente/historial") || + pathname.startsWith("/cliente/perfil") || pathname.startsWith("/cliente/turnos")) { const ok = await verifyCookie(req, "cliente_session"); if (!ok) return NextResponse.redirect(new URL("/cliente", req.url));