Skip to main content

Quick Quote Tool

This ~50-line tool demonstrates the full bridge loop for a standalone tool: read customers and products (now including sku), let the user build a quote, and submit via quotes.create.
<!DOCTYPE html>
<html>
<head><script src="/bridge/sdk.js"></script></head>
<body>
  <h2>Quick Quote</h2>
  <label>Customer:</label>
  <select id="customer"></select>
  <label>Product:</label>
  <select id="product"></select>
  <label>Qty:</label>
  <input id="qty" type="number" value="1" min="1" />
  <button onclick="submit()">Create Quote</button>
  <div id="result"></div>
  <script>
    window.pipeline.customers.list().then(function(r) {
      r.items.forEach(function(c) {
        var o = document.createElement('option');
        o.value = JSON.stringify(c);
        o.text = c.name;
        document.getElementById('customer').add(o);
      });
    });
    window.pipeline.products.list().then(function(r) {
      r.items.forEach(function(p) {
        var o = document.createElement('option');
        o.value = JSON.stringify(p);
        // p.sku is now included in the response shape
        o.text = p.name + ' [' + p.sku + '] ($' + p.price + ')';
        document.getElementById('product').add(o);
      });
    });
    function submit() {
      var c = JSON.parse(document.getElementById('customer').value);
      var p = JSON.parse(document.getElementById('product').value);
      var qty = parseFloat(document.getElementById('qty').value);
      window.pipeline.quotes.create({
        customerName: c.name,
        customerEmail: c.email,
        upsertByEmail: true,
        title: 'Quick Quote',
        lines: [{
          description: p.name,
          productCode: p.sku,
          qty: qty,
          unitPrice: p.price,
          taxRate: 0.15
        }]
      }).then(function(r) {
        document.getElementById('result').textContent = 'Quote created: ' + r.ref + ' (total: $' + r.total + ')';
      }).catch(function(e) {
        document.getElementById('result').textContent = 'Error: ' + e.message;
      });
    }
  </script>
</body>
</html>

Reference: Ziptrak Retail Estimator

This is the full reference implementation for a quoting_extension tool. It shows initBridge with skuIdMap, and collectQuoteLines with groupKey, productId, costPrice, unit, location, and specification. manifest.json:
{
  "name": "Ziptrak Retail Estimator",
  "shortName": "Ziptrak",
  "type": "quoting_extension",
  "version": "2.0.0",
  "description": "Calculate supply and install costs for Ziptrak outdoor blinds. Adds itemised lines directly to the open quote.",
  "entry": "index.html"
}
index.html (key JS excerpt):
<!DOCTYPE html>
<html>
<head>
  <script src="/bridge/sdk.js"></script>
