// KSANET — Configurador v2 · Step 2 (Personalización de acabados) const { useState, useEffect, useMemo } = React; const ChevronP = ({ dir = 'd' }) => ( ); const StepperP = ({ step, onGo }) => { const steps = [ { n: '01', label: 'Vivienda', short: 'Tu vivienda' }, { n: '02', label: 'Acabados', short: 'Personalización' }, { n: '03', label: 'Plan económico', short: 'Aportaciones' }, { n: '04', label: 'Reserva', short: 'Confirmación' }, ]; return ( ); }; const TopBarP = ({ promoId }) => (
Volver a la vivienda
KSANET Configurador · acabados
); // Room visualization (CSS-perspective room with dynamic colors) const RoomScene = ({ picks, room }) => { const cat = (id) => window.PERS_CATEGORIES.find(c => c.id === id); const opt = (catId, optId) => cat(catId)?.options.find(o => o.id === optId); const suelo = opt('suelo', picks.suelo); const cocina = opt('cocina', picks.cocina); const enc = opt('encimera', picks.encimera); const bano = opt('bano', picks.bano); // Wall is constant warm; floor + island change const styleVars = { '--floor-color': suelo?.swatch || '#c8a878', '--floor-pattern': (suelo?.pattern === 'pat-wood') ? 'repeating-linear-gradient(90deg, rgba(0,0,0,0.06) 0 1px, transparent 1px 12%, rgba(0,0,0,0.04) 12% 12.5%, transparent 12.5% 24%)' : (suelo?.pattern === 'pat-stone') ? 'radial-gradient(circle at 20% 30%, rgba(0,0,0,0.06) 0 14%, transparent 14%), radial-gradient(circle at 70% 60%, rgba(255,255,255,0.18) 0 10%, transparent 10%)' : 'none', '--island-color': (room === 'cocina' ? cocina?.swatch : '#2a2825') || '#2a2825', '--counter-color': enc?.swatch || '#d8d2c3', '--wall-color': (room === 'bano' ? (bano?.swatch || '#dcd4c2') : '#ece6d8'), }; return (
Tu vista
Tu configuración
{window.PERS_ROOMS.find(r => r.id === room)?.name}
{room === 'cocina' ? ( ) : room === 'salon' ? ( ) : (
VIVIENDA · {window.location.search.includes('unit=') ? new URLSearchParams(location.search).get('unit') : '—'} RENDER ESQUEMÁTICO · NO VINCULANTE
{room === 'cocina' &&
} {/* Hot tags pointing to elements */}
1 Suelo · {suelo?.name}
{room === 'cocina' && (
2 Cocina · {cocina?.name}
)} {room !== 'cocina' && room !== 'bano' && (
2 Pared · neutra cálida
)} {room === 'bano' && (
2 Revestimiento · {bano?.name}
)}
)}
); }; // ─── Salón photo scene with overlays ─── const SalonPhotoScene = ({ picks, suelo }) => { const cat = (id) => window.PERS_CATEGORIES.find(c => c.id === id); const opt = (catId, optId) => cat(catId)?.options.find(o => o.id === optId); const puertas = opt('puertas', picks.puertas); return (
Vista del salón-comedor
1
Suelo · salón y dormitorios
{suelo?.name || '—'}
2
Puerta interior
{puertas?.name || '—'}
3
Carpintería exterior
Aluminio RPT · vidrio bajo emisivo
4
Terraza · pavimento exterior
Porcelánico antideslizante C3
i

Salón-comedor · características

  • Iluminación arquitectónica
    Foseado LED perimetral integrado en techo · luminaria lineal sobre isla
    Pre-instalación de serie
  • Climatización por aerotermia
    Suelo radiante/refrescante zonificado · termostato wifi por estancia
    Calificación A · ECO
  • Carpintería exterior
    Aluminio con rotura puente térmico · vidrio doble bajo emisivo · persianas con motor opcional
    Apertura corredera · sistema minimalista
i

Salida a terraza / jardín

  • Pérgola bioclimática
    Lamas orientables · iluminación LED integrada
    Opcional · plantas bajas
  • Pavimento exterior
    Gres porcelánico antideslizante de gran formato · clase 3 · continuidad visual con interior
    Acabado piedra clara
  • Punto de agua y luz exterior
    Toma de riego, punto eléctrico estanco IP44 e iluminación arquitectónica baja
    De serie
); }; // ─── Kitchen photo scene with overlays ─── const KitchenPhotoScene = ({ picks, cocina, enc }) => { const equip = window.PERS_EQUIPAMIENTO_COCINA; const [campana, setCampana] = useState('vedo-60'); return (
Vista de la cocina
1
Frentes · Hönnun C100
{cocina?.name || '—'}
2
Encimera · MY TOP
{enc?.name || '—'}
3
Equipamiento Whirlpool
Placa · horno · microondas · campana
4
Fregadero Veravent + Stillo Hera
Acero inox · 45×40 · monomando
3

{equip.electro.title}

    {equip.electro.items.map(it => (
  • {it.name}
    {it.desc}
    REF · {it.ref}
  • ))}
{equip.electro.optionLabel}
{equip.electro.options.map(o => ( ))}
4

