Presupuestador puertas lacadas Gala Projectes

<!-- Contenedor -->
<div id="door-configurator-root"></div>

<!-- Tailwind (CDN, para las clases que ya usa tu TSX) -->
<script src="https://cdn.tailwindcss.com"></script>

<!-- React 18 + ReactDOM 18 (CDN) -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>

<!-- Babel standalone para transpilar TSX/TypeScript en el navegador -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<!-- Tu configurador en TSX (sin lucide-react; uso emojis/texto para iconos) -->
<script type="text/babel" data-presets="typescript,react">
  const { useState, useEffect } = React;

  const DoorConfigurator: React.FC = () => {
    const [step, setStep] = useState<number>(1);
    const [config, setConfig] = useState<any>({
      model: null,
      opening: null,
      openingSide: null,
      openingVariant: null,
      width: 82.5,
      height: 203,
      isCustomSize: false,
      fireRating: null,
      acoustic: false,
      isDouble: false,
      galce: 8,
      tapeta: 7,
      quantity: 1
    });
    const [configurations, setConfigurations] = useState<any[]>([]);
    const [currentPrice, setCurrentPrice] = useState<number>(0);

    const models = [
      { id: 'lisa', name: 'Lacada Blanca Lisa', price: 147.48, image: '🚪', description: 'Diseño minimalista y elegante' },
      { id: 'sofia', name: 'Lacada Blanca Sofía', price: 147.48 * 1.15, image: '🚪', description: '4 ranuras decorativas', increment: '+15%' }
    ];

    const openingTypes = [
      { id: 'abatible', name: 'Abatible', price: 0, icon: '🔄', description: 'Apertura tradicional con bisagras' },
      { id: 'oculta', name: 'Oculta/Enrasada', price: 215, icon: '📐', description: 'Integrada en la pared', increment: '+315€' },
      { id: 'corredera', name: 'Corredera', price: 0.6, icon: '↔️', description: 'Deslizante horizontal', increment: '+60%' }
    ];

    const fireRatings = [
      { id: null,  name: 'Sin certificación RF', price: 0 },
      { id: 'EI30', name: 'EI30 - 30 min', price: 250 },
      { id: 'EI45', name: 'EI45 - 45 min', price: 320 },
      { id: 'EI60', name: 'EI60 - 60 min', price: 420 },
      { id: 'EI90', name: 'EI90 - 90 min', price: 600 }
    ];

    const calculatePrice = () => {
      if (!config.model) return 0;
      const baseModel = models.find(m => m.id === config.model)!;
      let price = baseModel.price;

      // Dimensiones personalizadas
      if (config.isCustomSize) {
        const heightMultiplier: Record<number, number> = { 211: 1.12, 220: 1.72, 230: 1.82, 240: 1.98, 260: 2.10 };
        const widthMultiplier: Record<number, number>  = { 92.5: 1.12, 102: 1.20 };
        const hm = heightMultiplier[config.height] || 1;
        const wm = widthMultiplier[config.width]   || 1;
        price *= Math.max(hm, wm);
      }

      // Tipo de apertura
      if (config.opening === 'oculta')      price += 215;
      else if (config.opening === 'corredera') price *= 1.6;

      // Certificaciones
      if (config.fireRating) {
        const firePrice = fireRatings.find(f => f.id === config.fireRating)?.price || 0;
        price += firePrice;
      } else if (config.acoustic) {
        price += 250;
      }

      // Puerta doble
      if (config.isDouble) price *= 2;

      // Galce
      const galcePrice: Record<number, number> = { 11: 2.6, 12: 5.2, 13: 7.8 };
      price += galcePrice[config.galce] || 0;

      // Tapeta
      const tapetaPrice: Record<number, number> = { 8: 8.57, 9: 12 };
      price += tapetaPrice[config.tapeta] || 0;

      return price * config.quantity;
    };

    useEffect(() => { setCurrentPrice(calculatePrice()); }, [config]);

    const validateDimensions = () => {
      const surface = (config.width / 100) * (config.height / 100);
      const hasRFOrAcoustic = config.fireRating || config.acoustic;

      if (hasRFOrAcoustic) {
        if (surface > 2.03) return false;
        if (config.width > 92.5 || config.height > 220) return false;
      }
      if (config.opening === 'oculta' && config.fireRating && !['EI30', 'EI45'].includes(config.fireRating)) {
        return false;
      }
      return true;
    };

    const addConfiguration = () => {
      if (validateDimensions()) {
        const newConfig = { ...config, id: Date.now(), price: currentPrice };
        setConfigurations([...configurations, newConfig]);
        setConfig({
          model: null, opening: null, openingSide: null, openingVariant: null,
          width: 82.5, height: 203, isCustomSize: false,
          fireRating: null, acoustic: false, isDouble: false,
          galce: 8, tapeta: 7, quantity: 1
        });
        setStep(1);
      }
    };

    const totalPrice = configurations.reduce((sum, conf) => sum + conf.price, 0);

    const StepIndicator = () => (
      <div className="mb-8">
        <div className="flex justify-between items-center">
          {[1,2,3,4,5,6].map((s) => (
            <div key={s} className={`flex items-center ${s < 6 ? 'flex-1' : ''}`}>
              <div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold
                ${step >= s ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600'}`}>
                {step > s ? '✓' : s}
              </div>
              {s < 6 && <div className={`flex-1 h-1 mx-2 ${step > s ? 'bg-blue-600' : 'bg-gray-200'}`} />}
            </div>
          ))}
        </div>
        <div className="flex justify-between mt-2 text-xs text-gray-600">
          <span>Modelo</span><span>Apertura</span><span>Medidas</span><span>Extras</span><span>Install.</span><span>Resumen</span>
        </div>
      </div>
    );

    const PricePreview = () => (
      <div className="bg-gradient-to-r from-blue-50 to-blue-100 p-4 rounded-lg border-l-4 border-blue-500">
        <div className="flex justify-between items-center">
          <span className="text-sm font-medium text-gray-700">Precio actual:</span>
          <span className="text-xl font-bold text-blue-600">{currentPrice.toFixed(2)} €</span>
        </div>
        {config.quantity > 1 && (
          <div className="text-xs text-gray-600 mt-1">
            {(currentPrice / config.quantity).toFixed(2)} € × {config.quantity} unidades
          </div>
        )}
      </div>
    );

    const renderStep = () => {
      switch(step) {
        case 1:
          return (
            <div className="space-y-6">
              <h2 className="text-2xl font-bold text-gray-800">Elige tu modelo de puerta</h2>
              <div className="grid md:grid-cols-2 gap-4">
                {models.map(model => (
                  <div key={model.id} 
                    className={`p-6 border-2 rounded-lg cursor-pointer transition-all hover:shadow-lg
                      ${config.model === model.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'}`}
                    onClick={() => setConfig({...config, model: model.id})}>
                    <div className="text-center">
                      <div className="text-4xl mb-3">{model.image}</div>
                      <h3 className="font-semibold text-lg mb-2">{model.name}</h3>
                      <p className="text-gray-600 text-sm mb-3">{model.description}</p>
                      <div className="flex justify-between items-center">
                        <span className="text-lg font-bold text-blue-600">{model.price.toFixed(2)} €</span>
                        {model.increment && <span className="text-xs bg-orange-100 text-orange-600 px-2 py-1 rounded">{model.increment}</span>}
                      </div>
                    </div>
                    {config.model === model.id && <div className="text-blue-600 mx-auto mt-3 text-center text-lg">✓</div>}
                  </div>
                ))}
              </div>
            </div>
          );
        case 2:
          return (
            <div className="space-y-6">
              <h2 className="text-2xl font-bold text-gray-800">Tipo de apertura</h2>
              <div className="grid md:grid-cols-3 gap-4">
                {openingTypes.map(type => (
                  <div key={type.id}
                    className={`p-6 border-2 rounded-lg cursor-pointer transition-all hover:shadow-lg
                      ${config.opening === type.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'}`}
                    onClick={() => setConfig({...config, opening: type.id})}>
                    <div className="text-center">
                      <div className="text-3xl mb-3">{type.icon}</div>
                      <h3 className="font-semibold mb-2">{type.name}</h3>
                      <p className="text-gray-600 text-xs mb-3">{type.description}</p>
                      {type.increment && <span className="text-xs bg-green-100 text-green-600 px-2 py-1 rounded">{type.increment}</span>}
                    </div>
                    {config.opening === type.id && <div className="text-blue-600 mx-auto mt-3 text-center text-base">✓</div>}
                  </div>
                ))}
              </div>

              {config.opening && (
                <div className="mt-6 p-4 bg-gray-50 rounded-lg">
                  <h4 className="font-medium mb-3">Configuración adicional</h4>

                  <div className="space-y-4">
                    <div>
                      <label className="block text-sm font-medium mb-2">Sentido de apertura</label>
                      <div className="flex gap-3">
                        {['Derecha', 'Izquierda'].map(side => (
                          <button key={side}
                            className={`px-4 py-2 rounded-md text-sm font-medium transition-colors
                              ${config.openingSide === side ? 'bg-blue-600 text-white' : 'bg-white border border-gray-300 hover:bg-gray-50'}`}
                            onClick={() => setConfig({...config, openingSide: side})}>
                            {side}
                          </button>
                        ))}
                      </div>
                    </div>

                    {config.opening === 'oculta' && (
                      <div>
                        <label className="block text-sm font-medium mb-2">Variante de instalación</label>
                        <select className="w-full p-2 border border-gray-300 rounded-md"
                          value={config.openingVariant || ''}
                          onChange={(e) => setConfig({...config, openingVariant: e.target.value})}>
                          <option value="">Seleccionar...</option>
                          <option value="revoque">Pared de revoque</option>
                          <option value="panel">Panel</option>
                          <option value="pladur">Pared de pladur</option>
                        </select>
                      </div>
                    )}

                    {config.opening === 'corredera' && (
                      <div>
                        <label className="block text-sm font-medium mb-2">Posición</label>
                        <div className="flex gap-3">
                          {['Interior del tabique', 'Exterior del tabique'].map(pos => (
                            <button key={pos}
                              className={`px-4 py-2 rounded-md text-sm font-medium transition-colors
                                ${config.openingVariant === pos ? 'bg-blue-600 text-white' : 'bg-white border border-gray-300 hover:bg-gray-50'}`}
                              onClick={() => setConfig({...config, openingVariant: pos})}>
                              {pos}
                            </button>
                          ))}
                        </div>
                      </div>
                    )}
                  </div>
                </div>
              )}
            </div>
          );
        case 3:
          return (
            <div className="space-y-6">
              <h2 className="text-2xl font-bold text-gray-800">Dimensiones</h2>

              <div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500">
                <div className="flex items-center gap-2 mb-2">
                  <span className="text-blue-600">ℹ️</span>
                  <span className="font-medium text-blue-800">Medidas estándar</span>
                </div>
                <p className="text-sm text-blue-700">82,5 × 203 cm incluido en el precio base</p>
              </div>

              <div className="space-y-4">
                <div>
                  <label className="flex items-center gap-2 mb-4">
                    <input type="checkbox" 
                      checked={config.isCustomSize}
                      onChange={(e) => setConfig({...config, isCustomSize: e.target.checked})}
                      className="w-4 h-4 text-blue-600" />
                    <span className="font-medium">Medidas personalizadas</span>
                  </label>
                </div>

                {config.isCustomSize && (
                  <div className="grid md:grid-cols-2 gap-6 p-4 bg-gray-50 rounded-lg">
                    <div>
                      <label className="block text-sm font-medium mb-2">Ancho (cm)</label>
                      <select className="w-full p-2 border border-gray-300 rounded-md"
                        value={config.width}
                        onChange={(e) => setConfig({...config, width: parseFloat(e.target.value)})}>
                        <option value={82.5}>82,5 cm (estándar)</option>
                        <option value={92.5}>92,5 cm (+12%)</option>
                        <option value={102}>102 cm (+20%)</option>
                      </select>
                    </div>

                    <div>
                      <label className="block text-sm font-medium mb-2">Alto (cm)</label>
                      <select className="w-full p-2 border border-gray-300 rounded-md"
                        value={config.height}
                        onChange={(e) => setConfig({...config, height: parseFloat(e.target.value)})}>
                        <option value={203}>203 cm (estándar)</option>
                        <option value={211}>211 cm (+12%)</option>
                        <option value={220}>220 cm (+72%)</option>
                        <option value={230}>230 cm (+82%)</option>
                        <option value={240}>240 cm (+98%)</option>
                        <option value={260}>260 cm (+110%)</option>
                      </select>
                    </div>
                  </div>
                )}

                {!validateDimensions() && (
                  <div className="bg-red-50 p-4 rounded-lg border-l-4 border-red-500">
                    <p className="text-red-700 text-sm font-medium">⚠️ Restricción técnica</p>
                    <p className="text-red-600 text-sm mt-1">
                      Las puertas RF y acústicas solo están disponibles hasta 92,5×220 cm y máximo 2,03 m² de superficie.
                      {config.opening === 'oculta' && config.fireRating && !['EI30', 'EI45'].includes(config.fireRating) && 
                        ' Las puertas enrasadas con RF solo admiten certificación hasta EI45.'}
                    </p>
                  </div>
                )}
              </div>
            </div>
          );
        case 4:
          return (
            <div className="space-y-6">
              <h2 className="text-2xl font-bold text-gray-800">Especificaciones técnicas</h2>

              <div className="space-y-6">
                <div>
                  <h3 className="font-semibold mb-3">Certificación cortafuegos (RF)</h3>
                  <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-3">
                    {fireRatings.map(rating => (
                      <button key={rating.id ?? 'none'}
                        className={`p-3 text-left border-2 rounded-lg transition-all
                          ${config.fireRating === rating.id ? 'border-red-500 bg-red-50' : 'border-gray-200 hover:border-gray-300'}`}
                        onClick={() => setConfig({...config, fireRating: rating.id, acoustic: rating.id ? true : config.acoustic})}>
                        <div className="font-medium text-sm">{rating.name}</div>
                        {rating.price > 0 && <div className="text-red-600 text-xs">+{rating.price}€</div>}
                        {rating.id && <div className="text-xs text-gray-500 mt-1">Incluye aislamiento acústico</div>}
                      </button>
                    ))}
                  </div>
                </div>

                {!config.fireRating && (
                  <div>
                    <label className="flex items-center gap-2">
                      <input type="checkbox"
                        checked={config.acoustic}
                        onChange={(e) => setConfig({...config, acoustic: e.target.checked})}
                        className="w-4 h-4 text-blue-600" />
                      <span className="font-medium">Aislamiento acústico 32 dB (+250€)</span>
                    </label>
                    <p className="text-sm text-gray-600 ml-6">Ideal para oficinas y espacios que requieren privacidad sonora</p>
                  </div>
                )}

                <div>
                  <label className="flex items-center gap-2">
                    <input type="checkbox"
                      checked={config.isDouble}
                      onChange={(e) => setConfig({...config, isDouble: e.target.checked})}
                      className="w-4 h-4 text-blue-600" />
                    <span className="font-medium">Puerta doble (×2 precio)</span>
                  </label>
                  <p className="text-sm text-gray-600 ml-6">Para pasos amplios o entradas principales</p>
                </div>
              </div>
            </div>
          );
        case 5:
          return (
            <div className="space-y-6">
              <h2 className="text-2xl font-bold text-gray-800">Instalación y cantidad</h2>

              <div className="grid md:grid-cols-2 gap-6">
                <div>
                  <label className="block text-sm font-medium mb-2">Galce según grosor del tabique</label>
                  <select className="w-full p-2 border border-gray-300 rounded-md"
                    value={config.galce}
                    onChange={(e) => setConfig({...config, galce: parseFloat(e.target.value)})}>
                    <option value={8}>8 cm (incluido)</option>
                    <option value={9}>9 cm (incluido)</option>
                    <option value={10}>10 cm (incluido)</option>
                    <option value={11}>11 cm (+2,6€)</option>
                    <option value={12}>12 cm (+5,2€)</option>
                    <option value={13}>13 cm (+7,8€)</option>
                  </select>
                </div>

                <div>
                  <label className="block text-sm font-medium mb-2">Tapeta</label>
                  <select className="w-full p-2 border border-gray-300 rounded-md"
                    value={config.tapeta}
                    onChange={(e) => setConfig({...config, tapeta: parseFloat(e.target.value)})}>
                    <option value={7}>7 cm (incluido)</option>
                    <option value={8}>8 cm (+8,57€)</option>
                    <option value={9}>9 cm (+12€)</option>
                  </select>
                </div>
              </div>

              <div>
                <label className="block text-sm font-medium mb-2">Cantidad</label>
                <div className="flex items-center gap-3">
                  <button className="w-10 h-10 border border-gray-300 rounded-md flex items-center justify-center hover:bg-gray-50"
                    onClick={() => setConfig({...config, quantity: Math.max(1, config.quantity - 1)})}>−</button>
                  <input type="number" min="1" max="99"
                    className="w-20 p-2 text-center border border-gray-300 rounded-md"
                    value={config.quantity}
                    onChange={(e) => setConfig({...config, quantity: parseInt(e.target.value) || 1})} />
                  <button className="w-10 h-10 border border-gray-300 rounded-md flex items-center justify-center hover:bg-gray-50"
                    onClick={() => setConfig({...config, quantity: config.quantity + 1})}>+</button>
                  <span className="text-sm text-gray-600 ml-2">unidades</span>
                </div>
              </div>
            </div>
          );
        case 6:
          return (
            <div className="space-y-6">
              <h2 className="text-2xl font-bold text-gray-800">Resumen del presupuesto</h2>

              {configurations.length > 0 && (
                <div className="space-y-4">
                  <h3 className="font-semibold text-lg">Configuraciones añadidas:</h3>
                  {configurations.map((conf, idx) => (
                    <div key={conf.id} className="bg-white p-4 border border-gray-200 rounded-lg shadow-sm">
                      <div className="flex justify-between items-start">
                        <div className="flex-1">
                          <h4 className="font-medium text-gray-800">Puerta #{idx + 1}</h4>
                          <div className="text-sm text-gray-600 mt-2 space-y-1">
                            <p>• {models.find(m => m.id === conf.model)?.name}</p>
                            <p>• {openingTypes.find(t => t.id === conf.opening)?.name}</p>
                            <p>• {conf.width} × {conf.height} cm</p>
                            {conf.fireRating && <p>• Certificación {conf.fireRating}</p>}
                            {conf.acoustic && !conf.fireRating && <p>• Aislamiento acústico</p>}
                            <p>• Cantidad: {conf.quantity}</p>
                          </div>
                        </div>
                        <div className="text-right">
                          <div className="text-lg font-bold text-blue-600">{conf.price.toFixed(2)} €</div>
                          <button className="text-xs text-gray-500 hover:text-blue-600 flex items-center gap-1 mt-2" title="Editar (no implementado)">
                            ✎ Editar
                          </button>
                        </div>
                      </div>
                    </div>
                  ))}

                  <div className="border-t pt-4">
                    <div className="flex justify-between items-center text-xl font-bold">
                      <span>Total estimado:</span>
                      <span className="text-blue-600">{totalPrice.toFixed(2)} €</span>
                    </div>
                    <p className="text-sm text-gray-600 mt-1">*IVA no incluido</p>
                  </div>
                </div>
              )}

              <div className="bg-yellow-50 p-4 rounded-lg border-l-4 border-yellow-400">
                <h4 className="font-medium text-yellow-800 mb-2">Configuración actual</h4>
                {config.model ? (
                  <div className="text-sm text-yellow-700 space-y-1">
                    <p>• {models.find(m => m.id === config.model)?.name}</p>
                    {config.opening && <p>• {openingTypes.find(t => t.id === config.opening)?.name}</p>}
                    <p>• Precio: {currentPrice.toFixed(2)} €</p>
                  </div>
                ) : (
                  <p className="text-sm text-yellow-700">No hay configuración activa</p>
                )}
              </div>

              <div className="flex gap-3">
                <button 
                  onClick={addConfiguration}
                  disabled={!config.model || !validateDimensions()}
                  className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-300 disabled:cursor-not-allowed">
                  + Añadir configuración
                </button>

                {configurations.length > 0 && (
                  <>
                    <button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" title="Exportar PDF (pendiente)">
                      ⬇ Exportar PDF
                    </button>
                    <button className="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700" title="Enviar email (pendiente)">
                      ✉ Enviar por email
                    </button>
                  </>
                )}
              </div>
            </div>
          );
        default:
          return null;
      }
    };

    return (
      <div className="max-w-6xl mx-auto p-6 bg-white min-h-screen">
        <header className="text-center mb-8">
          <h1 className="text-3xl font-bold text-gray-800 mb-2">Configurador de Puertas</h1>
          <p className="text-gray-600">Diseña tu puerta perfecta y obtén un presupuesto personalizado</p>
        </header>

        <div className="lg:grid lg:grid-cols-4 lg:gap-8">
          <div className="lg:col-span-3">
            <StepIndicator />
            <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
              {renderStep()}
            </div>

            <div className="flex justify-between mt-6">
              <button 
                onClick={() => setStep(Math.max(1, step - 1))}
                disabled={step === 1}
                className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
                ◀ Anterior
              </button>

              <button 
                onClick={() => setStep(Math.min(6, step + 1))}
                disabled={step === 6 || (step === 1 && !config.model) || (step === 2 && !config.opening) || (step === 3 && !validateDimensions())}
                className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed">
                Siguiente ▶
              </button>
            </div>
          </div>

          <div className="mt-6 lg:mt-0">
            <div className="sticky top-6 space-y-4">
              <PricePreview />

              {configurations.length > 0 && (
                <div className="bg-gray-50 p-4 rounded-lg">
                  <h4 className="font-medium text-gray-800 mb-2">Puertas configuradas</h4>
                  <div className="text-sm text-gray-600">
                    <p>{configurations.length} configuración{configurations.length > 1 ? 'es' : ''}</p>
                    <p className="font-medium text-blue-600">Total: {totalPrice.toFixed(2)} €</p>
                  </div>
                </div>
              )}
            </div>
          </div>
        </div>
      </div>
    );
  };

  const root = ReactDOM.createRoot(document.getElementById('door-configurator-root')!);
  root.render(<DoorConfigurator />);
</script>

Uso de cookies

Este sitio web utiliza cookies para mejorar la experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las cookies y de nuestra política de cookies. ACEPTAR

Aviso de cookies