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.                     {% set lockId = prefill.lock_identity|default(0) %}
  127.                     <div class="qr-field">
  128.                         <label>Email Address *{% if lockId %} <span style="font-size:11px;color:#888;">(your account)</span>{% endif %}</label>
  129.                         <input type="email" name="customer_email"
  130.                                value="{{ prefill.customer_email|default('') }}"
  131.                                required placeholder="you@company.com"
  132.                                {{ lockId ? 'readonly' : '' }}>
  133.                     </div>
  134.                     <div class="qr-field">
  135.                         <label>Full Name</label>
  136.                         <input type="text" name="customer_name"
  137.                                value="{{ prefill.customer_name|default('') }}"
  138.                                placeholder="Your name"
  139.                                {{ lockId ? 'readonly' : '' }}>
  140.                     </div>
  141.                 </div>
  142.                 <div class="qr-row">
  143.                     <div class="qr-field">
  144.                         <label>Phone</label>
  145.                         <input type="text" name="customer_phone"
  146.                                value="{{ prefill.customer_phone|default('') }}"
  147.                                placeholder="+31 6 12345678">
  148.                     </div>
  149.                     <div class="qr-field">
  150.                         <label>Company Name</label>
  151.                         <input type="text" name="company_name"
  152.                                value="{{ prefill.company_name|default('') }}"
  153.                                placeholder="Your company"
  154.                                {{ lockId and prefill.company_name|default('') != '' ? 'readonly' : '' }}>
  155.                     </div>
  156.                 </div>
  157.                 <div class="qr-row">
  158.                     <div class="qr-field">
  159.                         <label>Company Address</label>
  160.                         <input type="text" name="company_address"
  161.                                value="{{ prefill.company_address|default('') }}"
  162.                                placeholder="Street, City, Postal code">
  163.                     </div>
  164.                     <div class="qr-field">
  165.                         <label>Country</label>
  166.                         <input type="text" name="country" list="country-list"
  167.                                value="{{ prefill.country|default('') }}"
  168.                                placeholder="e.g. Netherlands">
  169.                         <datalist id="country-list">
  170.                             <option value="Netherlands"><option value="Germany"><option value="France">
  171.                             <option value="Belgium"><option value="United Kingdom"><option value="Sweden">
  172.                             <option value="Norway"><option value="Denmark"><option value="Finland">
  173.                             <option value="Spain"><option value="Italy"><option value="Portugal">
  174.                             <option value="Poland"><option value="Austria"><option value="Switzerland">
  175.                             <option value="United States"><option value="Canada"><option value="Australia">
  176.                             <option value="Singapore"><option value="India"><option value="Bangladesh">
  177.                             <option value="United Arab Emirates"><option value="South Africa">
  178.                         </datalist>
  179.                     </div>
  180.                 </div>
  181.                 <div class="qr-divider"></div>
  182.                 {# ── User Seats (both plans) ── #}
  183.                 <div class="qr-section-title">Team Size</div>
  184.                 <div class="qr-row qr-row--3">
  185.                     <div class="qr-field qr-field--counter">
  186.                         <label>Normal Users</label>
  187.                         <input type="number" name="normal_users" id="normalUsers" min="0"
  188.                                value="{{ prefill.normal_users|default(prefill.plan_type|default('team') == 'enterprise' ? 0 : 1) }}"
  189.                                class="seat-counter">
  190.                         {% if prefill.plan_type|default('team') == 'team' %}
  191.                             <span class="qr-price-hint">€8/mo</span>
  192.                         {% endif %}
  193.                     </div>
  194.                     <div class="qr-field qr-field--counter">
  195.                         <label>Admin Users</label>
  196.                         <input type="number" name="admin_users" id="adminUsers" min="0"
  197.                                value="{{ prefill.admin_users|default(0) }}"
  198.                                class="seat-counter">
  199.                         {% if prefill.plan_type|default('team') == 'team' %}
  200.                             <span class="qr-price-hint">€20/mo</span>
  201.                         {% endif %}
  202.                     </div>
  203.                     <div class="qr-field qr-field--counter">
  204.                         <label>AI-enabled Users</label>
  205.                         <input type="number" name="ml_users" id="mlUsers" min="0"
  206.                                value="{{ prefill.ml_users|default(0) }}"
  207.                                class="seat-counter">
  208.                         {% if prefill.plan_type|default('team') == 'team' %}
  209.                             <span class="qr-price-hint">€25/mo</span>
  210.                         {% endif %}
  211.                     </div>
  212.                 </div>
  213.                 {# ── Billing & Payment ── #}
  214.                 <div class="qr-section-title" style="margin-top:10px;">Billing Preferences</div>
  215.                 {% if prefill.plan_type|default('team') == 'team' %}
  216.                 <div class="qr-row">
  217.                     <div class="qr-field">
  218.                         <label>Billing Cycle</label>
  219.                         <select name="billing_cycle" id="billingCycle">
  220.                             <option value="monthly" {{ prefill.billing_cycle|default('monthly') == 'monthly' ? 'selected' : '' }}>Monthly</option>
  221.                             <option value="yearly"  {{ prefill.billing_cycle|default('monthly') == 'yearly'  ? 'selected' : '' }}>Yearly (save 20%)</option>
  222.                         </select>
  223.                     </div>
  224.                     <div class="qr-field">
  225.                         <label>Payment Method</label>
  226.                         <select name="payment_type">
  227.                             <option value="automatic" {{ prefill.payment_type|default('automatic') == 'automatic' ? 'selected' : '' }}>Card / Automatic</option>
  228.                             <option value="manual"    {{ prefill.payment_type|default('automatic') == 'manual'    ? 'selected' : '' }}>Bank Transfer</option>
  229.                         </select>
  230.                     </div>
  231.                 </div>
  232.                 {# ── Live Price Box ── #}
  233.                 <div class="qr-price-box" id="priceBox">
  234.                     <div class="qr-price-box__label">Estimated Price</div>
  235.                     <div class="qr-price-box__total" id="priceTotal">
  236.                         €{{ breakdown.base_amount|default(0)|number_format(2) }}
  237.                         <span style="font-size:1rem;font-weight:400;color:#7a8ba8;">
  238.                             / {{ prefill.billing_cycle|default('monthly') }}
  239.                         </span>
  240.                     </div>
  241.                     <div class="qr-price-box__sub" id="priceSub"></div>
  242.                     <div class="qr-price-box__breakdown" id="priceBreakdown">
  243.                         {% if breakdown %}
  244.                             {% if breakdown.subtotal_base > 0 %}
  245.                             <div class="row-item"><span>Package base</span><span>€{{ breakdown.subtotal_base|number_format(2) }}</span></div>
  246.                             {% endif %}
  247.                             {% if breakdown.extra_users > 0 %}
  248.                             <div class="row-item"><span>+{{ breakdown.extra_users }} extra users × €{{ breakdown.price_normal_monthly }}</span><span>€{{ breakdown.subtotal_normal|number_format(2) }}</span></div>
  249.                             {% endif %}
  250.                             {% if breakdown.extra_admins > 0 %}
  251.                             <div class="row-item"><span>+{{ breakdown.extra_admins }} extra admins × €{{ breakdown.price_admin_monthly }}</span><span>€{{ breakdown.subtotal_admin|number_format(2) }}</span></div>
  252.                             {% endif %}
  253.                             {% if breakdown.ml_users > 0 %}
  254.                             <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>
  255.                             {% endif %}
  256.                         {% endif %}
  257.                     </div>
  258.                 </div>
  259.                 {% else %}
  260.                 {# Enterprise — payment preference only, no pricing shown #}
  261.                 <input type="hidden" name="billing_cycle" value="monthly">
  262.                 <div class="qr-row">
  263.                     <div class="qr-field">
  264.                         <label>Payment Method Preference</label>
  265.                         <select name="payment_type">
  266.                             <option value="manual">Bank Transfer</option>
  267.                             <option value="automatic">Card / Automatic</option>
  268.                         </select>
  269.                     </div>
  270.                 </div>
  271.                 <div style="background:#f4f8ff;border:1.5px solid #d0dce8;border-radius:10px;padding:13px 16px;font-size:0.85rem;color:#4a6080;margin-top:6px;">
  272.                     Our team will review your team size and prepare a custom price. You will receive the quote by email.
  273.                 </div>
  274.                 {% endif %}
  275.                 <div class="qr-divider"></div>
  276.                 {# ── Promo Code ── #}
  277.                 <div class="qr-section-title">Promo Code</div>
  278.                 <input type="hidden" name="promo_code_id" id="promoCodeId" value="">
  279.                 <div class="qr-row qr-row--1" style="margin-bottom:20px;">
  280.                     <div class="qr-field">
  281.                         <label>Have a promo code? <span style="font-weight:400;color:#7a8ba8;">(optional)</span></label>
  282.                         <div style="display:flex;gap:10px;">
  283.                             <input type="text" id="promoCodeInput" placeholder="Enter code…" style="flex:1;">
  284.                             <button type="button" id="promoApplyBtn"
  285.                                     style="padding:9px 18px;border-radius:9px;border:1.5px solid #1d5b9e;background:#fff;color:#1d5b9e;font-weight:700;cursor:pointer;white-space:nowrap;">
  286.                                 Apply
  287.                             </button>
  288.                         </div>
  289.                         <div id="promoResult" style="margin-top:6px;font-size:0.82rem;"></div>
  290.                     </div>
  291.                 </div>
  292.                 <div class="qr-divider"></div>
  293.                 <div class="qr-row qr-row--1" style="margin-bottom:24px;">
  294.                     <div class="qr-field">
  295.                         <label>Notes / Special Requirements</label>
  296.                         <textarea name="customer_notes" rows="3"
  297.                                   placeholder="Any special requirements or questions…">{{ prefill.customer_notes|default('') }}</textarea>
  298.                     </div>
  299.                 </div>
  300.                 <div class="qr-btn-group">
  301.                     <button type="submit" class="qr-btn qr-btn--primary">
  302.                         <i class="ft-send"></i> Submit Quote Request
  303.                     </button>
  304.                     <a href="/" class="qr-btn qr-btn--secondary">Cancel</a>
  305.                 </div>
  306.             </form>
  307.         </div>
  308.     </div>
  309. </div>
  310. <script>
  311. (function() {
  312.     var promoLookUrl = '{{ path('quote_promo_lookup') }}';
  313.     var promoDiscount = 0;
  314.     // Promo code lookup (works for both Team and Enterprise)
  315.     var promoBtn = document.getElementById('promoApplyBtn');
  316.     if (promoBtn) {
  317.         promoBtn.addEventListener('click', function() {
  318.             var code = document.getElementById('promoCodeInput').value.trim();
  319.             if (!code) return;
  320.             var resultEl = document.getElementById('promoResult');
  321.             resultEl.innerHTML = '<span style="color:#7a8ba8;">Checking…</span>';
  322.             fetch(promoLookUrl + '?code=' + encodeURIComponent(code))
  323.                 .then(function(r){ return r.json(); })
  324.                 .then(function(data) {
  325.                     if (!data.success) {
  326.                         resultEl.innerHTML = '<span style="color:#e74c3c;">✗ ' + data.message + '</span>';
  327.                         document.getElementById('promoCodeId').value = '';
  328.                         promoDiscount = 0;
  329.                         if (typeof recalc === 'function') recalc();
  330.                         return;
  331.                     }
  332.                     document.getElementById('promoCodeId').value = data.id;
  333.                     resultEl.innerHTML = '<span style="color:#27ae60;">✓ ' + data.label + ' applied!</span>';
  334.                     // Store for team plan price calc
  335.                     if (data.promo_type === 1) {
  336.                         promoDiscount = data.max_discount !== null ? Math.min(data.promo_value, data.max_discount) : data.promo_value;
  337.                     } else {
  338.                         promoDiscount = data.promo_value; // pct — applied in recalc
  339.                         promoDiscount = -data.promo_value; // signal: negative = percent
  340.                     }
  341.                     if (typeof recalc === 'function') recalc();
  342.                 });
  343.         });
  344.         document.getElementById('promoCodeInput').addEventListener('keydown', function(e) {
  345.             if (e.key === 'Enter') { e.preventDefault(); promoBtn.click(); }
  346.         });
  347.     }
  348. {% if prefill.plan_type|default('team') == 'team' %}
  349.     var calcUrl      = '{{ path('quote_calculate_price') }}';
  350.     var debounce;
  351.     function recalc() {
  352.         clearTimeout(debounce);
  353.         debounce = setTimeout(function() {
  354.             var fd = new FormData();
  355.             fd.append('normal_users',  document.getElementById('normalUsers').value);
  356.             fd.append('admin_users',   document.getElementById('adminUsers').value);
  357.             fd.append('ml_users',      document.getElementById('mlUsers').value);
  358.             fd.append('billing_cycle', document.getElementById('billingCycle').value);
  359.             fd.append('plan_type',     'team');
  360.             fetch(calcUrl, { method: 'POST', body: fd })
  361.                 .then(function(r){ return r.json(); })
  362.                 .then(function(d) {
  363.                     var cycle = d.billing_cycle;
  364.                     // Apply promo discount
  365.                     var promoOff = 0;
  366.                     if (promoDiscount < 0) {
  367.                         // Percent promo
  368.                         promoOff = Math.round(d.base_amount * (-promoDiscount / 100) * 100) / 100;
  369.                     } else {
  370.                         promoOff = promoDiscount;
  371.                     }
  372.                     var total = Math.max(0, d.base_amount - promoOff);
  373.                     document.getElementById('priceTotal').innerHTML =
  374.                         '€' + total.toFixed(2) +
  375.                         '<span style="font-size:1rem;font-weight:400;color:#7a8ba8;"> / ' + cycle + '</span>';
  376.                     var sub = '';
  377.                     if (cycle === 'yearly') {
  378.                         sub = 'Save €' + d.yearly_savings.toFixed(2) + ' vs. monthly billing (20% off)';
  379.                     }
  380.                     if (promoOff > 0) {
  381.                         sub += (sub ? ' · ' : '') + 'Promo: −€' + promoOff.toFixed(2);
  382.                     }
  383.                     document.getElementById('priceSub').textContent = sub;
  384.                     var breakdown = '';
  385.                     if (d.subtotal_base > 0) {
  386.                         breakdown += '<div class="row-item"><span>Package base</span><span>€' + d.subtotal_base.toFixed(2) + '</span></div>';
  387.                     }
  388.                     if (d.extra_users > 0) {
  389.                         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>';
  390.                     }
  391.                     if (d.extra_admins > 0) {
  392.                         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>';
  393.                     }
  394.                     if (d.ml_users > 0) {
  395.                         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>';
  396.                     }
  397.                     if (promoOff > 0) {
  398.                         breakdown += '<div class="row-item" style="color:#27ae60;"><span>Promo discount</span><span>−€' + promoOff.toFixed(2) + '</span></div>';
  399.                     }
  400.                     document.getElementById('priceBreakdown').innerHTML = breakdown;
  401.                 });
  402.         }, 350);
  403.     }
  404.     document.querySelectorAll('.seat-counter, #billingCycle').forEach(function(el){
  405.         el.addEventListener('input', recalc);
  406.         el.addEventListener('change', recalc);
  407.     });
  408.     recalc();
  409. {% endif %}
  410. })();
  411. </script>