Quick Quote Tool
This ~50-line tool demonstrates the full bridge loop for astandalone 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 aquoting_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"
}
<!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>
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