// 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 }) => (
);
// 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 (
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 (
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.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
);
};
ReactDOM.createRoot(document.getElementById('root')).render();