/home/crealab/cntxt.brainware.com.co/cotizador-campestre-v5.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configurador Vivienda Campestre - CNTXT®</title>
<!-- React & Tailwind -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Librería PDF -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<style>
/* Tipografía Premium */
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700&display=swap');
body { margin: 0; background-color: #000; color: #fff; font-family: 'Manrope', sans-serif; }
.cursor-wait { cursor: wait; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #050505; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #D4AF37; }
/* Checkbox */
.custom-checkbox:checked + div { border-color: #D4AF37; background-color: #0a0a0a; }
.custom-checkbox:checked + div .check-icon { opacity: 1; transform: scale(1); }
.custom-checkbox:checked + div h3 { color: #fff; }
.custom-checkbox:checked + div .price-tag { color: #D4AF37; }
/* ESTILOS DEL DOCUMENTO (COTIZACIÓN - OCULTO) */
.quotation-sheet {
background-color: #ffffff; /* Fondo blanco para PDF impreso profesional */
color: #000000;
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 50px;
position: relative;
/* Oculto en pantalla normal, visible solo para html2pdf */
position: absolute;
left: -9999px;
top: 0;
}
/* Ajustes Impresión */
@media print {
body { background: #fff; color: #000; }
.quotation-sheet {
position: static;
left: 0;
display: block;
}
.no-print { display: none !important; }
}
</style>
</head>
<body>
<div id="root"><div style="display:flex;height:100vh;align-items:center;justify-content:center;color:#333;">Cargando experiencia CNTXT®...</div></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// --- CONFIGURACIÓN ---
const CLIENT_PORTAL_URL = "/portal-clientes"; // URL a donde se redirige al cliente
// --- ICONOS ---
const Icons = {
Cube: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21.12 6.4-6.05-4.06a2 2 0 0 0-2.17-.05L2.9 8.12a2 2 0 0 0-.9 1.7v9.58a2 2 0 0 0 1.12 1.83l12.9 4.43a2 2 0 0 0 1.37 0l5.5-2.2a2 2 0 0 0 1.1-1.83V8.12a2 2 0 0 0-.87-1.73ZM12 3v18.75"/></svg>,
Home: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>,
Check: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>,
ChevronRight: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>,
ArrowLeft: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>,
Plus: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>,
Eye: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>,
Video: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg>,
Globe: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>,
Layers: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>,
Zap: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
Loader: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>,
Printer: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect width="12" height="8" x="6" y="14"/></svg>,
Minus: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>,
Copy: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>,
Download: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>,
User: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
Lock: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>,
FileText: (p)=><svg {...p} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
};
// --- SERVICIOS ---
const BASE_PLAN = {
id: 'esencia_exterior',
name: 'ESENCIA EXTERIOR',
tagline: 'Visualización Arquitectónica Integral',
price: 3200000,
description: 'Paquete fundamental para entender volumetría y materialidad. Incluye modelado y visualización completa del edificio desde el exterior.',
features: [
'V0.1 Modelado arquitectónico sobre planos',
'V0.3 Modelo 3D de edificios exteriores',
'V1.2 Pack ilimitado imágenes exteriores',
'V4.1 Planimetría comercial ambientada'
]
};
const ADDON_CATEGORIES = [
{
name: "Interiorismo",
items: [
{ id: 'v0_4', code: 'V0.4', title: 'Modelo Ambientación Interior', price: 1200000, desc: 'Modelado de detalles especiales: Zócalos, cielos, muebles fijos.', icon: <Icons.Home/> },
{ id: 'v1_3', code: 'V1.3', title: 'Pack Renders Interiores', price: 800000, desc: 'Pack ilimitado de imágenes de interiores con iluminación configurada.', icon: <Icons.Home/> },
]
},
{
name: "Video & Animación",
items: [
{ id: 'v2_3', code: 'V2.3', title: 'Clip Comercial (40 seg)', price: 1200000, desc: 'Video cinematográfico versión Trailer para redes (720p).', icon: <Icons.Video/> },
{ id: 'v2_4', code: 'V2.4', title: 'Clip Emocional (1:20 min)', price: 2200000, desc: 'Video versión Trailer extendido cinematográfico (720p).', icon: <Icons.Video/> },
{ id: 'v2_1', code: 'V2.1', title: 'Video Comercial (2:30 min)', price: 4500000, desc: 'Video 2K con guion y dirección creativa completa.', icon: <Icons.Video/> },
{ id: 'v2_2', code: 'V2.2', title: 'Video Recorrido Espacial', price: 1800000, desc: 'Recorrido de entendimiento espacial en primera persona.', icon: <Icons.Video/> },
{ id: 'v2_5', code: 'V2.5', title: 'Escalado 8K (Upgrade)', price: 500000, desc: 'Escalado de videos hasta resolución 8K mediante IA.', icon: <Icons.Zap/> },
]
},
{
name: "Interactivo & Inmersivo",
items: [
{ id: 'v3_1', code: 'V3.1', title: 'Recorridos 360°', price: 950000, desc: '4 imágenes 360° alojadas en Kuula. Experiencia interactiva.', icon: <Icons.Eye/> },
{ id: 'v3_2', code: 'V3.2', title: 'Recorrido en Tiempo Real', price: 1500000, desc: 'Servicio en vivo (2 horas) para eventos o lanzamientos.', icon: <Icons.Eye/> },
{ id: 'v0_5', code: 'V0.5', title: 'Gaussian Splatting', price: 1500000, desc: 'Modelo implantado realista del entorno para plataformas.', icon: <Icons.Layers/> },
]
},
{
name: "Web & Comercial",
items: [
{ id: 'v5_1', code: 'V5.1', title: 'Web Inmobiliaria Basic', price: 2500000, desc: 'Diseño y montaje web con dominio propio.', icon: <Icons.Globe/> },
{ id: 'v5_4', code: 'V5.4', title: 'Landing Page Lanzamiento', price: 1800000, desc: 'Plataforma web para lanzamiento comercial (Wix).', icon: <Icons.Globe/> },
{ id: 'v5_3', code: 'V5.3', title: 'Web Lite (Web3D)', price: 3500000, desc: 'Plataforma web interactiva con integración 3D (Hasta 100 Und).', icon: <Icons.Globe/> },
{ id: 'v5_5', code: 'V5.5', title: 'Ficha de Lote Web', price: 1200000, desc: 'Link de interfaz con visita a lote y video drone.', icon: <Icons.Globe/> },
{ id: 'v5_2', code: 'V5.2', title: 'Brochure Digital/Print', price: 850000, desc: 'Diagramación PDF para envío virtual e impresión.', icon: <Icons.Layers/> },
]
}
];
const ALL_ADDONS = ADDON_CATEGORIES.flatMap(cat => cat.items);
// --- UTILIDADES ---
const formatCurrency = (val) => new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(val);
const generateOrderId = () => `VC-${Math.floor(10000 + Math.random() * 90000)}-${new Date().getFullYear().toString().substr(-2)}`;
const generatePassword = () => Math.random().toString(36).slice(-8).toUpperCase();
// --- COMPONENTES ---
const ProgressBar = ({ step }) => (
<div className="w-full h-1 bg-gray-900 sticky top-0 z-50">
<div className="h-full bg-[#D4AF37] transition-all duration-700 ease-out" style={{ width: `${(step / 3) * 100}%` }}></div>
</div>
);
const BasePlanCard = ({ plan }) => (
<div className="bg-neutral-900 border border-neutral-800 p-6 md:p-8 flex flex-col md:flex-row gap-8 items-start animate-fade-in relative overflow-hidden">
<div className="flex-grow z-10">
<div className="flex items-center gap-3 mb-3">
<Icons.Cube className="text-[#D4AF37] w-5 h-5" />
<h4 className="text-[#D4AF37] text-xs uppercase tracking-[0.2em] font-bold">{plan.tagline}</h4>
</div>
<h2 className="text-3xl md:text-4xl font-light text-white mb-4">{plan.name}</h2>
<p className="text-gray-400 font-light mb-8 max-w-xl text-sm leading-relaxed">{plan.description}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-3 gap-x-6">
{plan.features.map((feat, i) => (
<div key={i} className="flex items-start gap-3">
<div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-[#D4AF37] shrink-0"></div>
<span className="text-xs text-gray-300 uppercase tracking-wider">{feat}</span>
</div>
))}
</div>
</div>
<div className="w-full md:w-auto min-w-[180px] border-t md:border-t-0 md:border-l border-neutral-800 pt-6 md:pt-0 md:pl-8 text-right flex flex-row md:flex-col justify-between items-center md:items-end z-10">
<span className="text-[10px] text-gray-500 uppercase tracking-widest mb-1">Inversión Base (Unitario)</span>
<span className="text-3xl font-light text-white">{formatCurrency(plan.price)}</span>
</div>
</div>
);
const QuantitySelector = ({ quantity, setQuantity }) => (
<div className="max-w-5xl mx-auto mb-16 animate-fade-in">
<div className="flex flex-col md:flex-row items-center justify-between bg-black border-y border-neutral-800 py-6 px-4 md:px-0">
<div className="flex items-center gap-4 mb-4 md:mb-0">
<div className="p-3 bg-neutral-900 rounded-full text-[#D4AF37]">
<Icons.Copy className="w-5 h-5" />
</div>
<div>
<h3 className="text-sm font-bold text-white uppercase tracking-widest">Modelos de Vivienda</h3>
<p className="text-xs text-gray-500">¿Cuántas tipologías diferentes deseas cotizar?</p>
</div>
</div>
<div className="flex items-center gap-6">
<button onClick={() => setQuantity(q => Math.max(1, q - 1))} className="w-10 h-10 flex items-center justify-center border border-neutral-700 hover:border-white text-gray-400 hover:text-white transition-all rounded-sm hover:bg-neutral-900"><Icons.Minus className="w-4 h-4" /></button>
<div className="text-center w-12"><span className="text-3xl font-light text-[#D4AF37]">{quantity}</span></div>
<button onClick={() => setQuantity(q => q + 1)} className="w-10 h-10 flex items-center justify-center border border-neutral-700 hover:border-white text-gray-400 hover:text-white transition-all rounded-sm hover:bg-neutral-900"><Icons.Plus className="w-4 h-4" /></button>
</div>
</div>
</div>
);
const AddonItem = ({ item, isSelected, onToggle }) => (
<label className="relative cursor-pointer group animate-fade-in h-full block">
<input type="checkbox" className="custom-checkbox sr-only" checked={isSelected} onChange={() => onToggle(item.id)}/>
<div className={`p-5 border transition-all duration-200 h-full flex flex-col justify-between relative ${isSelected ? 'bg-neutral-900 border-[#D4AF37]' : 'bg-black border-neutral-800 hover:border-neutral-700'}`}>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<span className="text-[10px] text-gray-600 font-mono border border-neutral-800 px-1 rounded">{item.code}</span>
</div>
<div className={`w-4 h-4 border rounded-[2px] flex items-center justify-center transition-all duration-200 check-icon ${isSelected ? 'bg-[#D4AF37] border-[#D4AF37] opacity-100' : 'border-neutral-700 opacity-20'}`}>
<Icons.Check className="w-2.5 h-2.5 text-black" />
</div>
</div>
<div>
<div className={`text-gray-500 mb-2 ${isSelected ? 'text-[#D4AF37]' : ''}`}>{item.icon}</div>
<h3 className="text-sm font-medium mb-2 text-gray-300 leading-tight group-hover:text-white transition-colors">{item.title}</h3>
<p className="text-[10px] text-gray-500 mb-4 leading-relaxed min-h-[30px]">{item.desc}</p>
<div className="border-t border-neutral-800 pt-3 mt-auto">
<p className="text-xs font-mono text-gray-400 price-tag">+ {formatCurrency(item.price)}</p>
</div>
</div>
</div>
</label>
);
// --- DOCUMENTO PDF OCULTO ---
const HiddenPDFDocument = ({ orderId, formData, quantity, basePrice, selectedAddons, ALL_ADDONS, subtotal, total }) => (
<div id="quotation-document" className="quotation-sheet">
{/* Estilos inline para asegurar PDF exacto */}
<div style={{display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #ddd', paddingBottom: '30px', marginBottom: '30px'}}>
<div>
<h1 style={{fontSize: '36px', fontWeight: 'bold', margin: '0', color: '#000'}}>CNTXT®</h1>
<p style={{fontSize: '10px', textTransform: 'uppercase', letterSpacing: '3px', color: '#666', margin: '5px 0 0 0'}}>Arquitectura & Visualización</p>
</div>
<div style={{textAlign: 'right'}}>
<h2 style={{fontSize: '24px', fontWeight: '300', color: '#333', margin: '0', textTransform: 'uppercase'}}>Cotización</h2>
<p style={{fontSize: '16px', color: '#000', margin: '5px 0 0 0', fontFamily: 'monospace'}}>{orderId}</p>
<p style={{fontSize: '12px', color: '#666', margin: '5px 0 0 0'}}>{new Date().toLocaleDateString()}</p>
</div>
</div>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '40px', marginBottom: '40px'}}>
<div>
<h4 style={{fontSize: '10px', fontWeight: 'bold', textTransform: 'uppercase', color: '#999', margin: '0 0 10px 0'}}>Cliente</h4>
<p style={{fontSize: '16px', fontWeight: 'bold', margin: '0 0 5px 0', color: '#000'}}>{formData.name}</p>
<p style={{fontSize: '12px', color: '#333', margin: '0'}}>{formData.email}</p>
<p style={{fontSize: '12px', color: '#333', margin: '0'}}>{formData.phone}</p>
</div>
<div>
<h4 style={{fontSize: '10px', fontWeight: 'bold', textTransform: 'uppercase', color: '#999', margin: '0 0 10px 0'}}>Proyecto</h4>
<p style={{fontSize: '16px', fontWeight: 'bold', margin: '0 0 5px 0', color: '#000'}}>{formData.projectType || 'Vivienda Campestre'}</p>
<p style={{fontSize: '12px', color: '#333', margin: '0 0 5px 0'}}>{formData.location}</p>
<p style={{fontSize: '12px', color: '#000', fontWeight: 'bold', margin: '0'}}>{quantity} Modelo(s) de Vivienda</p>
</div>
</div>
<table style={{width: '100%', borderCollapse: 'collapse', marginBottom: '40px'}}>
<thead>
<tr style={{borderBottom: '1px solid #eee'}}>
<th style={{padding: '10px 0', textAlign: 'left', fontSize: '10px', textTransform: 'uppercase', color: '#999'}}>Descripción</th>
<th style={{padding: '10px 0', textAlign: 'right', fontSize: '10px', textTransform: 'uppercase', color: '#999'}}>Cant.</th>
<th style={{padding: '10px 0', textAlign: 'right', fontSize: '10px', textTransform: 'uppercase', color: '#999'}}>Unitario</th>
<th style={{padding: '10px 0', textAlign: 'right', fontSize: '10px', textTransform: 'uppercase', color: '#999'}}>Total</th>
</tr>
</thead>
<tbody>
<tr style={{borderBottom: '1px solid #f5f5f5'}}>
<td style={{padding: '15px 0'}}>
<p style={{fontWeight: 'bold', margin: '0 0 5px 0', fontSize: '14px', color: '#000'}}>{BASE_PLAN.name}</p>
<p style={{fontSize: '10px', color: '#666', margin: '0', maxWidth: '400px'}}>{BASE_PLAN.description}</p>
</td>
<td style={{padding: '15px 0', textAlign: 'right', fontFamily: 'monospace', fontSize: '12px', color: '#333'}}>{quantity}</td>
<td style={{padding: '15px 0', textAlign: 'right', fontFamily: 'monospace', fontSize: '12px', color: '#333'}}>{formatCurrency(basePrice)}</td>
<td style={{padding: '15px 0', textAlign: 'right', fontFamily: 'monospace', fontSize: '12px', fontWeight: 'bold', color: '#000'}}>{formatCurrency(basePrice * quantity)}</td>
</tr>
{selectedAddons.map(id => {
const item = ALL_ADDONS.find(a => a.id === id);
return (
<tr key={id} style={{borderBottom: '1px solid #f5f5f5'}}>
<td style={{padding: '12px 0 12px 20px'}}>
<p style={{margin: '0 0 3px 0', fontSize: '12px', color: '#333', fontWeight: '500'}}>+ {item.title}</p>
<p style={{fontSize: '10px', color: '#999', margin: '0'}}>{item.desc}</p>
</td>
<td style={{padding: '12px 0', textAlign: 'right', fontFamily: 'monospace', fontSize: '12px', color: '#666'}}>{quantity}</td>
<td style={{padding: '12px 0', textAlign: 'right', fontFamily: 'monospace', fontSize: '12px', color: '#666'}}>{formatCurrency(item.price)}</td>
<td style={{padding: '12px 0', textAlign: 'right', fontFamily: 'monospace', fontSize: '12px', color: '#333'}}>{formatCurrency(item.price * quantity)}</td>
</tr>
);
})}
</tbody>
</table>
<div style={{display: 'flex', justifyContent: 'flex-end', marginBottom: '60px'}}>
<div style={{width: '250px'}}>
<div style={{display: 'flex', justifyContent: 'space-between', padding: '5px 0', fontSize: '12px', color: '#666'}}>
<span>Subtotal</span>
<span style={{fontFamily: 'monospace'}}>{formatCurrency(subtotal)}</span>
</div>
<div style={{display: 'flex', justifyContent: 'space-between', padding: '5px 0', fontSize: '12px', color: '#666', borderBottom: '1px solid #eee'}}>
<span>IVA (19%)</span>
<span style={{fontFamily: 'monospace'}}>{formatCurrency(subtotal * 0.19)}</span>
</div>
<div style={{display: 'flex', justifyContent: 'space-between', padding: '15px 0', fontSize: '18px', fontWeight: 'bold', color: '#000'}}>
<span>Total</span>
<span>{formatCurrency(total)}</span>
</div>
</div>
</div>
<div style={{textAlign: 'center', borderTop: '1px solid #eee', paddingTop: '30px'}}>
<p style={{fontSize: '10px', textTransform: 'uppercase', letterSpacing: '2px', color: '#999', margin: '0 0 5px 0'}}>Contexto Arquitectura S.A.S</p>
<p style={{fontSize: '9px', color: '#aaa', margin: '0'}}>Cotización válida por 15 días. Sujeta a verificación final.</p>
</div>
</div>
);
// --- APP PRINCIPAL ---
const App = () => {
const [step, setStep] = useState(1);
const [selectedAddons, setSelectedAddons] = useState([]);
const [quantity, setQuantity] = useState(1);
const [formData, setFormData] = useState({ name: '', email: '', phone: '', location: '', projectType: '' });
const [orderId] = useState(generateOrderId());
const [generatedPassword] = useState(generatePassword());
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
// Cálculos
const basePrice = BASE_PLAN.price;
const addonsPrice = selectedAddons.reduce((sum, id) => {
const addon = ALL_ADDONS.find(a => a.id === id);
return sum + (addon ? addon.price : 0);
}, 0);
const unitSubtotal = basePrice + addonsPrice;
const subtotal = unitSubtotal * quantity;
const total = subtotal * 1.19;
const toggleAddon = (id) => {
if (selectedAddons.includes(id)) setSelectedAddons(prev => prev.filter(x => x !== id));
else setSelectedAddons(prev => [...prev, id]);
};
const handleInput = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleDownloadPDF = () => {
const element = document.getElementById('quotation-document');
if (!element) return;
setIsGeneratingPDF(true);
const opt = {
margin: 0,
filename: `Cotizacion_CNTXT_${orderId}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => setIsGeneratingPDF(false));
};
const handleGoToPortal = () => {
// Redirigir al portal de clientes
window.location.href = CLIENT_PORTAL_URL;
};
const handleSubmitData = () => {
setIsGeneratingPDF(true);
// Simulación de envío al backend para crear usuario
setTimeout(() => {
setStep(3); // Ir al Dashboard
setIsGeneratingPDF(false);
}, 1500);
};
return (
<div className="min-h-screen bg-black text-white selection:bg-[#D4AF37] selection:text-black pb-48 font-light">
<ProgressBar step={step} />
{/* RENDERIZADO DEL PDF OCULTO SIEMPRE PRESENTE PARA GENERACIÓN */}
<HiddenPDFDocument
orderId={orderId}
formData={formData}
quantity={quantity}
basePrice={basePrice}
selectedAddons={selectedAddons}
ALL_ADDONS={ALL_ADDONS}
subtotal={subtotal}
total={total}
/>
<main className="max-w-7xl mx-auto px-4 py-12 md:py-16">
{/* PASO 1: SELECCIÓN */}
{step === 1 && (
<div className="space-y-12 animate-fade-in">
<div className="text-center mb-8">
<span className="text-[#D4AF37] text-[10px] uppercase tracking-[0.3em]">Configurador</span>
<h1 className="text-3xl md:text-5xl font-extralight mt-3 tracking-tight">Vivienda Campestre</h1>
</div>
<section className="max-w-5xl mx-auto">
<BasePlanCard plan={BASE_PLAN} />
</section>
<QuantitySelector quantity={quantity} setQuantity={setQuantity} />
<section>
<div className="text-center mb-10">
<h3 className="text-sm uppercase tracking-widest text-white mb-2">Personalización Extendida</h3>
<p className="text-xs text-gray-500">Selecciona los servicios complementarios por tipología</p>
</div>
<div className="space-y-12">
{ADDON_CATEGORIES.map((cat, idx) => (
<div key={idx}>
<div className="flex items-center gap-4 mb-6">
<h4 className="text-xs font-bold text-[#D4AF37] uppercase tracking-widest shrink-0">{cat.name}</h4>
<div className="h-px bg-neutral-900 flex-grow"></div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{cat.items.map(addon => (
<AddonItem key={addon.id} item={addon} isSelected={selectedAddons.includes(addon.id)} onToggle={toggleAddon} />
))}
</div>
</div>
))}
</div>
<div className="h-40 md:h-48"></div>
</section>
<div className="fixed bottom-0 left-0 w-full bg-black/95 backdrop-blur-md border-t border-neutral-800 p-4 md:px-8 z-40 transition-all shadow-2xl">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-center md:text-left flex items-center gap-6">
<div>
<p className="text-[10px] text-gray-500 uppercase tracking-widest mb-1">Configuración</p>
<span className="text-white font-mono text-sm">{quantity} Modelo{quantity > 1 ? 's' : ''} x ({1 + selectedAddons.length} Servicios)</span>
</div>
<div className="h-8 w-px bg-neutral-800 hidden md:block"></div>
<div>
<p className="text-[10px] text-gray-500 uppercase tracking-widest mb-1">Total Estimado</p>
<div className="text-2xl text-[#D4AF37] font-light">{formatCurrency(total)}</div>
</div>
</div>
<button onClick={() => setStep(2)} className="w-full md:w-auto bg-white hover:bg-gray-200 text-black px-10 py-3 text-xs font-bold uppercase tracking-[0.15em] transition-all flex justify-center items-center gap-3">
Continuar <Icons.ChevronRight className="w-3 h-3" />
</button>
</div>
</div>
</div>
)}
{/* PASO 2: DATOS DEL CLIENTE */}
{step === 2 && (
<div className="max-w-3xl mx-auto animate-fade-in">
<button onClick={() => setStep(1)} className="flex items-center gap-2 text-gray-500 hover:text-white mb-10 text-[10px] uppercase tracking-widest transition-colors"><Icons.ArrowLeft className="w-3 h-3" /> Volver a Configuración</button>
<div className="border border-neutral-800 bg-neutral-900/50 p-8 md:p-12">
<div className="mb-10 text-center">
<h2 className="text-2xl md:text-3xl font-light text-white mb-2">Crear Cuenta & Cotizar</h2>
<p className="text-gray-500 text-sm font-light">Generaremos tu usuario y documento oficial automáticamente.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10">
<div className="col-span-1 md:col-span-2"><label className="block text-[10px] uppercase tracking-widest text-gray-500 mb-2">Nombre Completo</label><input type="text" name="name" onChange={handleInput} className="w-full bg-black border border-neutral-700 p-4 text-white text-sm focus:border-[#D4AF37] outline-none transition-colors placeholder-neutral-700" placeholder="Nombre Apellido" /></div>
<div><label className="block text-[10px] uppercase tracking-widest text-gray-500 mb-2">Email (Será tu usuario)</label><input type="email" name="email" onChange={handleInput} className="w-full bg-black border border-neutral-700 p-4 text-white text-sm focus:border-[#D4AF37] outline-none transition-colors placeholder-neutral-700" placeholder="correo@ejemplo.com" /></div>
<div><label className="block text-[10px] uppercase tracking-widest text-gray-500 mb-2">Teléfono</label><input type="tel" name="phone" onChange={handleInput} className="w-full bg-black border border-neutral-700 p-4 text-white text-sm focus:border-[#D4AF37] outline-none transition-colors placeholder-neutral-700" placeholder="+57 300..." /></div>
<div><label className="block text-[10px] uppercase tracking-widest text-gray-500 mb-2">Ubicación</label><input type="text" name="location" onChange={handleInput} className="w-full bg-black border border-neutral-700 p-4 text-white text-sm focus:border-[#D4AF37] outline-none transition-colors placeholder-neutral-700" placeholder="Ciudad / Municipio" /></div>
<div><label className="block text-[10px] uppercase tracking-widest text-gray-500 mb-2">Área Aprox.</label><input type="text" name="projectType" onChange={handleInput} className="w-full bg-black border border-neutral-700 p-4 text-white text-sm focus:border-[#D4AF37] outline-none transition-colors placeholder-neutral-700" placeholder="Ej: 250 m2" /></div>
</div>
<button onClick={handleSubmitData} disabled={!formData.email || !formData.name || isGeneratingPDF} className={`w-full py-4 text-xs font-bold uppercase tracking-[0.2em] transition-all flex justify-center items-center gap-2 ${(!formData.email || !formData.name) ? 'bg-neutral-800 text-gray-600 cursor-not-allowed' : 'bg-[#D4AF37] hover:bg-[#b5952f] text-black'}`}>
{isGeneratingPDF ? <><Icons.Loader className="animate-spin w-4 h-4"/> Procesando...</> : "Finalizar & Crear Cuenta"}
</button>
<p className="text-[10px] text-gray-600 text-center mt-4">Al continuar, aceptas recibir tus credenciales de acceso al correo ingresado.</p>
</div>
</div>
)}
{/* PASO 3: DASHBOARD CLIENTE (NUEVO) */}
{step === 3 && (
<div className="animate-fade-in">
{/* HEADER DASHBOARD */}
<div className="flex flex-col md:flex-row justify-between items-end border-b border-neutral-800 pb-6 mb-10">
<div>
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-[10px] uppercase tracking-[0.2em] text-gray-400">Cuenta Creada</p>
</div>
<h2 className="text-3xl font-light text-white">Hola, {formData.name.split(' ')[0]}</h2>
</div>
<div className="text-right mt-4 md:mt-0">
<p className="text-[10px] text-gray-500 uppercase tracking-widest">Tus Credenciales han sido enviadas a:</p>
<p className="text-sm text-[#D4AF37] font-mono">{formData.email}</p>
<p className="text-[10px] text-gray-600 mt-1">Contraseña temporal: <span className="text-white">********</span></p>
</div>
</div>
{/* SECCIÓN COTIZACIÓN ACTUAL */}
<div className="bg-neutral-900 border border-neutral-800 p-8 mb-12">
<div className="flex justify-between items-start mb-6">
<div>
<h3 className="text-lg font-medium text-white mb-1">Cotización Reciente</h3>
<p className="text-xs text-gray-500 font-mono">{orderId} • {new Date().toLocaleDateString()}</p>
</div>
<div className="bg-black/50 px-4 py-2 rounded border border-neutral-800">
<p className="text-[10px] text-gray-500 uppercase tracking-widest text-center">Estado</p>
<p className="text-xs text-[#D4AF37] font-bold uppercase">Pendiente Firma</p>
</div>
</div>
<div className="flex flex-col md:flex-row gap-8 items-center bg-black p-6 border border-neutral-800 mb-8">
<div className="flex-grow">
<p className="text-sm text-gray-300 font-medium mb-1">Vivienda Campestre - {quantity} Modelo(s)</p>
<p className="text-xs text-gray-500">Incluye: {BASE_PLAN.name} + {selectedAddons.length} Adicionales</p>
</div>
<div className="text-right">
<p className="text-xs text-gray-500 uppercase tracking-widest">Valor Total</p>
<p className="text-xl font-light text-white">{formatCurrency(total)}</p>
</div>
</div>
{/* ACCIONES DEL DASHBOARD */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button onClick={handleDownloadPDF} className="py-4 px-6 border border-neutral-700 hover:border-white text-gray-300 hover:text-white transition-all flex justify-center items-center gap-3 uppercase text-xs tracking-widest font-bold">
<Icons.Download className="w-4 h-4"/> Descargar PDF
</button>
<button onClick={handleGoToPortal} className="py-4 px-6 bg-[#D4AF37] hover:bg-[#b5952f] text-black transition-all flex justify-center items-center gap-3 uppercase text-xs tracking-widest font-bold shadow-lg shadow-[#D4AF37]/20">
Continuar al Portal de Clientes <Icons.ChevronRight className="w-4 h-4"/>
</button>
</div>
</div>
{/* HISTORIAL (SIMULADO) */}
<div>
<h4 className="text-xs text-gray-500 uppercase tracking-widest mb-4 border-b border-neutral-800 pb-2">Historial de Documentos</h4>
<div className="opacity-50 pointer-events-none">
<div className="flex justify-between items-center py-4 border-b border-neutral-900">
<div className="flex items-center gap-3">
<div className="p-2 bg-neutral-800 rounded"><Icons.FileText className="w-4 h-4 text-gray-500"/></div>
<div>
<p className="text-xs text-gray-400">Cotización Anterior</p>
<p className="text-[10px] text-gray-600">No hay documentos previos</p>
</div>
</div>
<span className="text-[10px] text-gray-700">--/--/----</span>
</div>
</div>
</div>
<div className="h-20"></div>
</div>
)}
</main>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>