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>&nbsp;</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>