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
-
- Salir
-
+
+
+ Mis datos
+
+
+ Salir
+
+
{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
}
+
setEditando(true)} className="self-start">
+ Modificar datos
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+function Dato({ etiqueta, valor }: { etiqueta: string; valor: string }) {
+ return (
+
+ );
+}
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));