WFDASH-WEBAPP/reports/intel-mockup.html
Dave 3a59fa104c feat(item-intel): add new item intel popover and backend workflows
This commit replaces the legacy item history and AI insights system with a standardized, fuzzy-matched item intel workflow:
- Update AiItemHistoryTrigger component to support both quote and order items via the `collection` prop, replacing the old `insights` prop with `intel`
- Add full `ItemIntel` type definitions to `src/lib/types.ts` for the new standardized insight payload
- Create new REST API endpoints for fetching and computing item intel for `quote_items` and `order_items`
- Rewrite the MCP insight endpoint to use the new shared `item-intel-service` instead of legacy individual insight functions
- Reorder quotes page tabs to place Delivery Note quotes alongside New quotes for better daily operator workflow
- Add item intel triggers to quote process and order process pages
- Remove deprecated document-level `AiInsightsPanel` components from quote and order detail pages
- Add bulk backfill endpoint for populating intel across existing active items
2026-08-11 02:57:57 +02:00

383 lines
No EOL
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Item Intel Mockup · Q202608094</title>
<style>
:root {
--bg: #f8fafc;
--panel: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--muted-2: #94a3b8;
--accent: #4f46e5;
--accent-soft: #eef2ff;
--green: #16a34a;
--green-soft: #dcfce7;
--amber: #d97706;
--amber-soft: #fef3c7;
--red: #dc2626;
--red-soft: #fee2e2;
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
background: var(--bg);
color: var(--text);
line-height: 1.45;
}
.page { max-width: 880px; margin: 0 auto; padding: 24px 16px 80px; }
.header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px; }
.header h1 { font-size: 18px; font-weight: 600; margin: 0; }
.badge {
background: var(--accent-soft); color: var(--accent);
font-size: 10px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.05em; padding: 2px 8px; border-radius: 999px;
}
.sub { color: var(--muted); font-size: 13px; margin-bottom: 24px; }
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 0; }
.tab {
padding: 8px 14px; font-size: 13px; font-weight: 500;
border: 1px solid transparent; border-bottom: none;
border-radius: 6px 6px 0 0; cursor: pointer; color: var(--muted); background: transparent;
}
.tab[aria-selected="true"] {
color: var(--accent); background: var(--panel);
border-color: var(--border); margin-bottom: -1px;
}
.popover {
background: var(--panel); border: 1px solid var(--border);
border-radius: 0 8px 8px 8px; padding: 16px;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
}
.item-header {
display: flex; align-items: baseline; justify-content: space-between;
gap: 12px; padding-bottom: 12px;
border-bottom: 1px dashed var(--border); margin-bottom: 14px;
}
.item-desc { font-weight: 600; font-size: 14px; }
.item-meta { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.summary {
background: var(--accent-soft); color: var(--accent);
padding: 8px 12px; border-radius: 6px; font-size: 12px; margin-bottom: 16px;
}
.summary b { color: var(--text); }
.section { margin-bottom: 18px; }
.section:last-child { margin-bottom: 0; }
.section-title {
display: flex; align-items: center; justify-content: space-between;
font-size: 10px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.06em; color: var(--muted); margin-bottom: 6px;
}
.section-title .pill {
background: var(--border); color: var(--text);
padding: 1px 6px; border-radius: 999px;
font-size: 10px; font-weight: 600; text-transform: none; letter-spacing: 0;
}
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th, td {
text-align: left; padding: 6px 8px;
border-bottom: 1px solid var(--border); vertical-align: middle;
}
th {
font-weight: 600; color: var(--muted); font-size: 10px;
text-transform: uppercase; letter-spacing: 0.04em; background: #f1f5f9;
}
tr:last-child td { border-bottom: none; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
td.dim { color: var(--muted); }
td.mono { font-family: var(--mono); font-size: 11px; }
.status {
display: inline-block; font-size: 10px; font-weight: 700;
padding: 1px 6px; border-radius: 4px;
text-transform: uppercase; letter-spacing: 0.04em;
}
.status.in_stock { background: var(--green-soft); color: var(--green); }
.status.low { background: var(--amber-soft); color: var(--amber); }
.status.out { background: var(--red-soft); color: var(--red); }
.status.received { background: var(--green-soft); color: var(--green); }
.status.sent { background: var(--accent-soft); color: var(--accent); }
.status.delivered { background: var(--green-soft); color: var(--green); }
.status.pending { background: var(--border); color: var(--muted); }
.status.problem { background: var(--red-soft); color: var(--red); }
.status.emailed { background: var(--accent-soft); color: var(--accent); }
.status.approved { background: var(--green-soft); color: var(--green); }
.btn {
border: 1px solid var(--border); background: var(--panel);
color: var(--text); font-size: 11px; font-weight: 500;
padding: 3px 8px; border-radius: 4px; cursor: pointer; margin-right: 4px;
}
.btn:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.primary { background: var(--accent); color: white; border-color: var(--accent); }
.btn.primary:hover { background: #4338ca; }
.empty { color: var(--muted-2); font-style: italic; padding: 8px 0; font-size: 12px; }
.footer-note {
margin-top: 24px; padding: 12px 16px;
background: #f1f5f9; border-radius: 6px;
font-size: 11px; color: var(--muted);
}
.legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; font-size: 11px; color: var(--muted); }
.legend span::before { content: "•"; margin-right: 4px; color: var(--accent); }
.panel-hidden { display: none; }
</style>
</head>
<body>
<div class="page">
<div class="header">
<h1>Item Intel Mockup</h1>
<span class="badge">Q202608094</span>
</div>
<div class="sub">
Click a tab to swap the quote item. This is the popover that would render when you click the
ⓘ icon next to a line item. Limits: stock ≤3, supplier history = last per unique supplier,
price list ≤5, recent quote ≤2, recent order ≤2.
</div>
<div class="tabs" role="tablist">
<button class="tab" aria-selected="true" data-tab="0">M20 x 30 set screw black<span class="count" style="font-size:10px;color:var(--muted-2);margin-left:4px;">qty 399</span></button>
<button class="tab" aria-selected="false" data-tab="1">SAE 50 - 20L<span class="count" style="font-size:10px;color:var(--muted-2);margin-left:4px;">qty 2</span></button>
<button class="tab" aria-selected="false" data-tab="2">Silicone sealant clear<span class="count" style="font-size:10px;color:var(--muted-2);margin-left:4px;">qty 12</span></button>
</div>
<!-- Item 0 — M20 set screw -->
<div class="popover" data-panel="0">
<div class="item-header">
<div>
<div class="item-desc">M20 x 30 set screw black</div>
<div class="item-meta">quote_item · rar7m1d0m6jgeb5 · qty 399 · no SKU</div>
</div>
</div>
<div class="summary">
out of stock · bought from <b>3 suppliers</b> — top: Gracal R 6.22 (3d ago) · last quoted R 12.20 14d ago (Q000032) · last order 11d ago
</div>
<div class="section">
<div class="section-title">Stock List <span class="pill">3 rows</span></div>
<table>
<thead><tr><th>SKU</th><th>Name</th><th class="num">Qty</th><th class="num">Cost</th><th>Supplier</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">PKS11</td><td>M20 X 75 SET SCREW ZPL</td><td class="num">0</td><td class="num dim">—</td><td>Pro-Tech Fasteners</td><td><span class="status out">out</span></td><td><button class="btn">copy</button></td></tr>
<tr><td class="mono">PKS12</td><td>M20 X 80 SET SCREW ZPL</td><td class="num">0</td><td class="num dim">—</td><td>Pro-Tech Fasteners</td><td><span class="status out">out</span></td><td><button class="btn">copy</button></td></tr>
<tr><td class="mono">PKS06</td><td>M12 X 40 SET SCREW ZPL</td><td class="num">0</td><td class="num dim">—</td><td>NSS Fastners</td><td><span class="status out">out</span></td><td><button class="btn">copy</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Supplier History <span class="pill">3 rows · 3 suppliers</span></div>
<table>
<thead><tr><th>Supplier</th><th class="num">Days ago</th><th class="num">Last cost</th><th class="num">×bought</th><th>Last PO</th><th></th></tr></thead>
<tbody>
<tr><td>Gracal Trading</td><td class="num">3</td><td class="num">R 6.22</td><td class="num">4</td><td class="mono">PO26080706STK</td><td><button class="btn primary">apply</button></td></tr>
<tr><td>Pro-Tech Fasteners</td><td class="num">3</td><td class="num">R 8.92</td><td class="num">2</td><td class="mono">PO26080705STK</td><td><button class="btn primary">apply</button></td></tr>
<tr><td>NSS Fastners</td><td class="num">3</td><td class="num">R 1.75</td><td class="num">2</td><td class="mono">PO26080704STK</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Price List <span class="pill">0 rows</span></div>
<div class="empty">No exact price list match — only M20 threaded rods exist, no M20 set screws.</div>
</div>
<div class="section">
<div class="section-title">Recent Quote <span class="pill">2 rows</span></div>
<table>
<thead><tr><th>Quote</th><th class="num">Days ago</th><th class="num">Qty</th><th class="num">Cost</th><th class="num">Sell</th><th>Description</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">Q000032</td><td class="num">14</td><td class="num">100</td><td class="num">R 10.00</td><td class="num">R 12.20</td><td>M20 x 30 DAVE</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">Q000121</td><td class="num">14</td><td class="num">160</td><td class="num">R 10.96</td><td class="num">R 14.25</td><td>M20 X 60MM HT SET SCREW HDG</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Recent Order <span class="pill">2 rows</span></div>
<table>
<thead><tr><th>Order</th><th class="num">Days ago</th><th class="num">Qty</th><th class="num">Sell</th><th>Status</th><th>Supplier</th><th>Description</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">1114938-OR</td><td class="num">11</td><td class="num">50</td><td class="num">R 2.12</td><td><span class="status delivered">delivered</span></td><td>NSS Fastners</td><td>M12 X 30 SET SCREW</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">WYNAND06/08/2026</td><td class="num">3</td><td class="num">50</td><td class="num">R 8.26</td><td><span class="status pending">collection_pool</span></td><td>NSS Fastners</td><td>M20 X 130 SET SCREW GR10.9 BLK</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
</div>
<!-- Item 1 — SAE 50 -->
<div class="popover panel-hidden" data-panel="1">
<div class="item-header">
<div>
<div class="item-desc">SAE 50 - 20L</div>
<div class="item-meta">quote_item · 0zbkoxfdlz0t7mh · qty 2 · no SKU</div>
</div>
</div>
<div class="summary">
in stock · <b>qty 2 @ R 990</b> (AFRIQ LUBE) · on price list · last order 11d ago
</div>
<div class="section">
<div class="section-title">Stock List <span class="pill">3 rows</span></div>
<table>
<thead><tr><th>SKU</th><th>Name</th><th class="num">Qty</th><th class="num">Cost</th><th>Supplier</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">SAE50SF/CF4-20</td><td>SAE 50 - 20L</td><td class="num">2</td><td class="num">R 990.00</td><td>AFRIQ LUBE</td><td><span class="status in_stock">in stock</span></td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">SAE40SFCF4-20</td><td>SAE 40 - 20L</td><td class="num">2</td><td class="num">R 963.00</td><td>AFRIQ LUBE</td><td><span class="status in_stock">in stock</span></td><td><button class="btn">copy</button></td></tr>
<tr><td class="mono">HYD68-20</td><td>HYDRAULIC 68 - 20L</td><td class="num">16</td><td class="num">R 748.00</td><td>AFRIQ LUBE</td><td><span class="status in_stock">in stock</span></td><td><button class="btn">copy</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Supplier History <span class="pill">1 row · 1 supplier</span></div>
<table>
<thead><tr><th>Supplier</th><th class="num">Days ago</th><th class="num">Last cost</th><th class="num">×bought</th><th>Last PO</th><th></th></tr></thead>
<tbody>
<tr><td>AFRIQ LUBE AND SERVICES CC</td><td class="num">10</td><td class="num">R 990.00</td><td class="num">5</td><td class="mono">PO26073109STK</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Price List <span class="pill">5 rows</span></div>
<table>
<thead><tr><th>SKU</th><th>Description</th><th class="num">Cost</th><th class="num">Sell</th><th>Supplier</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">AFRIQ-...SAE_50_SF_CF4-20LT</td><td>SAE 50 SF/CF4 - 20 LT</td><td class="num">R 879.00</td><td class="num">R 879.00</td><td>AFRIQ LUBE</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">AFRIQ-...SAE_50_SG_CF4-20LT</td><td>SAE 50 SG/CF4 - 20 LT</td><td class="num">R 859.00</td><td class="num">R 859.00</td><td>AFRIQ LUBE</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">AFRIQ-...SAE_50_SF_CF4-20X500ML</td><td>SAE 50 SF/CF4 - 20X500ML</td><td class="num">R 566.00</td><td class="num">R 566.00</td><td>AFRIQ LUBE</td><td><button class="btn">copy</button></td></tr>
<tr><td class="mono">AFRIQ-...SAE_50_SF_CF4-12X1LT</td><td>SAE 50 SF/CF4 - 12X1LT</td><td class="num">R 586.00</td><td class="num">R 586.00</td><td>AFRIQ LUBE</td><td><button class="btn">copy</button></td></tr>
<tr><td class="mono">AFRIQ-...SAE_50_SF_CF4-210LT</td><td>SAE 50 SF/CF4 - 210 LT</td><td class="num">R 8,594.00</td><td class="num">R 8,594.00</td><td>AFRIQ LUBE</td><td><button class="btn">copy</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Recent Quote <span class="pill">2 rows</span></div>
<table>
<thead><tr><th>Quote</th><th class="num">Days ago</th><th class="num">Qty</th><th class="num">Cost</th><th class="num">Sell</th><th>Description</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">RFQ000125</td><td class="num">14</td><td class="num">1</td><td class="num">R 1,687.24</td><td class="num">R 2,277.77</td><td>20L</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">Q007498</td><td class="num">14</td><td class="num">6</td><td class="num">R 434.00</td><td class="num">R 607.60</td><td>ANTIFREEZE 50% 20L</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Recent Order <span class="pill">2 rows</span></div>
<table>
<thead><tr><th>Order</th><th class="num">Days ago</th><th class="num">Qty</th><th class="num">Sell</th><th>Status</th><th>Supplier</th><th>Description</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">PO055945</td><td class="num">11</td><td class="num">2</td><td class="num">R 585.90</td><td><span class="status delivered">delivered</span></td><td>AFRIQ LUBE</td><td>20L ANTIFREEZE COOLANT — ALL SEASON 50%</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">13/05/2026 Johan</td><td class="num">11</td><td class="num">20</td><td class="num">R 597.50</td><td><span class="status delivered">delivered</span></td><td>AFRIQ LUBE</td><td>Antifreeze 50% All season 20L</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
</div>
<!-- Item 2 — Silicone clear -->
<div class="popover panel-hidden" data-panel="2">
<div class="item-header">
<div>
<div class="item-desc">Silicone sealant clear</div>
<div class="item-meta">quote_item · h5szkwdxs1u2quk · qty 12 · no SKU</div>
</div>
</div>
<div class="summary">
in stock · <b>qty 24 @ R 32.50</b> (Matus) · last quoted R 45.10 14d ago (Q000782, approved) · last order 11d ago
</div>
<div class="section">
<div class="section-title">Stock List <span class="pill">3 rows</span></div>
<table>
<thead><tr><th>SKU</th><th>Name</th><th class="num">Qty</th><th class="num">Cost</th><th>Supplier</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">SKU2-FE6EB3</td><td>SILICONE CLEAR</td><td class="num">24</td><td class="num">R 32.50</td><td>Matus - Impumemelo</td><td><span class="status in_stock">in stock</span></td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">SKU3-27031C</td><td>SILICONE WHITE</td><td class="num">24</td><td class="num">R 32.50</td><td>Matus - Impumemelo</td><td><span class="status in_stock">in stock</span></td><td><button class="btn">copy</button></td></tr>
<tr><td class="mono">SKU4-9FED0D</td><td>SILICONE HIGH HEAT H/R</td><td class="num">24</td><td class="num">R 106.50</td><td>Matus - Impumemelo</td><td><span class="status in_stock">in stock</span></td><td><button class="btn">copy</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Supplier History <span class="pill">1 row · 1 supplier</span></div>
<table>
<thead><tr><th>Supplier</th><th class="num">Days ago</th><th class="num">Last cost</th><th class="num">×bought</th><th>Last PO</th><th></th></tr></thead>
<tbody>
<tr><td>Matus - Impumemelo</td><td class="num">10</td><td class="num">R 32.50</td><td class="num">4</td><td class="mono">PO26073110STK</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Price List <span class="pill">0 rows</span></div>
<div class="empty">No price list match — catalogue only has silicone spray / Renov'Plastique, no clear sealant.</div>
</div>
<div class="section">
<div class="section-title">Recent Quote <span class="pill">2 rows</span></div>
<table>
<thead><tr><th>Quote</th><th class="num">Days ago</th><th class="num">Qty</th><th class="num">Cost</th><th class="num">Sell</th><th>Status</th><th>Description</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">Q000782</td><td class="num">14</td><td class="num">4</td><td class="num">R 30.45</td><td class="num">R 45.10</td><td><span class="status approved">approved</span></td><td>Clear silicone</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">Q000783</td><td class="num">14</td><td class="num">4</td><td class="num">R 30.45</td><td class="num">R 45.10</td><td><span class="status approved">approved</span></td><td>Clear silicone</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">Recent Order <span class="pill">2 rows</span></div>
<table>
<thead><tr><th>Order</th><th class="num">Days ago</th><th class="num">Qty</th><th class="num">Sell</th><th>Status</th><th>Supplier</th><th>Description</th><th></th></tr></thead>
<tbody>
<tr><td class="mono">MSP17006</td><td class="num">11</td><td class="num">8</td><td class="num">R 41.11</td><td><span class="status delivered">delivered</span></td><td>Fowkes Bros</td><td>SILICONE SEALANT CLEAR 260ML</td><td><button class="btn primary">apply</button></td></tr>
<tr><td class="mono">MSP17141</td><td class="num">11</td><td class="num">10</td><td class="num">R 40.50</td><td><span class="status delivered">delivered</span></td><td>CNL - IN STOCK</td><td>SERMAT Silicone Sealant Clear</td><td><button class="btn primary">apply</button></td></tr>
</tbody>
</table>
</div>
</div>
<div class="footer-note">
<strong>What this is.</strong> Static HTML mockup — the actual popover that will render in the app
uses the same JSON shape from <span class="mono">reports/item-intel-q202608094.json</span>.
<br><br>
<strong>Per-section limits (your spec):</strong>
<div class="legend">
<span><b>Stock List</b> — ≤ 3 fuzzy-matched stock items (sorted by description-fuzzy score)</span>
<span><b>Supplier History</b> — last order at every unique supplier, derived from <span class="mono">stock_purchase_items</span> joined to <span class="mono">stock_purchases</span></span>
<span><b>Price List</b> — ≤ 5 matches ranked by overlapScore against <span class="mono">search_text</span></span>
<span><b>Recent Quote</b> — ≤ 2 most recent matches</span>
<span><b>Recent Order</b> — ≤ 2 most recent matches</span>
</div>
<br>
<strong>Buttons:</strong>
<div class="legend">
<span><b>copy</b> — puts cost / supplier / date on the clipboard for manual paste</span>
<span><b>apply</b> — PATCHes the item's cost_price / selling_price / supplier / pricing_notes from this row</span>
</div>
<br>
<strong>Placement:</strong> Option A — replace the ⓘ popover content on the existing
<span class="mono">AiItemHistoryTrigger</span> (already wired into quote detail, approvals,
sprint, and orders sprint pages).
</div>
</div>
<script>
const tabs = document.querySelectorAll(".tab");
const panels = document.querySelectorAll("[data-panel]");
tabs.forEach((t) => {
t.addEventListener("click", () => {
const idx = t.dataset.tab;
tabs.forEach((x) => x.setAttribute("aria-selected", x === t ? "true" : "false"));
panels.forEach((p) => p.classList.toggle("panel-hidden", p.dataset.panel !== idx));
});
});
</script>
</body>
</html>