FBR POS Integration Extension for Manager.io

You can start from this AI generated Codes

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <style>
    @page { size: 58mm auto; margin: 0; }

    *, ::after, ::before { margin: 0; padding: 0; box-sizing: border-box; border: 0 solid; }

    body {
      font-family: Consolas, 'Courier New', monospace;
      font-size: 11px; line-height: 1.4; color: #111;
      width: 58mm; padding: 4mm 3mm;
    }
    @media print { body { padding: 2mm 3mm; } }

    /* Header */
    #header { text-align: center; margin-bottom: 3px; }
    #business-logo img { max-width: 46mm; max-height: 15mm; display: block; margin: 0 auto 3px; }
    #business-name    { font-size: 14px; font-weight: bold; text-transform: uppercase; letter-spacing: .5px; }
    #business-address { font-size: 10px; color: #555; margin-top: 1px; }
    #title       { font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; margin-top: 4px; }
    #description { font-size: 10px; color: #444; margin-top: 2px; }

    /* Dividers */
    .div       { border: none; border-top: 1px dashed #aaa; margin: 4px 0; }
    .div-solid { border: none; border-top: 1px solid  #111; margin: 4px 0; }

    /* Fields */
    #fields { font-size: 11px; margin-bottom: 2px; }
    #fields .row { display: flex; justify-content: space-between; gap: 4px; padding: 1px 0; }
    #fields .lbl { color: #555; flex: 1; }
    #fields .val { font-weight: bold; text-align: right; }

    /* Recipient */
    #recipient-section { margin-bottom: 3px; text-align: center; }
    #recipient-section > small { font-size: 9px; text-transform: uppercase; color: #777; letter-spacing: .4px; }
    #recipient-name   { font-size: 11px; font-weight: bold; }
    #recipient-detail { font-size: 10px; color: #555; }

    /* Items table — 3 col: Item | Qty | Total */
    #items-table { width: 100%; border-collapse: collapse; font-size: 9.5px; }

    #items-table th {
      font-size: 10px; font-weight: bold; text-transform: uppercase; color: #444;
      padding: 2px 1px; white-space: nowrap;
      border-top: 1px solid #111; border-bottom: 1px solid #111;
    }
    #items-table td { padding: 2px 1px; vertical-align: top; text-align: right; }
    #items-table tr.item-row td { border-bottom: 1px dotted #ddd; }

    .col-item  { text-align: left !important; word-break: break-word; line-height: 1.3; }
    .col-qty   { text-align: center !important; width: 18px; }
    .col-total { font-weight: bold; width: 30px; }

    /* Column totals */
    #items-table tr.col-total td {
      font-size: 10px; font-weight: bold; text-align: right;
      white-space: nowrap; border-top: 1px solid #111; padding: 2px 1px;
    }

    /* Invoice totals */
    #items-table tr.total td { font-size: 10px; white-space: nowrap; text-align: right; padding: 1px; }
    #items-table tr.total td:first-child { color: #555; }
    #items-table tr.total.emphasis td {
      font-size: 11px; font-weight: bold;
      border-top: 1px solid #111; padding-top: 3px;
    }

    /* Tax breakdown table */
    #tax-breakdown { width: 100%; border-collapse: collapse; font-size: 9px; margin-top: 3px; }
    #tax-breakdown thead th {
      font-size: 9px; font-weight: bold; text-transform: uppercase; color: #777;
      padding: 1px; border-bottom: 1px dashed #aaa; text-align: right;
    }
    #tax-breakdown thead th:first-child { text-align: left; }
    #tax-breakdown td { padding: 1px; text-align: right; color: #444; }
    #tax-breakdown td:first-child { text-align: left; font-weight: bold; }

    /* Custom fields */
    #custom-fields { margin-top: 4px; font-size: 10px; text-align: center; }
    #custom-fields .cf-lbl { font-size: 9px; font-weight: bold; text-transform: uppercase; color: #777; letter-spacing: .3px; }
    #custom-fields .cf-val { font-size: 10px; word-break: break-all; margin-bottom: 4px; }
    #custom-fields .cf-qr  { text-align: center; margin: 3px 0 5px; }
    #custom-fields .cf-qr img { max-width: 35mm; width: 35mm !important; height: auto !important; image-rendering: pixelated; display: block; margin: 0 auto; }

    /* Amount in words */
    #amount-in-words { display: none; font-size: 10px; margin-top: 4px; border-top: 1px dashed #aaa; padding-top: 3px; text-align: center; }
    #amount-in-words strong { font-size: 9px; text-transform: uppercase; color: #666; display: block; margin-bottom: 1px; }

    /* Footers & bottom */
    #footers { font-size: 11px; margin-top: 4px; }
    #receipt-footer { text-align: center; margin-top: 6px; padding-top: 4px; border-top: 1px dashed #aaa; font-size: 9px; color: #888; }
  </style>