{equip.fregaderia.title}

    {equip.fregaderia.items.map(it => (
  • {it.name}
    {it.desc}
    REF · {it.ref}
  • ))}
); }; const RoomSwitch = ({ room, setRoom }) => (
{window.PERS_ROOMS.map(r => ( ))}
); const Presets = ({ activePreset, applyPreset }) => (
{window.PERS_PRESETS.map(p => ( ))}
); const Category = ({ cat, picks, setPick, toggleExtra, open, setOpen }) => { const isOpen = open; const isToggle = cat.type === 'toggles'; // pick label and price delta let pickLabel = ''; let delta = 0; if (!isToggle) { const sel = cat.options.find(o => o.id === picks[cat.id]); pickLabel = sel?.name || '—'; delta = sel?.price || 0; } else { const count = (picks.extras || []).length; pickLabel = count === 0 ? 'Ninguno' : `${count} seleccionado${count === 1 ? '' : 's'}`; delta = (picks.extras || []).reduce((s, id) => { const o = cat.options.find(x => x.id === id); return s + (o?.price || 0); }, 0); } return (
{isOpen && (
{!isToggle && (
{cat.options.map(opt => ( ))}
)} {isToggle && (
{cat.options.map(opt => { const on = (picks.extras || []).includes(opt.id); return (
toggleExtra(opt.id)}>
{opt.name}
{opt.desc}
+ {opt.price.toLocaleString('es-ES')} €
); })}
)}
)}
); }; const SummaryCard = ({ basePrice, picks, total }) => { const cat = (id) => window.PERS_CATEGORIES.find(c => c.id === id); const opt = (catId, optId) => cat(catId)?.options.find(o => o.id === optId); const lineFor = (catId) => { const o = opt(catId, picks[catId]); if (!o) return null; return (
{cat(catId).name} {o.name} {o.price > 0 ? `+ ${o.price.toLocaleString('es-ES')} €` : 'incl.'}
); }; const extras = (picks.extras || []).map(id => { const o = opt('extras', id); return o ? (
{o.name} + {o.price.toLocaleString('es-ES')} €
) : null; }); return (

Tu configuración

Vivienda base {basePrice.toLocaleString('es-ES')} €
{['suelo','cocina','encimera','bano','puertas'].map(lineFor)} {extras}
Total estimado {total.toLocaleString('es-ES')} €
); }; // ───── Step 2 main ───── const PersonalizationApp = () => { const params = new URLSearchParams(window.location.search); const promoId = params.get('id') || 'edificio-victoria'; const unitId = params.get('unit') || ''; const data = window.getTour(promoId); let basePrice = 0; let unitMeta = null; try { const stored = JSON.parse(sessionStorage.getItem('ksa_cfg') || 'null'); if (stored && stored.unitId === unitId) { basePrice = stored.basePrice; unitMeta = stored; } } catch (e) {} // Fallback: derive from tour data if (!basePrice && data) { const allUnits = data.building.floors.flatMap(f => f.units.map(u => ({ ...u, floor: f }))); const u = allUnits.find(x => x.id === unitId) || allUnits.find(x => x.state === 'available'); if (u) { const t = data.typologies.find(x => x.code === u.code); basePrice = t?.priceFrom || 296507; unitMeta = { unitId: u.id, code: u.code, typoName: t?.name, useful: t?.useful, dorms: t?.dorms, basePrice, floor: u.floor.id }; } } if (!basePrice) basePrice = 296507; const defaultPicks = window.PERS_PRESETS[0].picks; const [picks, setPicks] = useState({ ...defaultPicks }); const [open, setOpen] = useState('suelo'); const [room, setRoom] = useState('salon'); const [activePreset, setActivePreset] = useState('esencial'); const setPick = (catId, optId) => { setPicks(p => ({ ...p, [catId]: optId })); setActivePreset(null); }; const toggleExtra = (id) => { setPicks(p => { const ex = p.extras || []; return { ...p, extras: ex.includes(id) ? ex.filter(x => x !== id) : [...ex, id] }; }); setActivePreset(null); }; const applyPreset = (preset) => { setPicks({ ...preset.picks }); setActivePreset(preset.id); }; const total = useMemo(() => { let t = basePrice; for (const c of window.PERS_CATEGORIES) { if (c.type === 'toggles') { for (const id of (picks.extras || [])) { const o = c.options.find(x => x.id === id); if (o) t += o.price; } } else { const o = c.options.find(x => x.id === picks[c.id]); if (o) t += o.price; } } return t; }, [picks, basePrice]); const acabadosTotal = total - basePrice; return (
{ if (n === 1) window.location.href = `configurador-v2.html?id=${promoId}`; }}/>
{unitMeta?.code || 'Vivienda'} · acabados
Vivienda
{unitMeta?.code || '—'} {unitMeta?.typoName || ''}
Acabados elegidos
{acabadosTotal === 0 ? 'Memoria estándar' : `+ ${acabadosTotal.toLocaleString('es-ES')} €`}
Precio total estimado
{total.toLocaleString('es-ES')} €
+ IVA · plan económico en el siguiente paso
); }; ReactDOM.createRoot(document.getElementById('root')).render();