src/CompanyGroupBundle/Resources/views/pages/quotes/request_quote.html.twig line 1

Open in your IDE?
  1. {% include '@Application/inc/central_header.html.twig' %}
  2. {% set breakdown = breakdown is defined ? breakdown : {} %}
  3. {% set prefill   = prefill   is defined ? prefill   : {} %}
  4. <style>
  5. .qr-shell {
  6.     min-height: 100vh;
  7.     background: linear-gradient(135deg,#f4f8ff 0%,#edf3fb 100%);
  8.     padding: 40px 0 60px;
  9. }
  10. .qr-card {
  11.     max-width: 820px;
  12.     margin: 0 auto;
  13.     background: #fff;
  14.     border-radius: 20px;
  15.     box-shadow: 0 12px 50px rgba(17,42,84,0.10);
  16.     overflow: hidden;
  17. }
  18. .qr-card__header {
  19.     background: linear-gradient(135deg,#1d5b9e,#0f3565);
  20.     padding: 32px 40px 28px;
  21.     color: #fff;
  22. }
  23. .qr-card__header h1 { font-size: 1.5rem; font-weight: 700; margin: 0 0 4px; }
  24. .qr-card__header p  { opacity: 0.75; margin: 0; font-size: 0.92rem; }
  25. .qr-card__body { padding: 36px 40px; }
  26. .qr-section-title {
  27.     font-size: 0.72rem;
  28.     font-weight: 700;
  29.     text-transform: uppercase;
  30.     letter-spacing: 0.1em;
  31.     color: #7a8ba8;
  32.     margin: 0 0 14px;
  33. }
  34. .qr-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 20px; }
  35. .qr-row--3 { grid-template-columns: 1fr 1fr 1fr; }
  36. .qr-row--1 { grid-template-columns: 1fr; }
  37. .qr-field label {
  38.     display: block;
  39.     font-size: 0.82rem;
  40.     font-weight: 600;
  41.     color: #2c3e50;
  42.     margin-bottom: 5px;
  43. }
  44. .qr-field input, .qr-field select, .qr-field textarea {
  45.     width: 100%;
  46.     border: 1.5px solid #dce4ef;
  47.     border-radius: 9px;
  48.     padding: 9px 13px;
  49.     font-size: 0.92rem;
  50.     background: #fafbfd;
  51.     transition: border-color 0.2s;
  52.     box-sizing: border-box;
  53. }
  54. .qr-field input:focus, .qr-field select:focus, .qr-field textarea:focus {
  55.     border-color: #1d5b9e;
  56.     outline: none;
  57.     background: #fff;
  58. }
  59. .qr-field--counter { position: relative; }
  60. .qr-field--counter input { padding-right: 50px; }
  61. .qr-field--counter .qr-price-hint {
  62.     position: absolute;
  63.     right: 12px; top: 50%; transform: translateY(-50%);
  64.     font-size: 0.75rem;
  65.     color: #7a8ba8;
  66.     pointer-events: none;
  67. }
  68. .qr-divider { height: 1px; background: #eef1f6; margin: 28px 0; }
  69. .qr-price-box {
  70.     background: linear-gradient(135deg,#f4f8ff,#edf3fb);
  71.     border: 1.5px solid #dce4ef;
  72.     border-radius: 14px;
  73.     padding: 22px 26px;
  74.     margin-bottom: 24px;
  75. }
  76. .qr-price-box__label { font-size: 0.8rem; color: #7a8ba8; margin-bottom: 4px; }
  77. .qr-price-box__total { font-size: 2rem; font-weight: 800; color: #1d5b9e; }
  78. .qr-price-box__sub   { font-size: 0.82rem; color: #7a8ba8; margin-top: 2px; }
  79. .qr-price-box__breakdown { margin-top: 12px; font-size: 0.82rem; color: #4a6080; }
  80. .qr-price-box__breakdown .row-item {
  81.     display: flex; justify-content: space-between;
  82.     padding: 3px 0; border-bottom: 1px dashed #e0e8f0;
  83. }
  84. .qr-price-box__breakdown .row-item:last-child { border: 0; }
  85. .qr-btn-group { display: flex; gap: 14px; }
  86. .qr-btn {
  87.     padding: 12px 28px;
  88.     border-radius: 10px;
  89.     font-size: 0.95rem;
  90.     font-weight: 700;
  91.     border: none;
  92.     cursor: pointer;
  93.     transition: transform 0.1s, box-shadow 0.1s;
  94. }
  95. .qr-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0,0,0,0.12); }
  96. .qr-btn--primary { background: #1d5b9e; color: #fff; }
  97. .qr-btn--secondary { background: #f0f4fa; color: #1d5b9e; border: 1.5px solid #d0dce8; }
  98. .qr-plan-badge {
  99.     display: inline-flex; align-items: center; gap: 6px;
  100.     background: #e8f0fb; color: #1d5b9e;
  101.     border-radius: 20px; padding: 4px 14px;
  102.     font-size: 0.82rem; font-weight: 700; margin-bottom: 22px;
  103. }
  104. </style>
  105. <div class="qr-shell">
  106.     <div class="qr-card">
  107.         <div class="qr-card__header">
  108.             <h1>{{ page_title|default('Request a Quote') }}</h1>
  109.             <p>Fill in the details below and our team will prepare your custom proposal.</p>
  110.         </div>
  111.         <div class="qr-card__body">
  112.             {% for type, messages in app.flashes %}
  113.                 {% for msg in messages %}
  114.                     <div class="alert alert-{{ type == 'error' ? 'danger' : type }} mb-3">{{ msg }}</div>
  115.                 {% endfor %}
  116.             {% endfor %}
  117.             <span class="qr-plan-badge">
  118.                 <i class="ft-package"></i>
  119.                 {{ prefill.plan_type|default('team')|title }} Plan
  120.             </span>
  121.             <form method="POST" action="{{ path('quote_request') }}" id="quoteForm">
  122.                 <input type="hidden" name="plan_type" value="{{ prefill.plan_type|default('team') }}">
  123.                 {# ── Contact Information ── #}
  124.                 <div class="qr-section-title">Contact Information</div>
  125.                 <div class="qr-row">
  126.                     <div class="qr-field">
  127.                         <label>Email Address *</label>
  128.                         <input type="email" name="customer_email"
  129.                                value="{{ prefill.customer_email|default('') }}"
  130.                                required placeholder="you@company.com">
  131.                     </div>
  132.                     <div class="qr-field">
  133.                         <label>Full Name</label>
  134.                         <input type="text" name="customer_name"
  135.                                value="{{ prefill.customer_name|default('') }}"
  136.                                placeholder="Your name">
  137.                     </div>
  138.                 </div>
  139.                 <div class="qr-row">
  140.                     <div class="qr-field">
  141.                         <label>Phone</label>
  142.                         <input type="text" name="customer_phone"
  143.                                value="{{ prefill.customer_phone|default('') }}"
  144.                                placeholder="+31 6 12345678">
  145.                     </div>
  146.                     <div class="qr-field">
  147.                         <label>Company Name</label>
  148.                         <input type="text" name="company_name"
  149.                                value="{{ prefill.company_name|default('') }}"
  150.                                placeholder="Your company">
  151.                     </div>
  152.                 </div>
  153.                 <div class="qr-row">
  154.                     <div class="qr-field">
  155.                         <label>Company Address</label>
  156.                         <input type="text" name="company_address"
  157.                                value="{{ prefill.company_address|default('') }}"
  158.                                placeholder="Street, City, Postal code">
  159.                     </div>
  160.                     <div class="qr-field">
  161.                         <label>Country</label>
  162.                         <input type="text" name="country" list="country-list"
  163.                                value="{{ prefill.country|default('') }}"
  164.                                placeholder="e.g. Netherlands">
  165.                         <datalist id="country-list">
  166.                             <option value="Netherlands"><option value="Germany"><option value="France">
  167.                             <option value="Belgium"><option value="United Kingdom"><option value="Sweden">
  168.                             <option value="Norway"><option value="Denmark"><option value="Finland">
  169.                             <option value="Spain"><option value="Italy"><option value="Portugal">
  170.                             <option value="Poland"><option value="Austria"><option value="Switzerland">
  171.                             <option value="United States"><option value="Canada"><option value="Australia">
  172.                             <option value="Singapore"><option value="India"><option value="Bangladesh">
  173.                             <option value="United Arab Emirates"><option value="South Africa">
  174.                         </datalist>
  175.                     </div>
  176.                 </div>
  177.                 <div class="qr-divider"></div>
  178.                 {# ── User Seats (both plans) ── #}
  179.                 <div class="qr-section-title">Team Size</div>
  180.                 <div class="qr-row qr-row--3">
  181.                     <div class="qr-field qr-field--counter">
  182.                         <label>Normal Users</label>
  183.                         <input type="number" name="normal_users" id="normalUsers" min="0"
  184.                                value="{{ prefill.normal_users|default(prefill.plan_type|default('team') == 'enterprise' ? 0 : 1) }}"
  185.                                class="seat-counter">
  186.                         {% if prefill.plan_type|default('team') == 'team' %}
  187.                             <span class="qr-price-hint">€8/mo</span>
  188.                         {% endif %}
  189.                     </div>
  190.                     <div class="qr-field qr-field--counter">
  191.                         <label>Admin Users</label>
  192.                         <input type="number" name="admin_users" id="adminUsers" min="0"
  193.                                value="{{ prefill.admin_users|default(0) }}"
  194.                                class="seat-counter">
  195.                         {% if prefill.plan_type|default('team') == 'team' %}
  196.                             <span class="qr-price-hint">€20/mo</span>
  197.                         {% endif %}
  198.                     </div>
  199.                     <div class="qr-field qr-field--counter">
  200.                         <label>AI-enabled Users</label>
  201.                         <input type="number" name="ml_users" id="mlUsers" min="0"
  202.                                value="{{ prefill.ml_users|default(0) }}"
  203.                                class="seat-counter">
  204.                         {% if prefill.plan_type|default('team') == 'team' %}
  205.                             <span class="qr-price-hint">€25/mo</span>
  206.                         {% endif %}
  207.                     </div>
  208.                 </div>
  209.                 {# ── Billing & Payment ── #}
  210.                 <div class="qr-section-title" style="margin-top:10px;">Billing Preferences</div>
  211.                 {% if prefill.plan_type|default('team') == 'team' %}
  212.                 <div class="qr-row">
  213.                     <div class="qr-field">
  214.                         <label>Billing Cycle</label>
  215.                         <select name="billing_cycle" id="billingCycle">
  216.                             <option value="monthly" {{ prefill.billing_cycle|default('monthly') == 'monthly' ? 'selected' : '' }}>Monthly</option>
  217.                             <option value="yearly"  {{ prefill.billing_cycle|default('monthly') == 'yearly'  ? 'selected' : '' }}>Yearly (save 20%)</option>
  218.                         </select>
  219.                     </div>
  220.                     <div class="qr-field">
  221.                         <label>Payment Method</label>
  222.                         <select name="payment_type">
  223.                             <option value="automatic" {{ prefill.payment_type|default('automatic') == 'automatic' ? 'selected' : '' }}>Card / Automatic</option>
  224.                             <option value="manual"    {{ prefill.payment_type|default('automatic') == 'manual'    ? 'selected' : '' }}>Bank Transfer</option>
  225.                         </select>
  226.                     </div>
  227.                 </div>
  228.                 {# ── Live Price Box ── #}
  229.                 <div class="qr-price-box" id="priceBox">
  230.                     <div class="qr-price-box__label">Estimated Price</div>
  231.                     <div class="qr-price-box__total" id="priceTotal">
  232.                         €{{ breakdown.base_amount|default(0)|number_format(2) }}
  233.                         <span style="font-size:1rem;font-weight:400;color:#7a8ba8;">
  234.                             / {{ prefill.billing_cycle|default('monthly') }}
  235.                         </span>
  236.                     </div>
  237.                     <div class="qr-price-box__sub" id="priceSub"></div>
  238.                     <div class="qr-price-box__breakdown" id="priceBreakdown">
  239.                         {% if breakdown %}
  240.                             {% if breakdown.subtotal_base > 0 %}
  241.                             <div class="row-item"><span>Package base</span><span>€{{ breakdown.subtotal_base|number_format(2) }}</span></div>
  242.                             {% endif %}
  243.                             {% if breakdown.extra_users > 0 %}
  244.                             <div class="row-item"><span>+{{ breakdown.extra_users }} extra users × €{{ breakdown.price_normal_monthly }}</span><span>€{{ breakdown.subtotal_normal|number_format(2) }}</span></div>
  245.                             {% endif %}
  246.                             {% if breakdown.extra_admins > 0 %}
  247.                             <div class="row-item"><span>+{{ breakdown.extra_admins }} extra admins × €{{ breakdown.price_admin_monthly }}</span><span>€{{ breakdown.subtotal_admin|number_format(2) }}</span></div>
  248.                             {% endif %}
  249.                             {% if breakdown.ml_users > 0 %}
  250.                             <div class="row-item"><span>AI-enabled users ({{ breakdown.ml_users }} × €{{ breakdown.price_ml_monthly }})</span><span>€{{ breakdown.subtotal_ml|number_format(2) }}</span></div>
  251.                             {% endif %}
  252.                         {% endif %}
  253.                     </div>
  254.                 </div>
  255.                 {% else %}
  256.                 {# Enterprise — payment preference only, no pricing shown #}
  257.                 <input type="hidden" name="billing_cycle" value="monthly">
  258.                 <div class="qr-row">
  259.                     <div class="qr-field">
  260.                         <label>Payment Method Preference</label>
  261.                         <select name="payment_type">
  262.                             <option value="manual">Bank Transfer</option>
  263.                             <option value="automatic">Card / Automatic</option>
  264.                         </select>
  265.                     </div>
  266.                 </div>
  267.                 <div style="background:#f4f8ff;border:1.5px solid #d0dce8;border-radius:10px;padding:13px 16px;font-size:0.85rem;color:#4a6080;margin-top:6px;">
  268.                     Our team will review your team size and prepare a custom price. You will receive the quote by email.
  269.                 </div>
  270.                 {% endif %}
  271.                 <div class="qr-divider"></div>
  272.                 {# ── Promo Code ── #}
  273.                 <div class="qr-section-title">Promo Code</div>
  274.                 <input type="hidden" name="promo_code_id" id="promoCodeId" value="">
  275.                 <div class="qr-row qr-row--1" style="margin-bottom:20px;">
  276.                     <div class="qr-field">
  277.                         <label>Have a promo code? <span style="font-weight:400;color:#7a8ba8;">(optional)</span></label>
  278.                         <div style="display:flex;gap:10px;">
  279.                             <input type="text" id="promoCodeInput" placeholder="Enter code…" style="flex:1;">
  280.                             <button type="button" id="promoApplyBtn"
  281.                                     style="padding:9px 18px;border-radius:9px;border:1.5px solid #1d5b9e;background:#fff;color:#1d5b9e;font-weight:700;cursor:pointer;white-space:nowrap;">
  282.                                 Apply
  283.                             </button>
  284.                         </div>
  285.                         <div id="promoResult" style="margin-top:6px;font-size:0.82rem;"></div>
  286.                     </div>
  287.                 </div>
  288.                 <div class="qr-divider"></div>
  289.                 <div class="qr-row qr-row--1" style="margin-bottom:24px;">
  290.                     <div class="qr-field">
  291.                         <label>Notes / Special Requirements</label>
  292.                         <textarea name="customer_notes" rows="3"
  293.                                   placeholder="Any special requirements or questions…">{{ prefill.customer_notes|default('') }}</textarea>
  294.                     </div>
  295.                 </div>
  296.                 <div class="qr-btn-group">
  297.                     <button type="submit" class="qr-btn qr-btn--primary">
  298.                         <i class="ft-send"></i> Submit Quote Request
  299.                     </button>
  300.                     <a href="/" class="qr-btn qr-btn--secondary">Cancel</a>
  301.                 </div>
  302.             </form>
  303.         </div>
  304.     </div>
  305. </div>
  306. <script>
  307. (function() {
  308.     var promoLookUrl = '{{ path('quote_promo_lookup') }}';
  309.     var promoDiscount = 0;
  310.     // Promo code lookup (works for both Team and Enterprise)
  311.     var promoBtn = document.getElementById('promoApplyBtn');
  312.     if (promoBtn) {
  313.         promoBtn.addEventListener('click', function() {
  314.             var code = document.getElementById('promoCodeInput').value.trim();
  315.             if (!code) return;
  316.             var resultEl = document.getElementById('promoResult');
  317.             resultEl.innerHTML = '<span style="color:#7a8ba8;">Checking…</span>';
  318.             fetch(promoLookUrl + '?code=' + encodeURIComponent(code))
  319.                 .then(function(r){ return r.json(); })
  320.                 .then(function(data) {
  321.                     if (!data.success) {
  322.                         resultEl.innerHTML = '<span style="color:#e74c3c;">✗ ' + data.message + '</span>';
  323.                         document.getElementById('promoCodeId').value = '';
  324.                         promoDiscount = 0;
  325.                         if (typeof recalc === 'function') recalc();
  326.                         return;
  327.                     }
  328.                     document.getElementById('promoCodeId').value = data.id;
  329.                     resultEl.innerHTML = '<span style="color:#27ae60;">✓ ' + data.label + ' applied!</span>';
  330.                     // Store for team plan price calc
  331.                     if (data.promo_type === 1) {
  332.                         promoDiscount = data.max_discount !== null ? Math.min(data.promo_value, data.max_discount) : data.promo_value;
  333.                     } else {
  334.                         promoDiscount = data.promo_value; // pct — applied in recalc
  335.                         promoDiscount = -data.promo_value; // signal: negative = percent
  336.                     }
  337.                     if (typeof recalc === 'function') recalc();
  338.                 });
  339.         });
  340.         document.getElementById('promoCodeInput').addEventListener('keydown', function(e) {
  341.             if (e.key === 'Enter') { e.preventDefault(); promoBtn.click(); }
  342.         });
  343.     }
  344. {% if prefill.plan_type|default('team') == 'team' %}
  345.     var calcUrl      = '{{ path('quote_calculate_price') }}';
  346.     var debounce;
  347.     function recalc() {
  348.         clearTimeout(debounce);
  349.         debounce = setTimeout(function() {
  350.             var fd = new FormData();
  351.             fd.append('normal_users',  document.getElementById('normalUsers').value);
  352.             fd.append('admin_users',   document.getElementById('adminUsers').value);
  353.             fd.append('ml_users',      document.getElementById('mlUsers').value);
  354.             fd.append('billing_cycle', document.getElementById('billingCycle').value);
  355.             fd.append('plan_type',     'team');
  356.             fetch(calcUrl, { method: 'POST', body: fd })
  357.                 .then(function(r){ return r.json(); })
  358.                 .then(function(d) {
  359.                     var cycle = d.billing_cycle;
  360.                     // Apply promo discount
  361.                     var promoOff = 0;
  362.                     if (promoDiscount < 0) {
  363.                         // Percent promo
  364.                         promoOff = Math.round(d.base_amount * (-promoDiscount / 100) * 100) / 100;
  365.                     } else {
  366.                         promoOff = promoDiscount;
  367.                     }
  368.                     var total = Math.max(0, d.base_amount - promoOff);
  369.                     document.getElementById('priceTotal').innerHTML =
  370.                         '€' + total.toFixed(2) +
  371.                         '<span style="font-size:1rem;font-weight:400;color:#7a8ba8;"> / ' + cycle + '</span>';
  372.                     var sub = '';
  373.                     if (cycle === 'yearly') {
  374.                         sub = 'Save €' + d.yearly_savings.toFixed(2) + ' vs. monthly billing (20% off)';
  375.                     }
  376.                     if (promoOff > 0) {
  377.                         sub += (sub ? ' · ' : '') + 'Promo: −€' + promoOff.toFixed(2);
  378.                     }
  379.                     document.getElementById('priceSub').textContent = sub;
  380.                     var breakdown = '';
  381.                     if (d.subtotal_base > 0) {
  382.                         breakdown += '<div class="row-item"><span>Package base</span><span>€' + d.subtotal_base.toFixed(2) + '</span></div>';
  383.                     }
  384.                     if (d.extra_users > 0) {
  385.                         breakdown += '<div class="row-item"><span>+' + d.extra_users + ' extra users × €' + d.price_normal_monthly.toFixed(2) + '</span><span>€' + d.subtotal_normal.toFixed(2) + '</span></div>';
  386.                     }
  387.                     if (d.extra_admins > 0) {
  388.                         breakdown += '<div class="row-item"><span>+' + d.extra_admins + ' extra admins × €' + d.price_admin_monthly.toFixed(2) + '</span><span>€' + d.subtotal_admin.toFixed(2) + '</span></div>';
  389.                     }
  390.                     if (d.ml_users > 0) {
  391.                         breakdown += '<div class="row-item"><span>AI-enabled users (' + d.ml_users + ' × €' + d.price_ml_monthly.toFixed(2) + ')</span><span>€' + d.subtotal_ml.toFixed(2) + '</span></div>';
  392.                     }
  393.                     if (promoOff > 0) {
  394.                         breakdown += '<div class="row-item" style="color:#27ae60;"><span>Promo discount</span><span>−€' + promoOff.toFixed(2) + '</span></div>';
  395.                     }
  396.                     document.getElementById('priceBreakdown').innerHTML = breakdown;
  397.                 });
  398.         }, 350);
  399.     }
  400.     document.querySelectorAll('.seat-counter, #billingCycle').forEach(function(el){
  401.         el.addEventListener('input', recalc);
  402.         el.addEventListener('change', recalc);
  403.     });
  404.     recalc();
  405. {% endif %}
  406. })();
  407. </script>