/* Torre Auro — "Ecosistema en construcción" Cada oficina disponible es un proyecto de licitación abierta. Cualquiera se suma desde su rol → da vida al ecosistema desde los preparativos. */ const PRJ_STORE_KEY = 'ta_proyectos_v1'; function prjLoad() { try { return JSON.parse(localStorage.getItem(PRJ_STORE_KEY)) || {}; } catch (e) { return {}; } } function prjSave(s) { try { localStorage.setItem(PRJ_STORE_KEY, JSON.stringify(s)); } catch (e) {} } function prjHash(str) { let h = 0; for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0; return h; } const PROJECT_ROLES = [ { id:'comercial', name:'Comercial / Broker', icon:'ti-briefcase', tagline:'Trae al inquilino ideal', contrib:'Promueve el espacio y conecta con la empresa que lo ocupará.', incentive:'Comisión por renta concretada', cupos:3 }, { id:'admin', name:'Administrativo', icon:'ti-file-text', tagline:'Gestión y contrato', contrib:'Coordina papeleo, contrato y enlace con el desarrollador.', incentive:'Honorarios + reputación', cupos:1 }, { id:'obra', name:'Proveedor de obra', icon:'ti-bucket-droplet', tagline:'Pintura, pisos, acondicionamiento', contrib:'Acondiciona la obra gris según el concepto del inquilino.', incentive:'Contrato de la obra', cupos:4 }, { id:'mobiliario', name:'Mobiliario y equipamiento', icon:'ti-armchair', tagline:'Amobla el espacio', contrib:'Provee mobiliario, equipo y soluciones de oficina.', incentive:'Contrato de suministro', cupos:3 }, { id:'interiores', name:'Diseño de interiores', icon:'ti-ruler-2', tagline:'Arquitectura y layout', contrib:'Diseña la distribución y la imagen del espacio.', incentive:'Contrato de diseño', cupos:2 }, { id:'branding', name:'Branding y señalización', icon:'ti-tag', tagline:'Identidad del espacio', contrib:'Señalización, rotulación y presencia de marca.', incentive:'Contrato de imagen', cupos:2 }, { id:'mudanzas', name:'Mudanzas y logística', icon:'ti-truck', tagline:'Traslado e instalación', contrib:'Logística de instalación y mudanza del inquilino.', incentive:'Contrato de servicio', cupos:2 }, { id:'inversionista',name:'Inversionista / Patrocinador',icon:'ti-coin', tagline:'Respalda el proyecto', contrib:'Aporta capital o patrocinio para acelerar el acondicionamiento.', incentive:'Retorno + reputación', cupos:2 }, { id:'marketing', name:'Marketing', icon:'ti-chart-arrows-vertical', tagline:'Campaña y contenido', contrib:'Estrategia de difusión, contenido y campañas digitales.', incentive:'Honorarios + reputación', cupos:2 }, { id:'embajador', name:'Embajador / Promotor', icon:'ti-speakerphone', tagline:'Difunde en sus redes', contrib:'Promociona el inmueble en su comunidad y redes sociales.', incentive:'Puntos + recompensa por referido', cupos:99 }, ]; const PRJ_NAMES = ['MV','RT','AL','GP','JC','DS','EN','LF','BR','OM','CV','TH','NK','PA','SR','Yed','Zam']; function ProjectModal() { const [open, setOpen] = useState(false); const [ctx, setCtx] = useState(null); // { floorId, code } const [view, setView] = useState('overview'); // overview | apply | done const [role, setRole] = useState(null); const [form, setForm] = useState({ nombre:'', contacto:'', propuesta:'' }); const [store, setStore] = useState(prjLoad()); const bodyRef = useRef(null); useEffect(() => { const onOpen = (e) => { setCtx(e.detail); setView('overview'); setRole(null); setForm({ nombre:'', contacto:'', propuesta:'' }); setStore(prjLoad()); setOpen(true); }; window.addEventListener('ta:open-project', onOpen); return () => window.removeEventListener('ta:open-project', onOpen); }, []); useEffect(() => { if (open) document.body.style.overflow = 'hidden'; else document.body.style.overflow = ''; return () => { document.body.style.overflow = ''; }; }, [open]); if (!ctx) return
; const floor = FLOOR_BY_ID[ctx.floorId]; const office = floor?.offices.find(o => o.code === ctx.code); if (!floor || !office) return
; const z = ZONES[floor.zone]; const acc = z.accHex; const ecoName = z.tag; const key = floor.id + '::' + office.code; const userApps = store[key] || []; // deterministic base data const seed = prjHash(key); const baseApplicants = (rid) => prjHash(key + rid) % 4; const appliedRoles = new Set(userApps.map(a => a.role)); const countFor = (rid) => baseApplicants(rid) + userApps.filter(a => a.role === rid).length; const totalApplicants = PROJECT_ROLES.reduce((a, r) => a + countFor(r.id), 0); const rolesWithTeam = PROJECT_ROLES.filter(r => countFor(r.id) > 0).length; // progress / stage const rented = !office.ok; let progress, stage; if (rented) { progress = 100; stage = 'Operando'; } else { const base = 22 + (seed % 26); // 22–47 baseline const boost = Math.min(28, userApps.length * 6); // user participation pushes it progress = Math.min(96, base + boost); stage = progress < 30 ? 'Publicado' : progress < 60 ? 'Licitación abierta' : progress < 85 ? 'Equipo en formación' : 'Acondicionamiento'; } const daysLeft = rented ? 0 : 9 + (seed % 25); // participants (anonymized initials) const participants = []; PROJECT_ROLES.forEach((r, i) => { const n = countFor(r.id); for (let k = 0; k < Math.min(n, 4); k++) { participants.push({ ini: PRJ_NAMES[(seed + i * 7 + k * 3) % PRJ_NAMES.length], role: r.name, rid: r.id }); } }); // supplier proposals (visible, summary only) const PROPOSALS = [ { rid:'obra', who:'Acabados del Golfo', text:'Pintura + piso porcelánico, obra gris a llave', range:'$120K – $180K', eta:'4 semanas' }, { rid:'mobiliario', who:'Oficina Integral MX', text:'Estaciones, sala de juntas y recepción', range:'$90K – $140K', eta:'3 semanas' }, { rid:'interiores', who:'Estudio Litoral', text:'Proyecto ejecutivo + dirección de obra', range:'$60K – $95K', eta:'2 semanas' }, ].filter((_, i) => ((seed >> i) & 1) || i === 0); function applyTo(r) { setRole(r); setView('apply'); } function submit(e) { e.preventDefault(); const next = { ...store, [key]: [...userApps, { role: role.id, nombre: form.nombre, ts: Date.now() }] }; prjSave(next); setStore(next); setView('done'); } return (
setOpen(false)} />
{/* HEADER */}
Ecosistema en construcción · Licitación abierta
{office.code}
{floor.sh} · {ecoName} · {office.m2} m² · {office.type}
{stage}

