WFDASH-WEBAPP/.tmp/stock-flow-overview.html

590 lines
24 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>WFDASH Stock Flow — How It Works Today</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=DM+Serif+Display&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
:root {
--bg: #fafafa;
--card: #ffffff;
--line: #e7e5e0;
--line-soft: #f0eeea;
--ink: #14171e;
--ink-2: #3a4256;
--ink-3: #7c7a72;
--accent: #2f6f5e;
--accent-soft: rgba(47, 111, 94, 0.08);
--accent-line: rgba(47, 111, 94, 0.22);
--warn: #c97b3f;
--warn-soft: rgba(201, 123, 63, 0.10);
--danger: #b06367;
--danger-soft: rgba(176, 99, 103, 0.10);
--pos: #5a4ec9;
--pos-soft: rgba(90, 78, 201, 0.10);
--pos-line: rgba(90, 78, 201, 0.25);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--ink);
font-family: "Outfit", system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 64px 28px 96px; }
/* Header */
.eyebrow {
display: inline-flex; align-items: center; gap: 8px;
font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--accent); font-weight: 600;
padding: 6px 12px; background: var(--accent-soft);
border-radius: 999px; margin-bottom: 20px;
}
h1 {
font-family: "DM Serif Display", serif;
font-weight: 400;
font-size: 44px;
line-height: 1.1;
margin: 0 0 16px;
letter-spacing: -0.01em;
}
.lede { color: var(--ink-2); font-size: 18px; max-width: 720px; margin: 0 0 48px; }
.lede strong { color: var(--ink); font-weight: 600; }
/* Section */
section { margin: 64px 0; }
h2 {
font-size: 22px; font-weight: 600; margin: 0 0 8px;
display: flex; align-items: center; gap: 10px;
}
h2 svg { color: var(--accent); flex-shrink: 0; }
h2 .num {
display: inline-grid; place-items: center;
width: 26px; height: 26px; border-radius: 8px;
background: var(--accent); color: white;
font-size: 13px; font-weight: 600; font-family: "Outfit";
}
.sub { color: var(--ink-3); font-size: 14px; margin: 0 0 24px; }
/* Source of truth card */
.truth {
background: var(--card); border: 1px solid var(--line);
border-radius: 16px; padding: 24px 28px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.truth-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
}
@media (max-width: 760px) { .truth-grid { grid-template-columns: 1fr; } }
.truth h3 {
font-size: 16px; font-weight: 600; margin: 0 0 6px;
font-family: "JetBrains Mono", monospace; color: var(--accent);
}
.truth p { margin: 0 0 12px; color: var(--ink-2); font-size: 14px; }
.kv { display: grid; grid-template-columns: 130px 1fr; row-gap: 8px; column-gap: 16px; margin: 0; }
.kv dt { color: var(--ink-3); font-size: 13px; font-family: "JetBrains Mono", monospace; }
.kv dd { margin: 0; font-size: 13px; color: var(--ink); font-family: "JetBrains Mono", monospace; }
/* Path cards: stock IN and stock OUT */
.paths { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 760px) { .paths { grid-template-columns: 1fr; } }
.path {
background: var(--card); border: 1px solid var(--line);
border-radius: 16px; padding: 22px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.path.in { border-top: 3px solid var(--accent); }
.path.out { border-top: 3px solid var(--warn); }
.path h3 {
font-size: 14px; font-weight: 600; margin: 0 0 4px;
text-transform: uppercase; letter-spacing: 0.06em;
}
.path.in h3 { color: var(--accent); }
.path.out h3 { color: var(--warn); }
.path h4 { font-size: 16px; font-weight: 600; margin: 8px 0 4px; color: var(--ink); }
.path p { font-size: 13.5px; color: var(--ink-2); margin: 0 0 12px; }
.path .where {
font-family: "JetBrains Mono", monospace; font-size: 12px;
color: var(--ink-3); padding: 6px 10px;
background: var(--line-soft); border-radius: 6px;
display: inline-block; margin-top: 4px;
}
.flow {
display: flex; align-items: center; gap: 6px;
font-family: "JetBrains Mono", monospace; font-size: 11.5px;
color: var(--ink-2); flex-wrap: wrap; margin: 8px 0 4px;
}
.flow .pill {
background: var(--line-soft); padding: 3px 8px; border-radius: 5px;
}
.flow .arrow { color: var(--ink-3); }
/* POS section */
.pos-card {
background: var(--card); border: 1px solid var(--line);
border-radius: 16px; padding: 28px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
position: relative; overflow: hidden;
}
.pos-card::before {
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
background: var(--pos);
}
.pos-grid {
display: grid; grid-template-columns: 280px 1fr; gap: 32px; align-items: start;
}
@media (max-width: 760px) { .pos-grid { grid-template-columns: 1fr; } }
.pos-icon {
background: var(--pos-soft); border-radius: 14px;
aspect-ratio: 1; display: grid; place-items: center;
border: 1px solid var(--pos-line);
}
.pos-icon svg { width: 64px; height: 64px; color: var(--pos); }
.pos-meta { font-size: 14px; color: var(--ink-2); }
.pos-meta p { margin: 0 0 12px; }
.pos-meta .endpoint {
display: block; font-family: "JetBrains Mono", monospace; font-size: 12.5px;
color: var(--ink); padding: 6px 10px; background: var(--line-soft);
border-radius: 6px; margin: 4px 0;
}
.pos-meta .endpoint .verb {
color: var(--pos); font-weight: 600; margin-right: 8px;
}
/* Pricing triggers */
.triggers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
@media (max-width: 820px) { .triggers { grid-template-columns: 1fr; } }
.trigger {
background: var(--card); border: 1px solid var(--line);
border-radius: 14px; padding: 18px 20px;
}
.trigger .when {
font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--warn); margin-bottom: 8px;
}
.trigger h4 { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
.trigger p { font-size: 13.5px; color: var(--ink-2); margin: 0 0 10px; }
.trigger .set-by {
font-family: "JetBrains Mono", monospace; font-size: 11.5px;
color: var(--ink-3);
}
.trigger .set-by b { color: var(--ink); font-weight: 500; }
/* Mermaid */
.mermaid-wrap {
background: var(--card); border: 1px solid var(--line);
border-radius: 16px; padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
overflow-x: auto;
}
.mermaid { font-family: "Outfit", sans-serif !important; }
.mermaid .nodeLabel { font-family: "Outfit", sans-serif !important; font-size: 13px !important; }
/* Legend */
.legend {
display: flex; flex-wrap: wrap; gap: 18px;
margin-top: 20px; font-size: 13px; color: var(--ink-2);
}
.legend-item { display: flex; align-items: center; gap: 8px; }
.legend-swatch { width: 14px; height: 14px; border-radius: 4px; }
/* Code refs */
.ref {
font-family: "JetBrains Mono", monospace; font-size: 12px;
color: var(--ink-3);
}
.ref b { color: var(--ink); font-weight: 500; }
/* Footer */
footer {
margin-top: 80px; padding-top: 24px; border-top: 1px solid var(--line);
font-size: 13px; color: var(--ink-3);
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="eyebrow">WFDASH · Stock &amp; POS Architecture</div>
<h1>How stock works today — and how it talks to the POS.</h1>
<p class="lede">
There is one source of truth (<strong>stock_ledger</strong>) and one denormalised
cache (<strong>stock_items.current_qty</strong>). Every stock movement
— buying from a supplier, booking a surplus, fulfilling a customer order, ringing
up a sale at the till — writes a ledger row first, then mirrors the qty into the
cache. The POS app never touches the cache directly; it goes through three
authenticated API endpoints.
</p>
<!-- 1. Source of truth -->
<section>
<h2><span class="num">1</span> The single source of truth</h2>
<p class="sub">Two PocketBase collections carry the entire stock system. The
ledger is append-only and signed; the cache is denormalised for fast reads.</p>
<div class="truth">
<div class="truth-grid">
<div>
<h3>stock_items</h3>
<p>The catalogue. One row per SKU. Carries all the
pricing, supplier linkage, and the running quantity the UI reads.</p>
<dl class="kv">
<dt>sku</dt><dd>supplier's code</dd>
<dt>name / unit</dt><dd>display + uom</dd>
<dt>current_qty</dt><dd>denormalised cache</dd>
<dt>min_qty</dt><dd>low-stock trigger</dd>
<dt>target_qty</dt><dd>auto-reorder goal</dd>
<dt>cost_price</dt><dd>latest purchase cost</dd>
<dt>selling_price</dt><dd>POS shelf price</dd>
<dt>markup</dt><dd>POS markup %</dd>
<dt>cost_history</dt><dd>append-only list</dd>
<dt>supplier</dt><dd>FK → suppliers</dd>
<dt>is_active</dt><dd>soft-hide flag</dd>
</dl>
</div>
<div>
<h3>stock_ledger</h3>
<p>The audit trail. Every stock movement — in or out — is a row here
with a signed quantity and a reason. The cache is rebuildable
from this table alone.</p>
<dl class="kv">
<dt>stock_item</dt><dd>FK → stock_items</dd>
<dt>change_qty</dt><dd>signed (+ / −)</dd>
<dt>unit_cost</dt><dd>when known</dd>
<dt>reason</dt><dd>enum (see below)</dd>
<dt>reference_type</dt><dd>e.g. "order_item"</dd>
<dt>reference_id</dt><dd>FK to the source</dd>
<dt>notes</dt><dd>free text + audit tags</dd>
<dt>created_by</dt><dd>user FK</dd>
</dl>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-swatch" style="background: var(--accent);"></div>stock-in paths (qty &gt; 0)</div>
<div class="legend-item"><div class="legend-swatch" style="background: var(--warn);"></div>stock-out paths (qty &lt; 0)</div>
<div class="legend-item"><div class="legend-swatch" style="background: var(--pos);"></div>POS app surfaces</div>
</div>
</div>
</section>
<!-- 2. Stock IN paths -->
<section>
<h2><span class="num">2</span> How items get INTO stock</h2>
<p class="sub">Three entry points. All three end in
<span class="ref"><b>stock_ledger.reason ∈ {stock_order, purchase_surplus, adjustment}</b></span>.</p>
<div class="paths">
<div class="path in">
<h3>Path A</h3>
<h4>Receive a stock purchase (the accountant's flow)</h4>
<p>Warehouse marks the PO received
(<span class="ref"><b>stock_purchases.status = "received"</b></span>).
Accountant then hits "Book into stock" on the invoicing page.</p>
<div class="flow">
<span class="pill">stock_purchase.received</span>
<span class="arrow">→</span>
<span class="pill">bookStockFromInvoice()</span>
<span class="arrow">→</span>
<span class="pill">stock_ledger +qty</span>
<span class="arrow">→</span>
<span class="pill">stock_items.current_qty + qty</span>
<span class="arrow">→</span>
<span class="pill">booked_at stamped</span>
</div>
<div class="where">src/lib/server/stock-booking-service.ts · /api/v1/invoicing/book</div>
<p style="font-size: 12.5px; color: var(--ink-3); margin-top: 10px;">
Partial lines supported — each line carries its own
<span class="ref"><b>quantity_received</b></span> accumulator.
</p>
</div>
<div class="path in">
<h3>Path B</h3>
<h4>Book a surplus from a customer collection</h4>
<p>When the supplier delivers more than the customer ordered
(e.g. MOQ), the driver records <span class="ref"><b>qty_collected</b></span>
on the order item. Operator splits the difference into stock.</p>
<div class="flow">
<span class="pill">order_item.collected</span>
<span class="arrow">→</span>
<span class="pill">findMatchingStockItem()</span>
<span class="arrow">→</span>
<span class="pill">stock_ledger +surplus</span>
<span class="arrow">→</span>
<span class="pill">stock_items.current_qty + surplus</span>
</div>
<div class="where">src/lib/server/stock-surplus.ts · /api/v1/stock/surplus-book</div>
<p style="font-size: 12.5px; color: var(--ink-3); margin-top: 10px;">
Match by SKU → by supplier + normalised description. Operator can override.
</p>
</div>
<div class="path in">
<h3>Path C</h3>
<h4>Manual stocktake adjustment</h4>
<p>Operator counts the shelf, finds a discrepancy vs. the cache,
and posts a signed correction.</p>
<div class="flow">
<span class="pill">/stock/[id]</span>
<span class="arrow">→</span>
<span class="pill">stock_items.adjust</span>
<span class="arrow">→</span>
<span class="pill">stock_ledger ±qty (reason: adjustment)</span>
<span class="arrow">→</span>
<span class="pill">current_qty patched</span>
</div>
<div class="where">/api/v1/stock/items/[id]/adjust</div>
<p style="font-size: 12.5px; color: var(--ink-3); margin-top: 10px;">
Compensation tagging kicks in if the cache write fails after the ledger row lands.
</p>
</div>
</div>
</section>
<!-- 3. Stock OUT paths -->
<section>
<h2><span class="num">3</span> How items get OUT of stock</h2>
<p class="sub">Three exit points. All three write
<span class="ref"><b>stock_ledger.reason = "customer_fulfillment"</b></span> with a
negative quantity (or <b>"adjustment"</b> for write-offs).</p>
<div class="paths">
<div class="path out">
<h3>Path D</h3>
<h4>Fulfil a customer order from stock</h4>
<p>Operator on <span class="ref"><b>/orders/new</b></span> ticks "fulfil from stock"
for a line. Same route used on <span class="ref"><b>/orders/[id]/process</b></span>
to redirect an existing line.</p>
<div class="flow">
<span class="pill">order_item create</span>
<span class="arrow">→</span>
<span class="pill">item_status = "stock_booked"</span>
<span class="arrow">→</span>
<span class="pill">stock_ledger −qty</span>
<span class="arrow">→</span>
<span class="pill">current_qty − qty</span>
</div>
<div class="where">src/app/api/v1/stock/fulfill/route.ts</div>
<p style="font-size: 12.5px; color: var(--ink-3); margin-top: 10px;">
`stock_booked_from` back-link written on the order_item for audit + drill-down.
</p>
</div>
<div class="path out">
<h3>Path E</h3>
<h4>POS sale (the till)</h4>
<p>External WFSALES POS app (Flutter, separate repo) calls a
<span class="ref"><b>Bearer POS_API_KEY</b></span>-protected endpoint,
commits a sale, and decrements stock in one transaction.</p>
<div class="flow">
<span class="pill">till checkout</span>
<span class="arrow">→</span>
<span class="pill">pos_sales + pos_sale_items</span>
<span class="arrow">→</span>
<span class="pill">stock_ledger −qty (ref: pos_sale)</span>
<span class="arrow">→</span>
<span class="pill">current_qty − qty</span>
</div>
<div class="where">src/lib/server/pos-service.ts · /api/v1/pos/checkout</div>
<p style="font-size: 12.5px; color: var(--ink-3); margin-top: 10px;">
Idempotent on <span class="ref"><b>client_ref</b></span> — offline till retries
don't double-decrement.
</p>
</div>
<div class="path out">
<h3>Path F</h3>
<h4>Write-off / manual shrink</h4>
<p>Damaged in the warehouse, expired, lost, given to a sample kit.
Same <span class="ref"><b>/stock/[id]/adjust</b></span> route as Path C,
just a negative qty.</p>
<div class="flow">
<span class="pill">/stock/[id]</span>
<span class="arrow">→</span>
<span class="pill">stock_items.adjust (−n)</span>
<span class="arrow">→</span>
<span class="pill">stock_ledger reason: adjustment</span>
</div>
<div class="where">/api/v1/stock/items/[id]/adjust</div>
<p style="font-size: 12.5px; color: var(--ink-3); margin-top: 10px;">
One route handles both directions — the sign of the qty is the difference.
</p>
</div>
</div>
</section>
<!-- 4. POS integration -->
<section>
<h2><span class="num">4</span> How the POS app links in</h2>
<p class="sub">The WFSALES POS is a <b>separate</b> app (Flutter) that runs on the
office till. It only ever talks to WFDASH through three authenticated endpoints
under <span class="ref"><b>/api/v1/pos/*</b></span>. The auth is a
device-scoped bearer token (<span class="ref"><b>POS_API_KEY</b></span>) — distinct
from the agent's key, because the till only needs catalog + checkout + history.</p>
<div class="pos-card">
<div class="pos-grid">
<div class="pos-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3"/>
<path d="M7 8h10M7 12h6M7 16h4"/>
<circle cx="17" cy="16" r="2.5"/>
</svg>
</div>
<div class="pos-meta">
<p><b>Catalog pull</b> — at till boot, the POS calls the catalog endpoint and caches
the active stock list locally. This is what makes the offline queue possible.</p>
<span class="endpoint"><span class="verb">GET</span>/api/v1/pos/catalog</span>
<p style="margin-top: 14px;"><b>Checkout</b> — when the cashier hits "ring up", a single
POST creates the sale + line items + the negative ledger rows + decrements the cache.
Idempotent on <span class="ref"><b>client_ref</b></span> so a retry of a lost-response sale
doesn't double-decrement stock.</p>
<span class="endpoint"><span class="verb">POST</span>/api/v1/pos/checkout</span>
<p style="margin-top: 14px;"><b>History + cashup</b> — the till pulls today's sales for
cashup reconciliation and the cashier's shift close.</p>
<span class="endpoint"><span class="verb">GET</span>/api/v1/pos/sales?from=…&amp;to=…</span>
<p style="margin-top: 14px; margin-bottom: 0; font-size: 13px; color: var(--ink-3);">
<b>Categories</b> in the catalog come from server-side matching
(<span class="ref"><b>match_brands → match_supplier_tags → match_keywords</b></span> on the
<b>categories</b> collection), not from a relation on the stock row. Tills can read the
category icon straight off the response.
</p>
</div>
</div>
</div>
</section>
<!-- 5. When prices get set -->
<section>
<h2><span class="num">5</span> When do prices get set?</h2>
<p class="sub">Three pricing fields on a stock row —
<span class="ref"><b>cost_price</b></span>, <span class="ref"><b>selling_price</b></span>,
<span class="ref"><b>markup</b></span>. Each is set by a different trigger.</p>
<div class="triggers">
<div class="trigger">
<div class="when">Trigger 1 · Receive a stock purchase</div>
<h4>cost_price (auto)</h4>
<p>When the accountant books a stock purchase, the line's
<span class="ref"><b>cost_price</b></span> overwrites the stock row's
<b>cost_price</b> and appends an immutable entry to
<span class="ref"><b>cost_history</b></span>.</p>
<div class="set-by">Set by: <b>buildStockReceiptCostPatch()</b> in
stock-cost-service.ts</div>
</div>
<div class="trigger">
<div class="when">Trigger 2 · Manual edit on /stock</div>
<h4>selling_price + markup (manual)</h4>
<p>Operator edits the shelf price or markup % directly on the stock detail page.
<b>If cost changes,</b> a zero-qty ledger row is written as an audit record
(reason: <span class="ref"><b>adjustment</b></span>, ref: <b>manual</b>).</p>
<div class="set-by">Set by: <b>updateStockItemsPricing()</b> in
stock-pricing-service.ts</div>
</div>
<div class="trigger">
<div class="when">Trigger 3 · AI / Hermes agent</div>
<h4>selling_price (autonomous)</h4>
<p>External agent (Hermes) keeps the catalogue in sync with supplier price lists —
e.g. when a price list PDF re-baselines, it PATCHes the
<span class="ref"><b>selling_price</b></span> field through the agent API.</p>
<div class="set-by">Set by: <b>updateStockItem()</b> in
stock-items-service.ts (PATCH /api/v1/stock/items)</div>
</div>
</div>
</section>
<!-- 6. The graph -->
<section>
<h2><span class="num">6</span> The whole flow, in one picture</h2>
<p class="sub">Green = stock-in. Orange = stock-out. Purple = POS surfaces.
Everything terminates in the same ledger.</p>
<div class="mermaid-wrap">
<pre class="mermaid">
flowchart TB
classDef in fill:#e8f1ed,stroke:#2f6f5e,color:#14171e
classDef out fill:#faeede,stroke:#c97b3f,color:#14171e
classDef pos fill:#ede9f8,stroke:#5a4ec9,color:#14171e
classDef cache fill:#ffffff,stroke:#7c7a72,color:#14171e,stroke-dasharray: 4 3
classDef truth fill:#f4f1ea,stroke:#14171e,color:#14171e
subgraph SOURCES["Stock-in sources"]
direction TB
SP["stock_purchase<br/>(accountant books)"]:::in
SU["customer order<br/>(supplier over-delivers)"]:::in
ADJ_IN["manual adjustment<br/>(stocktake correction)"]:::in
end
subgraph OUTS["Stock-out destinations"]
direction TB
CO["customer order<br/>(fulfil from stock)"]:::out
PSALE["POS sale<br/>(WFSALES till)"]:::pos
ADJ_OUT["manual write-off<br/>(damage / loss)"]:::out
end
LEDGER[("stock_ledger<br/>(append-only, signed qty)")]:::truth
CACHE[("stock_items.current_qty<br/>(denormalised cache)")]:::cache
POS_APP["WFSALES POS app<br/>(Flutter, separate repo)"]:::pos
POS_APP -- GET /api/v1/pos/catalog --> CACHE
POS_APP -- POST /api/v1/pos/checkout --> PSALE
SP -- "bookStockFromInvoice<br/>reason: stock_order" --> LEDGER
SU -- "bookSurplus<br/>reason: purchase_surplus" --> LEDGER
ADJ_IN -- "stock_items.adjust<br/>reason: adjustment" --> LEDGER
CO -- "stock/fulfill<br/>reason: customer_fulfillment" --> LEDGER
PSALE -- "checkoutSale<br/>reason: customer_fulfillment<br/>ref: pos_sale" --> LEDGER
ADJ_OUT -- "stock_items.adjust<br/>reason: adjustment" --> LEDGER
LEDGER -. "update current_qty" .-> CACHE
WFDASH_UI["WFDASH /stock, /stock-orders,<br/>/orders/[id] (operator UI)"]
WFDASH_UI -- "reads" --> CACHE
WFDASH_UI -- "drills into" --> LEDGER
</pre>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-swatch" style="background: var(--accent);"></div>Stock-in to ledger</div>
<div class="legend-item"><div class="legend-swatch" style="background: var(--warn);"></div>Stock-out to ledger</div>
<div class="legend-item"><div class="legend-swatch" style="background: var(--pos);"></div>POS-app surfaces</div>
<div class="legend-item"><div class="legend-swatch" style="background: white; border: 1px dashed var(--ink-3);"></div>Derived cache (rebuildable from ledger)</div>
</div>
</section>
<footer>
<span>WFDASH codebase · paths referenced as <code>src/...</code> from the repo root</span>
<span>Generated for the WFDASH operator walkthrough</span>
</footer>
</div>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'base',
themeVariables: {
fontFamily: '"Outfit", system-ui, sans-serif',
fontSize: '13px',
primaryColor: '#ffffff',
primaryTextColor: '#14171e',
primaryBorderColor: '#e7e5e0',
lineColor: '#7c7a72',
secondaryColor: '#fafafa',
tertiaryColor: '#fafafa',
},
flowchart: { curve: 'basis', padding: 12 }
});
</script>
</body>
</html>