</head>
<body>
  <!-- ... estimator UI ... -->
  <button id="submitQuoteBtn">Add to Quote</button>

  <script>
    // Maps uppercase SKU code → product GUID, populated during initBridge().
    // Sent as productId on each line so the host can inject task template sequences.
    let skuIdMap = {};

    // Tenant-specific SKU codes — edit to match the tenant's Stock_SKU.SKU values.
    const SKU_CODES = {
      mesh:   'ZPME',   // Ziptrak outdoor blinds - Mesh
      pvc:    'ZPPC',   // Ziptrak outdoor blinds - Clear PVC
      pelmet: 'PLMT'    // Pelmet for Ziptrak
    };

    const SKU_NAMES = {
      mesh:   'Ziptrak Outdoor Blind \u2013 Mesh',
      pvc:    'Ziptrak Outdoor Blind \u2013 Clear PVC',
      pelmet: 'Pelmet for Ziptrak'
    };

    async function initBridge() {
      if (typeof window.pipeline === 'undefined' || !window.pipeline.quotes) {
        // Running outside Pipeline — disable submit button
        document.getElementById('submitQuoteBtn').disabled = true;
        return;
      }

      try {
        // Load all products and build sku→id map for task template injection
        const productResult = await window.pipeline.products.list({ page: 1, pageSize: 500 });
        const products = (productResult && productResult.items) ? productResult.items : [];
        products.forEach(function(p) {
          if (p.sku && p.id) skuIdMap[p.sku.toUpperCase()] = p.id;
        });
      } catch (_) {
        // Product lookup is informational — tool still submits with SKU_CODES as free-form
      }
    }

    // Build BridgeAddLineDto[] for all configured blind rows.
    // Each blind row generates up to 3 lines (blind, pelmet, add-extra) sharing a groupKey.
    function collectQuoteLines() {
      const lines = [];

      rows.forEach(function(row, idx) {
        const blindCalc = getBlindWholesale(row.type, row.widthMm, row.heightMm, row.reverseHandle);
        const pelmetCalc = getPelmetWholesale(row.pelmetType, row.widthMm);
        if (!blindCalc.valid) return;

        const blindMarkup = num(document.getElementById('blindMarkup').value) / 100;
        const pelmetMarkup = num(document.getElementById('pelmetMarkup').value) / 100;
        const blindRetail = blindCalc.price * (1 + blindMarkup);
        const pelmetRetail = pelmetCalc.valid ? pelmetCalc.price * (1 + pelmetMarkup) : 0;
        const installCost = /* per-row install rate */ 150;
        const rowLabel = row.label || ('Blind ' + (idx + 1));
        const groupKey = 'blind-' + (idx + 1);   // all lines for this blind share one group

        // Main blind line — fabric + installation bundled as one retail price
        const blindSkuCode = row.type === 'Mesh' ? SKU_CODES.mesh : SKU_CODES.pvc;
        const blindSkuName = row.type === 'Mesh' ? SKU_NAMES.mesh : SKU_NAMES.pvc;
        lines.push({
          description:   blindSkuName,
          productCode:   blindSkuCode,
          productId:     skuIdMap[blindSkuCode.toUpperCase()] || null,  // triggers task injection
          qty:           1,
          unitPrice:     Math.round((blindRetail + installCost) * 100) / 100,  // retail (marked-up)
          costPrice:     Math.round((blindCalc.price + installCost) * 100) / 100,  // buy price
          taxRate:       15,
          location:      rowLabel,                 // e.g. "Patio", "Dining"
          specification: row.type + ', ' + blindCalc.band,  // e.g. "Mesh, W ≤ 3.0m, H ≤ 2.5m"
          unit:          'T_ITEM',                 // each blind is one item
          groupKey:      groupKey
        });

        // Pelmet line — grouped with the blind above
        if (pelmetCalc.valid && pelmetRetail > 0) {
          lines.push({
            description:   SKU_NAMES.pelmet,
            productCode:   SKU_CODES.pelmet,
            productId:     skuIdMap[SKU_CODES.pelmet.toUpperCase()] || null,
            qty:           1,
            unitPrice:     Math.round(pelmetRetail * 100) / 100,
            costPrice:     Math.round(pelmetCalc.price * 100) / 100,
            taxRate:       15,
            location:      rowLabel,
            specification: row.pelmetType,
            unit:          'T_ITEM',
            groupKey:      groupKey               // same group as the blind above
          });
        }

        // Optional: add a per-m² fabric charge as a separate line
        // const widthM = row.widthMm / 1000;
        // const heightM = row.heightMm / 1000;
        // lines.push({
        //   description: 'Mesh fabric',
        //   qty: widthM * heightM,
        //   unitPrice: 45.00,
        //   taxRate: 15,
        //   unit: 'T_SQUARE_METRE',
        //   groupKey: groupKey
        // });
      });

      return lines;
    }

    document.getElementById('submitQuoteBtn').addEventListener('click', async function() {
      const quoteLines = collectQuoteLines();
      if (quoteLines.length === 0) return;

      try {
        await window.pipeline.quotes.addLines(quoteLines);
        alert(quoteLines.length + ' lines added to quote.');
      } catch (err) {
        alert('Failed: ' + err.message);
      }
    });

    initBridge();
  </script>
</body>
</html>
What the Ziptrak example demonstrates:
  • manifest.json with type: "quoting_extension" so addLines is unlocked
  • initBridge() calls products.list({ pageSize: 500 }) and populates skuIdMap
  • collectQuoteLines() builds lines with:
    • groupKey ("blind-1", "blind-2") so blind + pelmet lines are grouped in the quote
    • productId resolved from skuIdMap for task template injection
    • productCode as the human-readable SKU
    • costPrice (wholesale) alongside unitPrice (retail) for real margin in the COB panel
    • unit: "T_ITEM" for per-blind lines (use "T_SQUARE_METRE" for fabric area charges)
    • location for the area/zone label and specification for dimensions and config detail