{rented ? 'Este espacio ya está operando. Su proyecto fue construido por la comunidad del ecosistema.' : 'Este espacio es un proyecto abierto. Cualquier persona puede sumarse desde su rol para que se concrete — de forma transparente, como una licitación colectiva.'}

{view === 'overview' && ( <> {/* PROGRESS */}
{stage} {progress}%
{totalApplicants} participantes · {rolesWithTeam}/{PROJECT_ROLES.length} roles activos {rented ? 'Proyecto completado' : daysLeft + ' días para cerrar equipo'}
{/* ROLES */}

Ubicación en el plano

{office.code} · {office.type} · {office.m2} m²
{floor.planClass && (
{office.code} {office.use} · {office.m2} m²
)}
{/* ROLES */}

Súmate desde tu rol

Elige cómo aportas al proyecto
{PROJECT_ROLES.map(r => { const n = countFor(r.id); const did = appliedRoles.has(r.id); const full = r.cupos < 90 && n >= r.cupos; return (
{r.name}
{r.tagline}
{r.incentive}
{n} {r.cupos < 90 ? `de ${r.cupos}` : 'sumados'}
{rented ? ( ) : did ? ( Aplicaste ) : full ? ( Completo ) : ( )}
); })}
{/* PARTICIPANTS */}

Quiénes se han sumado

Resumen público · sin datos sensibles
{participants.slice(0, 14).map((p, i) => (
{p.ini}
))} {participants.length > 14 && (
+{participants.length - 14}
)} {participants.length === 0 && (
Sé el primero en sumarte a este proyecto.
)}
{/* PROPOSALS */} {PROPOSALS.length > 0 && (

Propuestas de proveedores

Ofertas visibles · transparencia de licitación
{PROPOSALS.map((p, i) => { const r = PROJECT_ROLES.find(x => x.id === p.rid); return (
{r?.name} {p.eta}
{p.who}
{p.text}
{p.range}
); })}
)} {/* INCENTIVE FOOTER */}
Cada participante gana comisión por renta concretada o el contrato de su servicio, y suma puntos de reputación en el ecosistema Torre Auro.
)} {view === 'apply' && role && (
{role.name}
{office.code} · {floor.sh} · {ecoName}

{role.contrib}

{role.incentive}
setForm({ ...form, nombre: e.target.value })} placeholder="¿Cómo te identificas en el ecosistema?" />
setForm({ ...form, contacto: e.target.value })} placeholder="Correo o teléfono" />