Examples
Quick Quote Tool
Section titled “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
Section titled “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 Vera — 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.jsonwithtype: "quoting_extension"soaddLinesis unlockedinitBridge()callsproducts.list({ pageSize: 500 })and populatesskuIdMapcollectQuoteLines()builds lines with:groupKey("blind-1","blind-2") so blind + pelmet lines are grouped in the quoteproductIdresolved fromskuIdMapfor task template injectionproductCodeas the human-readable SKUcostPrice(wholesale) alongsideunitPrice(retail) for real margin in the COB panelunit: "T_ITEM"for per-blind lines (use"T_SQUARE_METRE"for fabric area charges)locationfor the area/zone label andspecificationfor dimensions and config detail