</head>
<body>

  <div id="header">
    <div id="business-logo"></div>
    <div id="business-name"></div>
    <div id="business-address"></div>
    <div id="title"></div>
    <div id="description"></div>
  </div>

  <hr class="div-solid" />
  <div id="fields"></div>
  <hr class="div" />

  <div id="recipient-section">
    <small>Bill To</small>
    <div id="recipient-name"></div>
    <div id="recipient-detail"></div>
  </div>

  <hr class="div" />

  <!-- 3-col items table -->
  <table id="items-table">
    <thead><tr id="table-headers"></tr></thead>
    <tbody id="table-rows"></tbody>
  </table>

  <!-- Tax breakdown per tax code -->
  <table id="tax-breakdown" style="display:none">
    <thead>
      <tr>
        <th style="text-align:left">Tax</th>
        <th>Taxable</th>
        <th>Tax Amt</th>
      </tr>
    </thead>
    <tbody id="tax-rows"></tbody>
  </table>

  <div id="custom-fields"></div>
  <div id="amount-in-words"></div>
  <div id="footers"></div>
  <div id="receipt-footer">*** Thank You ***</div>

  <script src="resources/writtennumber/writtennumber.js"></script>
  <script>

    const $ = id => document.getElementById(id);

    function sendResize() {
      window.parent.postMessage({
        type: 'resize',
        width:  document.documentElement.scrollWidth  + 1,
        height: document.documentElement.scrollHeight + 1
      }, '*');
    }

    function el(tag, { cls, html, text, colSpan, id, dataset } = {}) {
      const e = document.createElement(tag);
      if (cls)     e.className   = cls;
      if (html)    e.innerHTML   = html;
      if (text)    e.textContent = text;
      if (colSpan) e.colSpan     = colSpan;
      if (id)      e.id          = id;
      if (dataset) Object.assign(e.dataset, dataset);
      return e;
    }

    function addField(parent, label, value) {
      parent.insertAdjacentHTML('beforeend',
        `<div class="row"><span class="lbl">${label}</span><span class="val">${value}</span></div>`);
    }

    function colIdx(cols, ...labels) {
      return cols.findIndex(c => labels.includes((c.label || '').toLowerCase()));
    }

    // ── Build tax breakdown from rows ──────────────────────────────────────
    // Groups by tax code, sums taxable amount and tax amount per code
    function buildTaxBreakdown(rows, fullCols) {
      const taxI    = colIdx(fullCols, 'tax', 'vat', 'gst');
      const amtI    = colIdx(fullCols, 'amount');
      const taxAmtI = colIdx(fullCols, 'tax amount');
      if (taxI < 0 || amtI < 0 || taxAmtI < 0) return [];

      const map = new Map();
      rows.forEach(row => {
        const code   = row.cells[taxI]?.text   || '';
        const amt    = row.cells[amtI]?.value  || 0;
        const taxAmt = row.cells[taxAmtI]?.value || 0;
        if (!code) return;
        const entry = map.get(code) || { taxable: 0, taxAmt: 0 };
        entry.taxable += amt;
        entry.taxAmt  += taxAmt;
        map.set(code, entry);
      });

      return [...map.entries()].map(([code, v]) => ({
        code,
        taxable: v.taxable.toFixed(2),
        taxAmt:  v.taxAmt.toFixed(2),
      }));
    }

    // ── Amount-in-words ────────────────────────────────────────────────────
    function spellRupeeLike(n, unit, subunit, single) {
      const fr = Math.round(n * 100) % 100;
      return spellOutRupees(n) + ' ' + unit +
        (fr === 0 ? ' Only' : fr === 1 ? ` and One ${single}` : ` and ${spellOutRupees(fr)} ${subunit}`);
    }

    function renderAmountInWords(aiw, totals) {
      const entry = [...(totals || [])].reverse().find(t => t.key === 'Total');
      if (!entry) return;
      const n = entry.number;
      let text;

      if (aiw.currencyPrefix === '₹' || aiw.currencyCode === 'INR')
        text = spellRupeeLike(n, 'Rupees', 'Paise', 'Paisa');
      else if (aiw.currencyPrefix === '৳' || aiw.currencyCode === 'BDT')
        text = spellRupeeLike(n, 'Taka', 'Paise', 'Paisa');
      else {
        const mult = 10 ** aiw.decimalPlaces;
        let wn = writtenNumber(Math.floor(n), { language: aiw.language, currency: aiw.currencyCode });
        if (wn) wn = wn[0].toUpperCase() + wn.slice(1);
        const fr = Math.round(n * mult) % mult;
        text = wn + (fr > 0 ? ` ${aiw.andText} ${fr}/${'1' + '0'.repeat(aiw.decimalPlaces)}` : '');
      }

      if (text) {
        const div = $('amount-in-words');
        div.style.display = 'block';
        div.innerHTML = `<strong>${aiw.label}</strong>${text}`;
      }
      sendResize();
    }

    // ── Main ───────────────────────────────────────────────────────────────
    window.addEventListener('message', ({ source, data: msg }) => {
      if (source !== window.parent || msg.type !== 'context-response') return;

      const d = msg.body;

      document.documentElement.dir = d.direction || 'ltr';
      document.title = [d.business?.name, d.title, d.reference].filter(Boolean).join(' - ');

      // Logo
      if (d.business?.logo) {
        const img = Object.assign(document.createElement('img'), { src: d.business.logo });
        img.addEventListener('load', sendResize);
        $('business-logo').appendChild(img);
      }

      // Header
      $('business-name').textContent  = d.business?.name || '';
      $('business-address').innerHTML = (d.business?.address || '').replace(/\n/g, '<br>');
      $('title').textContent          = (d.title || '').toUpperCase();
      $('description').textContent    = d.description || '';

      // Fields
      const fieldsDiv = $('fields');
      fieldsDiv.innerHTML = '';
      (d.fields || []).forEach(f => addField(fieldsDiv, f.label, f.text));

      // Recipient
      $('recipient-name').textContent = d.recipient?.name || '';
      $('recipient-detail').innerHTML = [d.recipient?.code, d.recipient?.address]
        .filter(Boolean).join('<br>');

      // ── 3-col table: Item | Qty | Total ───────────────────────────────────
      const fullCols = d.table.columns || [];
      const qtyI    = colIdx(fullCols, 'pce', 'qty', 'quantity');
      const itemI   = colIdx(fullCols, 'item', 'description', 'product');
      const totalI  = fullCols.findIndex(c => c.alwaysShow) !== -1
                        ? fullCols.findIndex(c => c.alwaysShow)
                        : colIdx(fullCols, 'total');

      const condensed = [
        { label: 'Item',  cls: 'col-item',  srcI: itemI  !== -1 ? itemI  : 1, sumText: null },
        { label: fullCols[qtyI]?.label || 'Qty', cls: 'col-qty', srcI: qtyI, sumText: fullCols[qtyI]?.sumText || null },
        { label: fullCols[totalI]?.label || 'Total', cls: 'col-total', srcI: totalI, sumText: fullCols[totalI]?.sumText || null },
      ];

      // Headers
      const thead = $('table-headers');
      thead.innerHTML = '';
      condensed.forEach(c => thead.appendChild(el('th', { cls: c.cls, text: c.label })));

      // Rows
      const tbody = $('table-rows');
      tbody.innerHTML = '';

      (d.table.rows || []).forEach(row => {
        const tr = el('tr', { cls: 'item-row' });
        condensed.forEach(col => {
          tr.appendChild(el('td', {
            cls:  col.cls,
            html: row.cells[col.srcI]?.text || ''
          }));
        });
        tbody.appendChild(tr);
      });

      // Column totals
      const ctTr = el('tr', { cls: 'col-total' });
      let hasCT = false;
      condensed.forEach(c => {
        const td = el('td', { cls: c.cls });
        if (c.sumText) { td.textContent = c.sumText; hasCT = true; }
        ctTr.appendChild(td);
      });
      if (hasCT) tbody.appendChild(ctTr);

      // Invoice totals — only show emphasis row (grand total), skip sub-total & tax rows
      (d.table.totals || []).filter(t => t.emphasis).forEach(t => {
        const tr = el('tr', { cls: 'total' + (t.emphasis ? ' emphasis' : '') });
        tr.appendChild(el('td', { text: t.label, colSpan: condensed.length - 1 }));
        const tdVal = el('td', { html: t.text, id: t.key || '', dataset: { value: t.number } });
        if (t.class) tdVal.classList.add(t.class);
        tr.appendChild(tdVal);
        tbody.appendChild(tr);
      });

      // ── Tax breakdown ──────────────────────────────────────────────────────
      const taxData = buildTaxBreakdown(d.table.rows || [], fullCols);
      if (taxData.length) {
        const taxTbody = $('tax-rows');
        taxTbody.innerHTML = '';
        taxData.forEach(({ code, taxable, taxAmt }) => {
          taxTbody.insertAdjacentHTML('beforeend',
            `<tr>
               <td>${code}</td>
               <td>${taxable}</td>
               <td>${taxAmt}</td>
             </tr>`);
        });
        $('tax-breakdown').style.display = 'table';
      }

      // Custom fields
      const cfDiv = $('custom-fields');
      cfDiv.innerHTML = '';
      (d.custom_fields || []).forEach(f => {
        if (f.displayAtTheTop) return addField(fieldsDiv, f.label, f.text);
        const isImg = (f.text || '').includes('<img');
        cfDiv.insertAdjacentHTML('beforeend',
          `<div class="cf-lbl">${f.label}</div>
           <div class="${isImg ? 'cf-qr' : 'cf-val'}">${
             isImg ? f.text : (f.text || '').replace(/\n/g, '<br>')
           }</div>`);

        // Strip inline width/height from QR images so CSS can scale them properly
        if (isImg) {
          cfDiv.querySelectorAll('.cf-qr img').forEach(img => {
            img.style.removeProperty('width');
            img.style.removeProperty('height');
          });
        }
      });

      // Amount in words
      if (d.amountInWords) {
        const s = Object.assign(document.createElement('script'),
          { src: `resources/writtennumber/lang-${d.amountInWords.language}.js` });
        s.onload = () => renderAmountInWords(d.amountInWords, d.table.totals);
        document.head.appendChild(s);
      }

      // Footers
      const footersDiv = $('footers');
      footersDiv.innerHTML = '';
      (d.footers || []).forEach(f => {
        const div = Object.assign(document.createElement('div'), { innerHTML: f });
        div.style.marginTop = '8px';
        div.querySelectorAll('script').forEach(s => {
          const ns = document.createElement('script');
          [...s.attributes].forEach(a => ns.setAttribute(a.name, a.value));
          ns.textContent = s.textContent;
          s.replaceWith(ns);
        });
        footersDiv.appendChild(div);
      });

      sendResize();
    });

    window.addEventListener('load', () =>
      window.parent.postMessage({ type: 'context-request' }, '*')
    );

  </script>
</body>
</html>