From 61024b8f3141f1113148bf26f1880b5e9278bee9 Mon Sep 17 00:00:00 2001 From: Cristian Felipe Cruz Buitron Date: Sat, 29 Aug 2026 05:16:18 +0000 Subject: [PATCH] feat(ecommerce): selector Departamento/Ciudad con DIVIPOLA real en checkout Reemplaza los inputs de texto libre de Ciudad/Departamento por dos selects en cascada, poblados con el dataset oficial de DIVIPOLA (33 departamentos + 1123 municipios, fuente proyecto26/colombia, verificado contra la codificacion real del DANE antes de incorporarlo -- id 5001 = Medellin, departmentId 5 = Antioquia, etc.). Sin dependencia npm nueva (JSON estatico + helper propio). Se guarda el id DIVIPOLA seleccionado en el estado del formulario, no solo el nombre -- EnvioClick (la integracion de envios real que reemplazara la tarifa fija de este ADR) va a necesitar identificar la ciudad sin ambiguedad, y las transportadoras colombianas referencian ciudades por este codigo. El nombre derivado del id sigue siendo lo que se manda a Medusa (shipping_address espera texto, no cambia el contrato existente). --- .../components/checkout/checkout-client.tsx | 60 +- workloads/ecommerce/lib/data/co-ciudades.json | 1127 +++++++++++++++++ .../ecommerce/lib/data/co-departamentos.json | 40 + workloads/ecommerce/lib/data/colombia.ts | 32 + 4 files changed, 1251 insertions(+), 8 deletions(-) create mode 100644 workloads/ecommerce/lib/data/co-ciudades.json create mode 100644 workloads/ecommerce/lib/data/co-departamentos.json create mode 100644 workloads/ecommerce/lib/data/colombia.ts diff --git a/workloads/ecommerce/components/checkout/checkout-client.tsx b/workloads/ecommerce/components/checkout/checkout-client.tsx index ee6db1d..2caef9f 100644 --- a/workloads/ecommerce/components/checkout/checkout-client.tsx +++ b/workloads/ecommerce/components/checkout/checkout-client.tsx @@ -6,9 +6,13 @@ import { ArrowLeft, MapPin, MessageCircle, Truck } from "lucide-react"; import { type FormEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { ciudadesPorDepartamento, departamentos } from "@/lib/data/colombia"; import { formatMoney } from "@/lib/utils"; import { useCartStore } from "@/store/cart-store"; +const SELECT_CLASS = + "h-12 w-full rounded-full border border-slate-200 bg-white px-5 text-sm text-slate-950 outline-none transition focus:border-cyan-500 focus:ring-4 focus:ring-cyan-300/30 disabled:opacity-50"; + // ADR 0009, siguiente corte tras el carrito real: direccion + seleccion de // envio contra el Store API real (mismos endpoints ya verificados en vivo // para las shipping options). El pago (PayU custom) todavia no existe -- @@ -35,7 +39,21 @@ async function postJson(url: string, body: unknown) { export function CheckoutClient() { const { cartId, items, hasHydrated } = useCartStore(); const [step, setStep] = useState("address"); - const [form, setForm] = useState({ firstName: "", lastName: "", address1: "", city: "", province: "", phone: "", email: "" }); + const [form, setForm] = useState({ + firstName: "", + lastName: "", + address1: "", + // Se guarda el id DIVIPOLA seleccionado, no el texto libre -- ver + // lib/data/colombia.ts. El nombre real se deriva de este id para + // mandarlo a Medusa (que solo espera texto en shipping_address). + departmentId: null as number | null, + cityId: null as number | null, + phone: "", + email: "", + }); + const availableCities = form.departmentId ? ciudadesPorDepartamento(form.departmentId) : []; + const selectedDepartamento = departamentos.find((departamento) => departamento.id === form.departmentId); + const selectedCiudad = availableCities.find((ciudad) => ciudad.id === form.cityId); const [shippingOptions, setShippingOptions] = useState([]); const [selectedOptionId, setSelectedOptionId] = useState(null); const [shippingTotal, setShippingTotal] = useState(0); @@ -48,11 +66,20 @@ export function CheckoutClient() { async function submitAddress(event: FormEvent) { event.preventDefault(); - if (!cartId) return; + if (!cartId || !selectedCiudad || !selectedDepartamento) return; setLoading(true); setError(null); try { - const cart = await postJson("/api/cart/address", { cartId, ...form }); + const cart = await postJson("/api/cart/address", { + cartId, + firstName: form.firstName, + lastName: form.lastName, + address1: form.address1, + city: selectedCiudad.name, + province: selectedDepartamento.name, + phone: form.phone, + email: form.email, + }); if (cart.error) throw new Error(cart.error); const response = await fetch(`/api/cart/shipping-options?cartId=${cartId}`); const data = await response.json(); @@ -86,7 +113,7 @@ export function CheckoutClient() { function continueOnWhatsApp() { const lines = items.map((item) => `• ${item.quantity} x ${item.product.name}`).join("\n"); - const message = `Hola Ari Shopping 👋\nQuiero confirmar mi pedido:\n\n${lines}\n\nSubtotal: ${formatMoney(itemsSubtotal, currency)}\nEnvío: ${formatMoney(shippingTotal, currency)}\nTotal: ${formatMoney(total, currency)}\n\nNombre: ${form.firstName} ${form.lastName}\nDirección: ${form.address1}, ${form.city}${form.province ? `, ${form.province}` : ""}\nTeléfono: ${form.phone}`; + const message = `Hola Ari Shopping 👋\nQuiero confirmar mi pedido:\n\n${lines}\n\nSubtotal: ${formatMoney(itemsSubtotal, currency)}\nEnvío: ${formatMoney(shippingTotal, currency)}\nTotal: ${formatMoney(total, currency)}\n\nNombre: ${form.firstName} ${form.lastName}\nDirección: ${form.address1}, ${selectedCiudad?.name ?? ""}${selectedDepartamento ? `, ${selectedDepartamento.name}` : ""}\nTeléfono: ${form.phone}`; void navigator.clipboard?.writeText(message); window.open(process.env.NEXT_PUBLIC_WHATSAPP_URL ?? "https://wa.link/rvcqqc", "_blank", "noopener,noreferrer"); } @@ -116,8 +143,25 @@ export function CheckoutClient() { setForm({ ...form, address1: event.target.value })} />
- setForm({ ...form, city: event.target.value })} /> - setForm({ ...form, province: event.target.value })} /> + +
setForm({ ...form, phone: event.target.value })} /> @@ -130,7 +174,7 @@ export function CheckoutClient() { {step === "shipping" && (
-

{form.address1}, {form.city}{form.province ? `, ${form.province}` : ""}

+

{form.address1}, {selectedCiudad?.name}{selectedDepartamento ? `, ${selectedDepartamento.name}` : ""}

{shippingOptions.map((option) => (