Example: Inventory Tracker
An inventory management app with quantities, categories, and low-stock tracking. Demonstrates number and boolean fields, category filtering, and bulkUpdateRecords for stocktake adjustments.
Schema
Entity: Items — key: items
| Field | Type | Notes |
|---|---|---|
name |
text | required |
sku |
text | required |
category |
select | electronics / furniture / supplies / other — required |
quantity |
number | required, min: 0 |
unit_price |
number | min: 0 |
location |
text | |
is_low_stock |
boolean | |
last_restocked |
date | |
notes |
textarea |
Source Code
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Inventory Tracker</title>7 <style>8 * { box-sizing: border-box; margin: 0; padding: 0; }9 body { font-family: system-ui, sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; color: #1a1a1a; }10 h1 { margin-bottom: 20px; }11 .toolbar { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }12 .toolbar select, .toolbar button { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }13 .toolbar button { background: #2563eb; color: white; border: none; cursor: pointer; }14 .toolbar button:hover { background: #1d4ed8; }15 .toolbar button.secondary { background: white; color: #374151; border: 1px solid #d1d5db; }16 .toolbar button.secondary:hover { background: #f3f4f6; }17 table { width: 100%; border-collapse: collapse; font-size: 14px; }18 th { text-align: left; padding: 8px 12px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; font-weight: 600; white-space: nowrap; }19 td { padding: 10px 12px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; }20 tr:hover td { background: #fafafa; }21 .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; }22 .badge.electronics { background: #dbeafe; color: #1e40af; }23 .badge.furniture { background: #fef3c7; color: #92400e; }24 .badge.supplies { background: #d1fae5; color: #065f46; }25 .badge.other { background: #f3f4f6; color: #374151; }26 .qty { font-weight: 600; }27 .qty.low { color: #dc2626; }28 .qty.ok { color: #059669; }29 .low-badge { display: inline-block; margin-left: 4px; font-size: 10px; background: #fee2e2; color: #dc2626; padding: 1px 5px; border-radius: 99px; font-weight: 600; }30 .actions button { padding: 4px 8px; font-size: 12px; border: 1px solid #d1d5db; border-radius: 4px; background: white; cursor: pointer; margin-right: 2px; }31 .actions button.delete { color: #dc2626; border-color: #fecaca; }32 .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 100; justify-content: center; align-items: flex-start; padding-top: 60px; }33 .modal-overlay.open { display: flex; }34 .modal { background: white; border-radius: 12px; padding: 24px; width: 100%; max-width: 520px; max-height: calc(100vh - 100px); overflow-y: auto; }35 .modal h2 { margin-bottom: 16px; }36 .field { margin-bottom: 12px; }37 .field label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; }38 .field input, .field select, .field textarea { width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }39 .field textarea { resize: vertical; min-height: 60px; }40 .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }41 .checkbox-row { display: flex; align-items: center; gap: 8px; margin-top: 4px; }42 .checkbox-row input { width: auto; }43 .checkbox-row label { font-size: 14px; font-weight: normal; margin: 0; }44 .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }45 .modal-actions button { padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; }46 .btn-cancel { background: white; border: 1px solid #d1d5db; }47 .btn-submit { background: #2563eb; color: white; border: none; }48 .error { color: #dc2626; font-size: 13px; margin-top: 4px; }49 .empty { text-align: center; padding: 40px; color: #9ca3af; }50 .loading { text-align: center; padding: 40px; color: #6b7280; }51 @media (max-width: 640px) { table, thead, tbody, th, td, tr { display: block; } th { display: none; } td { padding: 4px 0; } td::before { content: attr(data-label); font-weight: 600; margin-right: 8px; font-size: 11px; color: #6b7280; text-transform: uppercase; } }52 </style>53</head>54<body>55 <h1>Inventory</h1>5657 <div class="toolbar">58 <select id="filter-category">59 <option value="">All categories</option>60 <option value="electronics">Electronics</option>61 <option value="furniture">Furniture</option>62 <option value="supplies">Supplies</option>63 <option value="other">Other</option>64 </select>65 <select id="filter-stock">66 <option value="">All stock levels</option>67 <option value="low">Low stock only</option>68 <option value="ok">In stock only</option>69 </select>70 <button id="btn-create" style="display:none;">+ Add Item</button>71 </div>7273 <table id="item-table">74 <thead>75 <tr>76 <th>Name / SKU</th>77 <th>Category</th>78 <th>Quantity</th>79 <th>Unit Price</th>80 <th>Location</th>81 <th>Last Restocked</th>82 <th id="th-actions" style="display:none;"></th>83 </tr>84 </thead>85 <tbody id="item-body">86 <tr><td colspan="7"><div class="loading">Loading inventory…</div></td></tr>87 </tbody>88 </table>8990 <div class="modal-overlay" id="modal">91 <div class="modal">92 <h2 id="modal-title">Add Item</h2>93 <form id="item-form">94 <div class="field-row">95 <div class="field">96 <label for="name">Name *</label>97 <input type="text" id="name" name="name" required>98 <div class="error" id="error-name"></div>99 </div>100 <div class="field">101 <label for="sku">SKU *</label>102 <input type="text" id="sku" name="sku" required>103 <div class="error" id="error-sku"></div>104 </div>105 </div>106 <div class="field-row">107 <div class="field">108 <label for="category">Category *</label>109 <select id="category" name="category" required>110 <option value="">Select…</option>111 <option value="electronics">Electronics</option>112 <option value="furniture">Furniture</option>113 <option value="supplies">Supplies</option>114 <option value="other">Other</option>115 </select>116 <div class="error" id="error-category"></div>117 </div>118 <div class="field">119 <label for="location">Location</label>120 <input type="text" id="location" name="location" placeholder="e.g. Warehouse A, Shelf 3">121 <div class="error" id="error-location"></div>122 </div>123 </div>124 <div class="field-row">125 <div class="field">126 <label for="quantity">Quantity *</label>127 <input type="number" id="quantity" name="quantity" min="0" required>128 <div class="error" id="error-quantity"></div>129 </div>130 <div class="field">131 <label for="unit_price">Unit Price</label>132 <input type="number" id="unit_price" name="unit_price" min="0" step="0.01" placeholder="0.00">133 <div class="error" id="error-unit_price"></div>134 </div>135 </div>136 <div class="field-row">137 <div class="field">138 <label for="last_restocked">Last Restocked</label>139 <input type="date" id="last_restocked" name="last_restocked">140 <div class="error" id="error-last_restocked"></div>141 </div>142 <div class="field">143 <label> </label>144 <div class="checkbox-row">145 <input type="checkbox" id="is_low_stock" name="is_low_stock">146 <label for="is_low_stock">Mark as low stock</label>147 </div>148 <div class="error" id="error-is_low_stock"></div>149 </div>150 </div>151 <div class="field">152 <label for="notes">Notes</label>153 <textarea id="notes" name="notes"></textarea>154 <div class="error" id="error-notes"></div>155 </div>156 <div class="error" id="error-general"></div>157 <div class="modal-actions">158 <button type="button" class="btn-cancel" id="btn-cancel">Cancel</button>159 <button type="submit" class="btn-submit">Save</button>160 </div>161 </form>162 </div>163 </div>164165 <script src="https://sdk.workapps.tech/v1.js"></script>166 <script>167 const sdk = new WorkAppsSDK();168 const { ErrorCode } = WorkAppsSDK;169170 let canEdit = false;171 let editingId = null;172173 async function init() {174 const bootstrap = await sdk.getBootstrap();175 canEdit = ['editor', 'admin'].includes(bootstrap.role);176 if (canEdit) {177 document.getElementById('btn-create').style.display = 'block';178 document.getElementById('th-actions').style.display = '';179 }180 loadItems();181 }182183 async function loadItems() {184 const tbody = document.getElementById('item-body');185 const category = document.getElementById('filter-category').value;186 const stockFilter = document.getElementById('filter-stock').value;187188 try {189 const filter = {};190 if (category) filter.category = { eq: category };191 if (stockFilter === 'low') filter.is_low_stock = { eq: true };192 if (stockFilter === 'ok') filter.is_low_stock = { eq: false };193194 const result = await sdk.listRecords('items', {195 filter: Object.keys(filter).length ? filter : undefined,196 sort: 'name:asc',197 pageSize: 100,198 });199200 if (result.data.length === 0) {201 tbody.innerHTML = '<tr><td colspan="7"><div class="empty">No items found.</div></td></tr>';202 return;203 }204205 tbody.innerHTML = result.data.map(item => {206 const qtyClass = item.is_low_stock ? 'low' : 'ok';207 const price = item.unit_price != null208 ? `$${Number(item.unit_price).toFixed(2)}`209 : '—';210211 return `212 <tr>213 <td data-label="Name">214 <strong>${escapeHtml(item.name)}</strong><br>215 <span style="font-size:12px;color:#6b7280;">${escapeHtml(item.sku)}</span>216 </td>217 <td data-label="Category">218 <span class="badge ${item.category}">${escapeHtml(item.category)}</span>219 </td>220 <td data-label="Quantity">221 <span class="qty ${qtyClass}">${item.quantity}</span>222 ${item.is_low_stock ? '<span class="low-badge">LOW</span>' : ''}223 </td>224 <td data-label="Unit Price">${price}</td>225 <td data-label="Location">${item.location ? escapeHtml(item.location) : '—'}</td>226 <td data-label="Last Restocked">${item.last_restocked ?? '—'}</td>227 ${canEdit ? `228 <td class="actions">229 <button onclick="openEdit('${item.id}')">Edit</button>230 <button class="delete" onclick="deleteItem('${item.id}')">Delete</button>231 </td>232 ` : '<td></td>'}233 </tr>234 `;235 }).join('');236 } catch (error) {237 tbody.innerHTML = `<tr><td colspan="7"><div class="error">Failed to load inventory: ${escapeHtml(error.message)}</div></td></tr>`;238 }239 }240241 document.getElementById('filter-category').addEventListener('change', loadItems);242 document.getElementById('filter-stock').addEventListener('change', loadItems);243244 document.getElementById('btn-create').addEventListener('click', () => {245 editingId = null;246 document.getElementById('modal-title').textContent = 'Add Item';247 document.getElementById('item-form').reset();248 document.getElementById('modal').classList.add('open');249 });250251 document.getElementById('btn-cancel').addEventListener('click', closeModal);252253 async function openEdit(id) {254 const result = await sdk.getRecord('items', id);255 const item = result.data;256 editingId = id;257 document.getElementById('modal-title').textContent = 'Edit Item';258 document.getElementById('name').value = item.name || '';259 document.getElementById('sku').value = item.sku || '';260 document.getElementById('category').value = item.category || '';261 document.getElementById('location').value = item.location || '';262 document.getElementById('quantity').value = item.quantity ?? '';263 document.getElementById('unit_price').value = item.unit_price ?? '';264 document.getElementById('last_restocked').value = item.last_restocked || '';265 document.getElementById('is_low_stock').checked = !!item.is_low_stock;266 document.getElementById('notes').value = item.notes || '';267 document.getElementById('modal').classList.add('open');268 }269270 document.getElementById('item-form').addEventListener('submit', async (e) => {271 e.preventDefault();272 clearErrors();273274 const quantityRaw = document.getElementById('quantity').value;275 const priceRaw = document.getElementById('unit_price').value;276277 const data = {278 name: document.getElementById('name').value,279 sku: document.getElementById('sku').value,280 category: document.getElementById('category').value,281 location: document.getElementById('location').value || null,282 quantity: quantityRaw !== '' ? Number(quantityRaw) : null,283 unit_price: priceRaw !== '' ? Number(priceRaw) : null,284 last_restocked: document.getElementById('last_restocked').value || null,285 is_low_stock: document.getElementById('is_low_stock').checked,286 notes: document.getElementById('notes').value || null,287 };288289 try {290 if (editingId) {291 await sdk.updateRecord('items', editingId, data);292 } else {293 await sdk.createRecord('items', data);294 }295 closeModal();296 loadItems();297 } catch (error) {298 if (error.code === ErrorCode.ValidationFailed) {299 const fieldErrors = error.getFieldErrors();300 Object.entries(fieldErrors).forEach(([field, message]) => {301 const el = document.getElementById('error-' + field);302 if (el) el.textContent = message;303 });304 } else {305 document.getElementById('error-general').textContent = error.message;306 }307 }308 });309310 async function deleteItem(id) {311 // In production, use a custom confirmation dialog — never window.confirm312 try {313 await sdk.deleteRecord('items', id);314 loadItems();315 } catch (error) {316 console.error('Delete failed:', error.message);317 }318 }319320 function closeModal() {321 document.getElementById('modal').classList.remove('open');322 }323324 function escapeHtml(str) {325 if (str == null) return '';326 const div = document.createElement('div');327 div.textContent = String(str);328 return div.innerHTML;329 }330331 function clearErrors() {332 document.querySelectorAll('.error').forEach(el => el.textContent = '');333 }334335 init();336 </script>337</body>338</html>
Key Patterns
Boolean Filter
1// Low stock only2filter.is_low_stock = { eq: true };34// In stock only5filter.is_low_stock = { eq: false };
Number Field Handling
Always convert form input strings to numbers before sending to the SDK — form inputs return strings even for type="number":
1quantity: document.getElementById('quantity').value !== ''2 ? Number(document.getElementById('quantity').value)3 : null,
Bulk Stocktake Adjustment
Update multiple items at once after a stocktake. Check result.error per batch to handle partial failures:
1const adjustments = [2 { id: '01JA...', data: { quantity: 45, is_low_stock: false } },3 { id: '01JB...', data: { quantity: 3, is_low_stock: true } },4 { id: '01JC...', data: { quantity: 0, is_low_stock: true } },5];67const result = await sdk.bulkUpdateRecords('items', adjustments);89if (result.error) {10 console.warn('Some updates failed:', result.error.details);11}
Low-Stock Indicator
1const qtyClass = item.is_low_stock ? 'low' : 'ok';2// render: <span class="qty low">3 <span class="low-badge">LOW</span></span>