erp_be/docs/MODULE_DATA_FLOW.html

1462 lines
48 KiB
HTML
Raw 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.0" />
<title>ERP Backend — Module &amp; Data Flow Diagram</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
:root {
--bg: #f8fafc;
--surface: #ffffff;
--text: #1e293b;
--muted: #64748b;
--border: #e2e8f0;
--accent: #2563eb;
--accent-soft: #eff6ff;
--code-bg: #f1f5f9;
--sidebar-w: 280px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
color: var(--text);
background: var(--bg);
}
.layout {
display: flex;
min-height: 100vh;
}
nav.toc {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-w);
height: 100vh;
overflow-y: auto;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 1.25rem 1rem;
font-size: 0.875rem;
}
nav.toc h2 {
margin: 0 0 0.75rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
nav.toc a {
display: block;
padding: 0.35rem 0.5rem;
color: var(--text);
text-decoration: none;
border-radius: 4px;
}
nav.toc a:hover {
background: var(--accent-soft);
color: var(--accent);
}
main {
margin-left: var(--sidebar-w);
flex: 1;
max-width: 960px;
padding: 2.5rem 2rem 4rem;
}
h1 {
font-size: 2rem;
margin: 0 0 0.5rem;
line-height: 1.25;
}
.meta {
color: var(--muted);
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
}
.meta p { margin: 0.35rem 0; }
.meta a { color: var(--accent); }
h2 {
font-size: 1.5rem;
margin: 2.5rem 0 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
scroll-margin-top: 1rem;
}
h2:first-of-type {
border-top: none;
padding-top: 0;
margin-top: 0;
}
h3 {
font-size: 1.125rem;
margin: 1.5rem 0 0.75rem;
color: #334155;
}
p { margin: 0.75rem 0; }
ul { margin: 0.75rem 0; padding-left: 1.5rem; }
li { margin: 0.35rem 0; }
strong { font-weight: 600; }
code {
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
font-size: 0.875em;
background: var(--code-bg);
padding: 0.15em 0.4em;
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0 1.5rem;
font-size: 0.9rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
th, td {
padding: 0.65rem 0.85rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: #f1f5f9;
font-weight: 600;
}
tr:last-child td { border-bottom: none; }
.diagram {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
margin: 1rem 0 1.5rem;
overflow: hidden;
}
.diagram-toolbar {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
background: #f8fafc;
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.diagram-toolbar .spacer { flex: 1; }
.diagram-toolbar button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.diagram-toolbar button:hover {
background: var(--accent-soft);
border-color: #93c5fd;
color: var(--accent);
}
.diagram-toolbar button.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.diagram-zoom-label {
min-width: 3.5rem;
text-align: center;
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
user-select: none;
}
.diagram-viewport {
position: relative;
overflow: hidden;
height: min(70vh, 560px);
min-height: 280px;
background: #fafbfc;
touch-action: none;
user-select: none;
}
.diagram-viewport.can-pan { cursor: grab; }
.diagram-viewport.is-panning { cursor: grabbing; }
.diagram-stage {
position: absolute;
inset: 0;
overflow: hidden;
}
.diagram-canvas {
display: inline-block;
transform-origin: 0 0;
will-change: transform;
}
.diagram-canvas .mermaid,
.diagram-canvas svg {
display: block;
margin: 0;
max-width: none !important;
pointer-events: none;
}
.diagram-fullscreen {
display: none;
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(15, 23, 42, 0.92);
flex-direction: column;
}
.diagram-fullscreen.is-open {
display: flex;
}
.diagram-fullscreen-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: #1e293b;
color: #f8fafc;
border-bottom: 1px solid #334155;
flex-shrink: 0;
}
.diagram-fullscreen-title {
font-size: 0.95rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
}
.diagram-fullscreen-header .diagram-toolbar {
flex: 1;
background: transparent;
border: none;
padding: 0;
}
.diagram-fullscreen-header .diagram-toolbar button {
background: #334155;
border-color: #475569;
color: #f8fafc;
}
.diagram-fullscreen-header .diagram-toolbar button:hover {
background: #475569;
border-color: #64748b;
color: #fff;
}
.diagram-fullscreen-header .diagram-toolbar button.active {
background: var(--accent);
border-color: var(--accent);
}
.diagram-fullscreen-header .diagram-zoom-label {
color: #94a3b8;
}
.diagram-fullscreen-close {
min-width: 2.5rem !important;
font-size: 1.25rem !important;
line-height: 1;
}
.diagram-fullscreen-viewport {
flex: 1;
position: relative;
overflow: hidden;
background: #0f172a;
touch-action: none;
user-select: none;
}
.diagram-fullscreen-viewport.can-pan { cursor: grab; }
.diagram-fullscreen-viewport.is-panning { cursor: grabbing; }
.diagram-fullscreen-stage {
position: absolute;
inset: 0;
overflow: hidden;
}
.diagram-fullscreen-canvas {
display: inline-block;
transform-origin: 0 0;
will-change: transform;
background: var(--surface);
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35);
}
.diagram-fullscreen-canvas svg {
pointer-events: none;
}
.diagram-fullscreen-hint {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
font-size: 0.8rem;
color: #94a3b8;
pointer-events: none;
white-space: nowrap;
}
.note {
background: var(--accent-soft);
border-left: 4px solid var(--accent);
padding: 0.85rem 1rem;
margin: 1rem 0;
border-radius: 0 6px 6px 0;
font-size: 0.95rem;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 2rem 0;
}
@media (max-width: 900px) {
nav.toc { display: none; }
main { margin-left: 0; padding: 1.5rem 1rem 3rem; }
}
@media print {
nav.toc { display: none; }
main { margin-left: 0; max-width: 100%; }
.diagram { break-inside: avoid; }
.diagram-toolbar { display: none; }
.diagram-viewport { height: auto; min-height: 0; overflow: visible; }
.diagram-stage { position: static; overflow: visible; }
.diagram-canvas { transform: none !important; }
}
</style>
</head>
<body>
<div class="layout">
<nav class="toc">
<h2>Contents</h2>
<a href="#s1">1. System architecture</a>
<a href="#s2">2. API module map</a>
<a href="#s3">3. Request data flow</a>
<a href="#s4">4. Auth &amp; session</a>
<a href="#s5">5. Master dependencies</a>
<a href="#s6">6. Procurement → Asset</a>
<a href="#s7">7. Asset lifecycle</a>
<a href="#s8">8. AMC + renewal</a>
<a href="#s9">9. Insurance + renewal</a>
<a href="#s10">10. Service visits</a>
<a href="#s11">11. Depreciation</a>
<a href="#s12">12. Alerts</a>
<a href="#s13">13. Vendors</a>
<a href="#s14">14. Users &amp; roles</a>
<a href="#s15">15. Settings</a>
<a href="#s16">16. Cross-cutting</a>
<a href="#s17">17. Entity relationships</a>
<a href="#s18">18. Asset API reference</a>
<a href="#s19">19. FE screen mapping</a>
<a href="#s20">20. Renewal summary</a>
</nav>
<main>
<h1>ERP Backend — Module &amp; Data Flow Diagram</h1>
<div class="meta">
<p><strong>Stack:</strong> Express · PostgreSQL · Prisma · JWT + RBAC</p>
<p><strong>Base API:</strong> <code>/api/v1</code></p>
<p><strong>Phase 1 modules:</strong> Auth, Users, Roles, Masters (14), Vendors, Purchase Orders, GRN, Assets (+ AMC / Insurance / Service / Alerts / Depreciation), Settings</p>
<p>Reference: <a href="../BACKEND_TASKS.md">BACKEND_TASKS.md</a> · <a href="../BACKEND_SETUP.md">BACKEND_SETUP.md</a></p>
</div>
<h2 id="s1">1. High-level system architecture</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TB
subgraph Client["Frontend / API Client"]
FE[React App / Postman]
end
subgraph Gateway["Express App (src/app.js)"]
HELMET[Helmet + CORS + HPP]
RATE[Rate Limiter /api]
REQID[Request ID]
ROUTES["/api/v1 Routes"]
SWAGGER[Swagger /api-docs]
ERR[Error Middleware]
end
subgraph Middleware["Per-route chain"]
AUTH[authenticate JWT]
RBAC[authorize module + action]
VAL[validate Joi schema]
CTRL[Controller]
end
subgraph Services["Business Layer"]
SVC["*.service.js"]
REPO["*.repository.js PO/GRN/Assets"]
end
subgraph Data["PostgreSQL"]
PRISMA[(Prisma ORM)]
VIEWS[(SQL Views v_asset_*)]
AUDIT[(audit_logs)]
end
FE --> HELMET --> RATE --> REQID --> ROUTES
ROUTES --> AUTH --> RBAC --> VAL --> CTRL --> SVC
SVC --> PRISMA
SVC --> REPO --> PRISMA
SVC --> AUDIT
SVC --> VIEWS
CTRL --> ERR
</pre>
</div>
<h2 id="s2">2. Every API module map</h2>
<div class="diagram">
<pre class="mermaid">
flowchart LR
subgraph System
HEALTH[/health /healthz]
DOCS[/api-docs]
end
subgraph Security
AUTH_MOD["/auth login|refresh|logout|me"]
USERS["/users CRUD + export"]
ROLES["/roles CRUD + permissions matrix"]
end
subgraph Masters["/masters/* (14 sub-masters)"]
M1[uom]
M2[item-categories / subcategories / items]
M3[brands / gst-rates]
M4[payment-terms / delivery-terms]
M5[item-categories / subcategories]
M6[departments / designations]
M7[locations / plants / warehouses]
M8[document-series]
end
subgraph Procurement
VEND["/vendors + addresses|contacts|bank|items"]
PO["/purchase-orders workflow + PDF"]
GRN["/grn receipt + cancel + PDF"]
end
subgraph Assets["/assets"]
A_CORE[CRUD + transfer]
A_AMC[AMC contracts + renew]
A_SVC[service visits + status]
A_INS[insurance + renew]
A_DEPR[depreciation preview]
A_ALERT[expiry + service alerts]
end
subgraph Config
SET["/settings company + email SMTP"]
end
System --> Security --> Masters --> Procurement --> Assets --> Config
</pre>
</div>
<h2 id="s3">3. Standard request data flow (all protected routes)</h2>
<div class="diagram">
<pre class="mermaid">
sequenceDiagram
participant FE as Frontend
participant API as Express Route
participant AUTH as authenticate
participant RBAC as authorize
participant VAL as validate
participant SVC as Service
participant DB as PostgreSQL
participant AUD as audit_logs
FE->>API: HTTP + Bearer JWT
API->>AUTH: Verify token, load user + roles
AUTH->>RBAC: Check module permission (view/create/edit/delete/approve/export)
RBAC->>VAL: Joi validate body/query
VAL->>SVC: Business logic
SVC->>DB: Prisma read/write (deleted_at: null)
SVC->>AUD: auditLog on mutations
SVC-->>FE: ApiResponse { success, message, data, meta }
</pre>
</div>
<p><strong>RBAC modules:</strong> <code>USERS</code>, <code>ROLES</code>, <code>MASTERS</code>, <code>VENDOR</code>, <code>PURCHASE_ORDER</code>, <code>GRN</code>, <code>ASSET</code>, <code>SETTINGS</code></p>
<h2 id="s4">4. Auth &amp; session flow</h2>
<div class="diagram">
<pre class="mermaid">
stateDiagram-v2
[*] --> Login: POST /auth/login
Login --> Active: Valid credentials
Login --> Locked: MAX_LOGIN_ATTEMPTS exceeded
Login --> RateLimited: Too many failed logins (IP)
Active --> TokenIssued: accessToken 15m + refreshToken
TokenIssued --> APIAccess: Bearer on /api/v1/*
APIAccess --> Refresh: access expired
Refresh --> TokenIssued: POST /auth/refresh (rotate hash in DB)
APIAccess --> Logout: POST /auth/logout (revoke refresh)
Locked --> Login: After LOCKOUT_DURATION_MINUTES
</pre>
</div>
<table>
<thead><tr><th>Layer</th><th>Storage</th><th>Reset</th></tr></thead>
<tbody>
<tr><td>Global API rate limit</td><td>In-memory per IP</td><td>Restart server or wait 15 min</td></tr>
<tr><td>Login rate limit</td><td>In-memory per IP</td><td>Restart or wait 15 min</td></tr>
<tr><td>Account lockout</td><td><code>users.locked_until</code></td><td>Wait 30 min or DB reset</td></tr>
</tbody>
</table>
<h2 id="s5">5. Master data dependency graph</h2>
<p>Masters must exist <strong>before</strong> transactional modules use them.</p>
<div class="diagram">
<pre class="mermaid">
flowchart TD
DS[document_series] --> VCODE[vendor_code / po_number / grn_number / asset_code]
LOC[locations plants + warehouses] --> PO
LOC --> GRN
LOC --> ASSET
DEPT[departments] --> USERS
DEPT --> ASSET
DES[designations] --> USERS
UOM[uom] --> ITEMS
IC[item_categories] --> ISUB[item_subcategories] --> ITEMS
GST[gst_rates] --> ITEMS
BRANDS[brands] --> ITEMS
BRANDS --> PO
PT[payment_terms] --> VEND
PT --> PO
DT[delivery_terms] --> PO
ICAT[item_categories] --> ISUB[item_subcategories]
ISUB --> ITEMS[items]
ICAT --> ASSETS[assets]
ISUB --> ASSETS
ACAT --> ASSET
ACAT --> GRN_AUTO[GRN auto-asset creation]
ASUB --> ASSET
ASUB --> GRN_AUTO
ITEMS --> PO_ITEMS
ITEMS --> GRN_ITEMS
VEND --> PO
VEND --> GRN
VEND --> AMC
VEND --> SVC_VISIT
</pre>
</div>
<h2 id="s6">6. Procurement → Asset end-to-end flow (core business chain)</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
START([Setup Masters + Vendor]) --> PO_CREATE
PO_CREATE["POST /purchase-orders<br/>status: DRAFT"] --> PO_EDIT["PUT /purchase-orders/:id<br/>add line items"]
PO_EDIT --> PO_SUBMIT["POST .../submit<br/>→ PENDING_APPROVAL"]
PO_SUBMIT --> PO_APPROVE{"Approve?"}
PO_APPROVE -->|Yes| PO_APPROVED["status: APPROVED"]
PO_APPROVE -->|No| PO_REJECTED["status: REJECTED → edit & resubmit"]
PO_REJECTED --> PO_EDIT
PO_APPROVED --> GRN_CREATE["POST /grn<br/>receive against PO lines"]
subgraph GRN_TXN["GRN Transaction (repository)"]
G1[Create grn POSTED]
G2[Create grn_items per line]
G3[Increment PO item received_qty]
G4{is_asset_item?}
G5[Auto-create assets 1 per accepted qty]
G6[Recalculate PO status]
G1 --> G2 --> G3 --> G4
G4 -->|Yes| G5 --> G6
G4 -->|No| G6
end
GRN_CREATE --> GRN_TXN
G6 --> PO_STATUS{Receipt status}
PO_STATUS -->|partial| PARTIAL[PARTIALLY_RECEIVED]
PO_STATUS -->|full| FULL[FULLY_RECEIVED]
G5 --> MANUAL_ASSET["POST /assets manual create also allowed"]
PARTIAL --> GRN_CREATE
FULL --> ASSET_OPS[Asset lifecycle ops]
GRN_CANCEL["POST /grn/:id/cancel"] --> REV1[Decrement received_qty]
REV1 --> REV2[Soft-delete linked assets]
REV2 --> REV3[GRN status CANCELLED]
</pre>
</div>
<h3>PO status lifecycle</h3>
<div class="diagram">
<pre class="mermaid">
stateDiagram-v2
[*] --> DRAFT
DRAFT --> PENDING_APPROVAL: submit
REJECTED --> PENDING_APPROVAL: resubmit
PENDING_APPROVAL --> APPROVED: approve
PENDING_APPROVAL --> REJECTED: reject
APPROVED --> PARTIALLY_RECEIVED: GRN partial
APPROVED --> FULLY_RECEIVED: GRN full
PARTIALLY_RECEIVED --> FULLY_RECEIVED: more GRN
APPROVED --> CLOSED: amend closes old PO
DRAFT --> CANCELLED: cancel
APPROVED --> CANCELLED: cancel if no receipts
</pre>
</div>
<h3>GRN goods receiving concept flow</h3>
<p>End-to-end warehouse receipt: PO-backed GRN posting, optional supporting documents, PO qty update, and asset auto-creation.</p>
<div class="diagram">
<pre class="mermaid">
flowchart TD
subgraph Physical["Physical receipt at warehouse"]
P1[Vendor delivery arrives]
P2[Verify PO / invoice / LR / vehicle]
P3[Inspect qty quality batch]
end
subgraph System["System — GRN posting"]
S1["GET open PO<br/>status APPROVED / PARTIALLY_RECEIVED"]
S2["POST /grn<br/>header + line items"]
S3["GRN txn: grn POSTED"]
S4["grn_items per accepted/rejected qty"]
S5["PO received_qty += accepted_qty"]
S6{Item is_asset_item?}
S7["Auto-create assets<br/>1 per accepted unit"]
S8["Recalculate PO status"]
end
subgraph Docs["Supporting documents"]
D1["POST /grn/:grnId/attachments"]
D2["grn_attachments row"]
D3["File in uploads/grn/{grnId}/"]
D4["GET .../download authenticated"]
end
P1 --> P2 --> P3 --> S1 --> S2 --> S3 --> S4 --> S5 --> S6
S6 -->|Yes| S7 --> S8
S6 -->|No| S8
S3 --> D1 --> D2 --> D3 --> D4
S8 --> OUT1{More qty pending?}
OUT1 -->|Yes| PARTIAL[PO PARTIALLY_RECEIVED]
OUT1 -->|No| FULL[PO FULLY_RECEIVED]
</pre>
</div>
<table>
<thead><tr><th>Step</th><th>API / table</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>1</td><td><code>POST /grn</code></td><td>Requires APPROVED PO with pending qty</td></tr>
<tr><td>2</td><td><code>grn</code>, <code>grn_items</code></td><td><code>accepted_qty + rejected_qty = current_qty</code></td></tr>
<tr><td>3</td><td><code>purchase_order_items.received_qty</code></td><td>Incremented in same transaction</td></tr>
<tr><td>4</td><td><code>assets</code></td><td>When <code>items.is_asset_item = true</code></td></tr>
<tr><td>5</td><td><code>POST /grn/:grnId/attachments</code></td><td>Invoice PDF, LR copy, photos</td></tr>
<tr><td>6</td><td><code>GET .../attachments/:id/download</code></td><td>RBAC download — not public <code>/uploads</code></td></tr>
</tbody>
</table>
<h3>GRN auto-asset creation (when <code>items.is_asset_item = true</code>)</h3>
<table>
<thead><tr><th>Field on Asset</th><th>Source</th></tr></thead>
<tbody>
<tr><td><code>asset_code</code></td><td><code>document_series</code> via <code>ASSET_{category.code}</code></td></tr>
<tr><td><code>asset_name</code></td><td>Item name (+ <code>#N</code> if qty &gt; 1)</td></tr>
<tr><td><code>item_category_id</code> / <code>item_subcategory_id</code></td><td>From item master (optional GRN line override)</td></tr>
<tr><td><code>plant_id</code></td><td>From PO</td></tr>
<tr><td><code>warehouse_id</code>, <code>vendor_id</code>, <code>po_id</code>, <code>grn_id</code>, <code>grn_item_id</code></td><td>From GRN</td></tr>
<tr><td><code>purchase_date</code></td><td><code>grn.grn_date</code></td></tr>
<tr><td><code>purchase_cost</code></td><td><code>grn_items.rate</code></td></tr>
<tr><td><code>useful_life_years</code>, <code>depreciation_method</code></td><td>From <code>item_categories</code> defaults</td></tr>
<tr><td><code>condition</code></td><td><code>NEW</code></td></tr>
<tr><td><code>status</code></td><td><code>IN_USE</code></td></tr>
</tbody>
</table>
<h2 id="s7">7. Asset module — full lifecycle</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TB
subgraph Create["Asset Creation Paths"]
P1[Manual POST /assets]
P2[Auto from GRN receipt]
end
subgraph Validate["normalizeAssetPayload validations"]
V1[item_category + subcategory match]
V2[plant / dept / warehouse / user refs]
V3[vendor / PO / GRN / grn_item refs]
V4[disposal_date required if DISPOSED/SCRAPPED]
V5[depreciation_rate required if method=OTHER]
V6[resolve rate from category defaults]
end
Create --> Validate --> SAVE[(assets table)]
SAVE --> CODE[asset_code from document_series]
SAVE --> READ["GET /assets/:id<br/>includes depreciation summary"]
SAVE --> TRANSFER["POST /assets/:id/transfer"]
TRANSFER --> TH[(asset_transfers)]
TRANSFER --> UPDATE_LOC[Update plant/dept/user/warehouse on asset]
SAVE --> UPDATE["PUT /assets/:id"]
SAVE --> DELETE["DELETE /assets/:id soft delete"]
UPDATE --> DISPOSE{status DISPOSED/SCRAPPED?}
DISPOSE -->|Yes| NEED_DATE[disposal_date required]
</pre>
</div>
<h3>Asset status &amp; condition enums</h3>
<table>
<thead><tr><th>Field</th><th>Values</th></tr></thead>
<tbody>
<tr><td><code>status</code></td><td><code>IN_USE</code>, <code>IDLE</code>, <code>UNDER_MAINTENANCE</code>, <code>DISPOSED</code>, <code>SCRAPPED</code></td></tr>
<tr><td><code>condition</code></td><td><code>NEW</code>, <code>GOOD</code>, <code>FAIR</code>, <code>POOR</code></td></tr>
</tbody>
</table>
<h2 id="s8">8. AMC contracts flow + renewal concept</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
A[(assets)] --> AMC_LIST["GET /assets/:id/amc"]
A --> AMC_CREATE["POST /assets/:id/amc"]
AMC_CREATE --> CHECK1{is_active=true?}
CHECK1 -->|Yes| DEACT[Deactivate other active AMC for same asset]
CHECK1 --> AMC_ROW[(asset_amc_contracts)]
DEACT --> AMC_ROW
AMC_ROW --> FIELDS["vendor_id, contract_type, start/end dates,<br/>renewal_date, annual_cost, visits_per_year, etc."]
AMC_ROW --> AMC_UPDATE["PUT /assets/:id/amc/:contractId"]
AMC_ROW --> AMC_RENEW["PATCH .../renew"]
subgraph RENEW_FLOW["Renewal Flow (AMC)"]
R1[Load existing contract]
R2[Set old contract is_active=false]
R3[Create NEW contract row with new dates]
R4[New contract is_active=true]
R5[auditLog action=RENEW]
R1 --> R2 --> R3 --> R4 --> R5
end
AMC_RENEW --> RENEW_FLOW
AMC_ROW --> SVC_LINK["Service visits can link amc_contract_id"]
</pre>
</div>
<div class="note">
<strong>Key rule:</strong> Only one active AMC per asset (<code>deactivateOtherActive</code>).<br>
<strong>Renewal ≠ update dates on same row</strong> — renewal creates a new contract record and deactivates the old one (history preserved).
</div>
<h2 id="s9">9. Insurance policies flow + renewal concept</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
A[(assets)] --> INS_LIST["GET /assets/:id/insurance"]
A --> INS_CREATE["POST /assets/:id/insurance"]
INS_CREATE --> CHECK{is_active=true?}
CHECK -->|Yes| DEACT[Deactivate other active policies]
CHECK --> POL[(asset_insurance_policies)]
DEACT --> POL
POL --> FIELDS["policy_no, insurer, sum_insured,<br/>annual_premium, start/end dates,<br/>renewal_date, is_auto_renewal, premium_paid"]
POL --> INS_UPDATE["PUT /assets/:id/insurance/:policyId"]
POL --> INS_RENEW["PATCH .../renew"]
subgraph RENEW_INS["Renewal Flow (Insurance)"]
I1[Deactivate old policy]
I2[Create new policy with new dates]
I3[premium_paid defaults false on renew]
I4[auditLog action=RENEW]
I1 --> I2 --> I3 --> I4
end
INS_RENEW --> RENEW_INS
</pre>
</div>
<p>Same pattern as AMC: <strong>one active policy</strong>, renewal = <strong>new row + deactivate old</strong>.</p>
<h2 id="s10">10. Service visits flow</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
A[(assets)] --> SV_LIST["GET /assets/:id/service-visits"]
A --> SV_CREATE["POST /assets/:id/service-visits"]
SV_CREATE --> VAL_SV["Validate amc_contract_id belongs to asset<br/>optional vendor_id ref"]
VAL_SV --> SV_ROW[(asset_service_visits)]
SV_ROW --> FIELDS["visit_type, visit_date, complaint details,<br/>engineer, work_done, parts_replaced,<br/>next_service_date, downtime_hours,<br/>service_cost, is_under_amc,<br/>asset_condition_after"]
SV_ROW --> SV_UPDATE["PUT .../service-visits/:visitId"]
SV_ROW --> SV_STATUS["PATCH .../status<br/>SCHEDULED|IN_PROGRESS|COMPLETED|etc."]
SV_ROW --> ALERT_VIEW["Feeds v_asset_next_service view"]
</pre>
</div>
<h3>Visit types</h3>
<p><code>PREVENTIVE</code>, <code>BREAKDOWN</code>, <code>INSPECTION</code>, <code>INSTALLATION</code>, <code>CALIBRATION</code>, <code>OTHER</code></p>
<h3>Link to AMC</h3>
<ul>
<li><code>amc_contract_id</code> optional but validated against the asset</li>
<li><code>is_under_amc</code> flag for cost tracking</li>
<li><code>next_service_date</code> drives <strong>service alerts</strong></li>
</ul>
<h2 id="s11">11. Depreciation flow</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
subgraph Inputs
CAT[item_categories defaults]
FORM[User form fields]
end
CAT --> |default_useful_life_years<br/>default_depreciation_method| CREATE
FORM --> CREATE["POST /assets or PUT /assets/:id"]
FORM --> PREVIEW["POST /assets/depreciation/calculate"]
subgraph Methods["depreciation_method"]
SLM[SLM Straight Line]
WDV[WDV Written Down Value]
OTHER[OTHER manual rate required]
end
PREVIEW --> CALC[calculateDepreciation]
CREATE --> STORE[(assets: method, rate, cost, salvage, life, purchase_date)]
STORE --> READ["GET /assets/:id"]
READ --> CALC
CALC --> OUT["annual_depreciation<br/>accumulated_depreciation<br/>book_value<br/>years_elapsed<br/>resolved depreciation_rate"]
SLM --> CALC
WDV --> CALC
OTHER --> CALC
</pre>
</div>
<table>
<thead><tr><th>Method</th><th>Rate auto-calc?</th><th>Formula concept</th></tr></thead>
<tbody>
<tr><td><strong>SLM</strong></td><td>Yes (if rate omitted)</td><td>Equal yearly depreciation on original cost</td></tr>
<tr><td><strong>WDV</strong></td><td>Yes (if rate omitted)</td><td>Depreciation on reducing book value year-by-year</td></tr>
<tr><td><strong>OTHER</strong></td><td>No — user must send <code>depreciation_rate</code></td><td>Custom % on cost</td></tr>
</tbody>
</table>
<p><strong>FE usage:</strong></p>
<ul>
<li><strong>Live preview</strong><code>POST /assets/depreciation/calculate</code> (no save)</li>
<li><strong>Saved asset view</strong><code>GET /assets/:id</code><code>data.depreciation</code> object</li>
<li><strong>Method dropdown</strong><code>GET /assets/depreciation-methods</code></li>
</ul>
<h2 id="s12">12. Alerts flow (cross-asset dashboards)</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
subgraph Sources
AMC[(asset_amc_contracts end_date)]
INS[(asset_insurance_policies policy_end_date)]
WAR[(assets warranty_expiry_date)]
SVC[(asset_service_visits next_service_date)]
end
AMC --> V1[v_asset_expiry_alerts]
INS --> V1
WAR --> V1
SVC --> V2[v_asset_next_service]
V1 --> API1["GET /assets/alerts/expiry<br/>?days=30&type=AMC|INSURANCE|WARRANTY"]
V2 --> API2["GET /assets/alerts/service<br/>?status=OVERDUE|DUE_THIS_WEEK|..."]
V1 --> LEVELS["EXPIRED / CRITICAL / WARNING / INFO"]
V2 --> SSTAT["OVERDUE / DUE_THIS_WEEK / DUE_THIS_MONTH / UPCOMING"]
</pre>
</div>
<p><strong>Requires DB views:</strong> run <code>scripts/patch-assets-amc-insurance.sql</code> + <code>scripts/patch-assets-views.sql</code></p>
<h2 id="s13">13. Vendor module data flow</h2>
<div class="diagram">
<pre class="mermaid">
flowchart LR
V[(vendors)] --> ADDR[vendor_addresses]
V --> CONT[vendor_contacts]
V --> BANK[vendor_bank_details AES encrypted]
V --> MAP[vendor_item_mapping]
V --> PO[purchase_orders]
V --> GRN[grn]
V --> AMC[asset_amc_contracts]
V --> SVC[asset_service_visits]
MAP --> ITEMS[items preferred vendor rates]
</pre>
</div>
<h2 id="s14">14. Users &amp; roles permission flow</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
U[(users)] --> UR[user_roles]
UR --> R[(roles)]
R --> RP[role_permissions]
RP --> P[(permissions)]
P --> M[(modules)]
U --> PO_APPROVE[PO approve/reject]
U --> ASSET_OPS[Asset CRUD/transfer]
U --> AUDIT[performed_by in audit_logs]
LOGIN["GET /auth/me"] --> PERM[Returns user + flat permissions list for FE menu/RBAC]
</pre>
</div>
<h2 id="s15">15. Settings module</h2>
<div class="diagram">
<pre class="mermaid">
flowchart LR
CO[(company singleton)] --> PDF[PO/GRN/Asset PDF headers]
EM[(email_settings SMTP encrypted)] --> MAIL[Future email notifications]
LOGO["POST /settings/company/logo"] --> UPLOADS[(uploads/)]
</pre>
</div>
<h2 id="s16">16. Cross-cutting concerns (every module)</h2>
<div class="diagram">
<pre class="mermaid">
flowchart TD
M[Every mutation] --> AUDIT[auditLog table_name, record_id, action, old/new JSON]
M --> SOFT[Soft delete deleted_at = now]
M --> USER[created_by / updated_by from req.user.id]
M --> REQ[request_id from middleware]
DOC[Document numbers] --> SERIES[document_series atomic increment]
SERIES --> VC[vendor_code]
SERIES --> PO_NUM[po_number]
SERIES --> GRN_NUM[grn_number]
SERIES --> ASSET_CODE[asset_code per category prefix]
PII[mobile, bank accounts] --> ENC[AES-256-GCM + HMAC blind index]
</pre>
</div>
<h2 id="s17">17. Complete entity relationship (simplified)</h2>
<div class="diagram">
<pre class="mermaid">
erDiagram
vendors ||--o{ purchase_orders : supplies
vendors ||--o{ grn : delivers
vendors ||--o{ asset_amc_contracts : maintains
vendors ||--o{ asset_service_visits : services
purchase_orders ||--o{ purchase_order_items : contains
purchase_orders ||--o{ grn : received_via
purchase_orders ||--o{ assets : sourced_from
grn ||--o{ grn_items : lines
grn ||--o{ grn_attachments : documents
grn_items ||--o{ assets : auto_created
items ||--o{ purchase_order_items : ordered
items ||--o{ grn_items : received
item_categories ||--o{ item_subcategories : has
item_categories ||--o{ items : classifies
item_categories ||--o{ assets : classifies
item_subcategories ||--o{ items : sub_classifies
item_subcategories ||--o{ assets : sub_classifies
assets ||--o{ asset_amc_contracts : has
assets ||--o{ asset_insurance_policies : insured_by
assets ||--o{ asset_service_visits : serviced
assets ||--o{ asset_transfers : moved
assets ||--o{ asset_attachments : files
asset_amc_contracts ||--o{ asset_service_visits : covers
locations ||--o{ assets : plant_warehouse
departments ||--o{ assets : assigned_dept
users ||--o{ assets : assigned_user
users ||--o{ user_roles : has
roles ||--o{ role_permissions : grants
permissions }o--|| modules : belongs_to
</pre>
</div>
<h2 id="s18">18. Asset sub-module API quick reference</h2>
<table>
<thead><tr><th>Area</th><th>Endpoints</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Core</td><td><code>GET/POST /assets</code>, <code>GET/PUT/DELETE /assets/:id</code></td><td>Includes computed <code>depreciation</code> on read</td></tr>
<tr><td>Transfer</td><td><code>POST /assets/:id/transfer</code>, <code>GET .../transfer-history</code></td><td>Blocked if DISPOSED/SCRAPPED</td></tr>
<tr><td>AMC</td><td><code>GET/POST /assets/:id/amc</code>, <code>GET/PUT .../:contractId</code>, <code>PATCH .../renew</code></td><td>One active contract</td></tr>
<tr><td>Service</td><td><code>GET/POST /assets/:id/service-visits</code>, <code>PUT</code>, <code>PATCH .../status</code></td><td>Links optional AMC</td></tr>
<tr><td>Insurance</td><td><code>GET/POST /assets/:id/insurance</code>, <code>GET/PUT</code>, <code>PATCH .../renew</code></td><td>One active policy</td></tr>
<tr><td>Depreciation</td><td><code>GET /depreciation-methods</code>, <code>POST /depreciation/calculate</code></td><td>Preview only</td></tr>
<tr><td>Alerts</td><td><code>GET /alerts/expiry</code>, <code>GET /alerts/service</code></td><td>SQL views</td></tr>
</tbody>
</table>
<h2 id="s19">19. Typical FE screen → API mapping</h2>
<table>
<thead><tr><th>Screen</th><th>APIs used</th></tr></thead>
<tbody>
<tr><td>Login</td><td><code>POST /auth/login</code>, <code>GET /auth/me</code></td></tr>
<tr><td>Master setup</td><td><code>/masters/*</code> CRUD</td></tr>
<tr><td>Vendor management</td><td><code>/vendors/*</code></td></tr>
<tr><td>Create PO</td><td>Masters dropdowns + <code>POST /purchase-orders</code></td></tr>
<tr><td>Approve PO</td><td><code>POST /purchase-orders/:id/approve</code></td></tr>
<tr><td>GRN receipt</td><td><code>POST /grn</code> (auto assets for asset items)</td></tr>
<tr><td>Asset list/detail</td><td><code>GET /assets</code>, <code>GET /assets/:id</code></td></tr>
<tr><td>Asset create/edit form</td><td>Masters + <code>POST /depreciation/calculate</code> (preview) + <code>POST/PUT /assets</code></td></tr>
<tr><td>AMC tab</td><td><code>/assets/:id/amc/*</code> + renew</td></tr>
<tr><td>Insurance tab</td><td><code>/assets/:id/insurance/*</code> + renew</td></tr>
<tr><td>Service log</td><td><code>/assets/:id/service-visits/*</code></td></tr>
<tr><td>Dashboard alerts</td><td><code>/assets/alerts/expiry</code>, <code>/assets/alerts/service</code></td></tr>
<tr><td>Transfer asset</td><td><code>POST /assets/:id/transfer</code></td></tr>
</tbody>
</table>
<h2 id="s20">20. Renewal concept summary (for FE)</h2>
<table>
<thead><tr><th>Entity</th><th>What "Renew" does</th><th>Old record</th><th>New record</th></tr></thead>
<tbody>
<tr>
<td><strong>AMC</strong></td>
<td><code>PATCH /assets/:id/amc/:contractId/renew</code></td>
<td><code>is_active = false</code></td>
<td>New row, <code>is_active = true</code>, new dates</td>
</tr>
<tr>
<td><strong>Insurance</strong></td>
<td><code>PATCH /assets/:id/insurance/:policyId/renew</code></td>
<td><code>is_active = false</code></td>
<td>New row, <code>is_active = true</code>, new policy period</td>
</tr>
</tbody>
</table>
<p>Both preserve <strong>full history</strong> — never overwrite the old contract/policy row.</p>
<hr />
<p class="meta">Generated from <a href="MODULE_DATA_FLOW.md">MODULE_DATA_FLOW.md</a> · Open in any browser · Mermaid diagrams require network for CDN</p>
</main>
</div>
<div id="diagram-fullscreen" class="diagram-fullscreen" aria-hidden="true">
<div class="diagram-fullscreen-header">
<span id="diagram-fullscreen-title" class="diagram-fullscreen-title">Diagram</span>
<div class="diagram-toolbar" id="diagram-fullscreen-toolbar"></div>
<button type="button" class="diagram-fullscreen-close" id="diagram-fullscreen-close" title="Exit fullscreen (Esc)">×</button>
</div>
<div class="diagram-fullscreen-viewport" id="diagram-fullscreen-viewport">
<div class="diagram-fullscreen-stage" id="diagram-fullscreen-stage">
<div class="diagram-fullscreen-canvas" id="diagram-fullscreen-canvas"></div>
</div>
</div>
<div class="diagram-fullscreen-hint">Drag to move · Scroll or Ctrl+scroll to zoom · + / / 0 keys · Esc to exit</div>
</div>
<script>
const ZOOM_MIN = 0.2;
const ZOOM_MAX = 5;
const ZOOM_STEP = 0.15;
const ZOOM_DEFAULT = 1;
function clampZoom(value) {
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(value * 100) / 100));
}
function createToolbar(onAction) {
const toolbar = document.createElement('div');
toolbar.className = 'diagram-toolbar';
toolbar.innerHTML = `
<button type="button" data-action="zoom-out" title="Zoom out (-)"></button>
<span class="diagram-zoom-label" data-zoom-label>100%</span>
<button type="button" data-action="zoom-in" title="Zoom in (+)">+</button>
<button type="button" data-action="zoom-reset" title="Reset view (0)">Reset</button>
<button type="button" data-action="zoom-fit" title="Fit to view">Fit</button>
<span class="spacer"></span>
<button type="button" data-action="fullscreen" title="Fullscreen (F)">⛶ Fullscreen</button>
`;
toolbar.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action]');
if (btn) onAction(btn.dataset.action);
});
return toolbar;
}
class DiagramViewer {
constructor({ viewport, stage, canvas, label, wheelNeedsModifier = true }) {
this.viewport = viewport;
this.stage = stage;
this.canvas = canvas;
this.label = label;
this.wheelNeedsModifier = wheelNeedsModifier;
this.zoom = ZOOM_DEFAULT;
this.panX = 0;
this.panY = 0;
this.isPanning = false;
this.pointerId = null;
this.lastPointer = { x: 0, y: 0 };
this.bindEvents();
}
render() {
this.canvas.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoom})`;
if (this.label) {
this.label.textContent = `${Math.round(this.zoom * 100)}%`;
}
}
getViewportPoint(clientX, clientY) {
const rect = this.stage.getBoundingClientRect();
return { x: clientX - rect.left, y: clientY - rect.top };
}
zoomAt(clientX, clientY, delta) {
const point = this.getViewportPoint(clientX, clientY);
const prevZoom = this.zoom;
const nextZoom = clampZoom(prevZoom + delta);
if (nextZoom === prevZoom) return;
this.panX = point.x - ((point.x - this.panX) * nextZoom) / prevZoom;
this.panY = point.y - ((point.y - this.panY) * nextZoom) / prevZoom;
this.zoom = nextZoom;
this.render();
}
zoomBy(delta, centerToStage = true) {
const rect = this.stage.getBoundingClientRect();
const x = centerToStage ? rect.left + rect.width / 2 : rect.left;
const y = centerToStage ? rect.top + rect.height / 2 : rect.top;
this.zoomAt(x, y, delta);
}
reset() {
this.zoom = ZOOM_DEFAULT;
this.panX = 0;
this.panY = 0;
this.render();
this.centerContent();
}
fitToView() {
const svg = this.canvas.querySelector('svg');
if (!svg) {
this.reset();
return;
}
const stageRect = this.stage.getBoundingClientRect();
const bbox = svg.getBBox();
const pad = 24;
const scaleX = (stageRect.width - pad * 2) / bbox.width;
const scaleY = (stageRect.height - pad * 2) / bbox.height;
const fitZoom = clampZoom(Math.min(scaleX, scaleY, 1));
this.zoom = fitZoom;
this.panX = (stageRect.width - bbox.width * fitZoom) / 2 - bbox.x * fitZoom;
this.panY = (stageRect.height - bbox.height * fitZoom) / 2 - bbox.y * fitZoom;
this.render();
}
centerContent() {
const svg = this.canvas.querySelector('svg');
if (!svg) return;
const stageRect = this.stage.getBoundingClientRect();
const bbox = svg.getBBox();
this.panX = (stageRect.width - bbox.width * this.zoom) / 2 - bbox.x * this.zoom;
this.panY = (stageRect.height - bbox.height * this.zoom) / 2 - bbox.y * this.zoom;
this.render();
}
handleAction(action) {
switch (action) {
case 'zoom-in':
this.zoomBy(ZOOM_STEP);
break;
case 'zoom-out':
this.zoomBy(-ZOOM_STEP);
break;
case 'zoom-reset':
this.reset();
break;
case 'zoom-fit':
this.fitToView();
break;
default:
break;
}
}
bindEvents() {
this.viewport.classList.add('can-pan');
this.viewport.addEventListener('pointerdown', (e) => {
if (e.button !== 0) return;
if (e.target.closest('button')) return;
this.isPanning = true;
this.pointerId = e.pointerId;
this.lastPointer = { x: e.clientX, y: e.clientY };
this.viewport.classList.add('is-panning');
this.viewport.setPointerCapture(e.pointerId);
e.preventDefault();
});
this.viewport.addEventListener('pointermove', (e) => {
if (!this.isPanning || e.pointerId !== this.pointerId) return;
this.panX += e.clientX - this.lastPointer.x;
this.panY += e.clientY - this.lastPointer.y;
this.lastPointer = { x: e.clientX, y: e.clientY };
this.render();
e.preventDefault();
});
const endPan = (e) => {
if (!this.isPanning || (e.pointerId !== undefined && e.pointerId !== this.pointerId)) return;
this.isPanning = false;
this.pointerId = null;
this.viewport.classList.remove('is-panning');
if (this.viewport.hasPointerCapture(e.pointerId)) {
this.viewport.releasePointerCapture(e.pointerId);
}
};
this.viewport.addEventListener('pointerup', endPan);
this.viewport.addEventListener('pointercancel', endPan);
this.viewport.addEventListener('lostpointercapture', () => {
this.isPanning = false;
this.pointerId = null;
this.viewport.classList.remove('is-panning');
});
this.viewport.addEventListener(
'wheel',
(e) => {
if (this.wheelNeedsModifier && !e.ctrlKey && !e.metaKey) return;
e.preventDefault();
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
this.zoomAt(e.clientX, e.clientY, delta);
},
{ passive: false }
);
}
}
const diagramViewers = [];
let fullscreenViewer = null;
let fullscreenToolbarReady = false;
function openFullscreen(sourceViewer) {
const overlay = document.getElementById('diagram-fullscreen');
const viewport = document.getElementById('diagram-fullscreen-viewport');
const stage = document.getElementById('diagram-fullscreen-stage');
const canvas = document.getElementById('diagram-fullscreen-canvas');
const title = document.getElementById('diagram-fullscreen-title');
const toolbarHost = document.getElementById('diagram-fullscreen-toolbar');
title.textContent = sourceViewer.title || 'Diagram';
canvas.innerHTML = sourceViewer.canvas.innerHTML;
if (!fullscreenToolbarReady) {
const toolbar = createToolbar((action) => {
if (action === 'fullscreen') return;
if (fullscreenViewer) fullscreenViewer.handleAction(action);
});
toolbarHost.appendChild(toolbar);
fullscreenToolbarReady = true;
}
if (!fullscreenViewer) {
fullscreenViewer = new DiagramViewer({
viewport,
stage,
canvas,
label: toolbarHost.querySelector('[data-zoom-label]'),
wheelNeedsModifier: false,
});
}
fullscreenViewer.fitToView();
overlay.classList.add('is-open');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function closeFullscreen() {
const overlay = document.getElementById('diagram-fullscreen');
overlay.classList.remove('is-open');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
fullscreenViewer = null;
}
function wrapDiagrams() {
document.querySelectorAll('.diagram').forEach((diagram, index) => {
let title = `Diagram ${index + 1}`;
const prevHeading = diagram.previousElementSibling;
if (prevHeading && (prevHeading.tagName === 'H2' || prevHeading.tagName === 'H3')) {
title = prevHeading.textContent.trim();
}
const viewport = document.createElement('div');
viewport.className = 'diagram-viewport';
const stage = document.createElement('div');
stage.className = 'diagram-stage';
const canvas = document.createElement('div');
canvas.className = 'diagram-canvas';
while (diagram.firstChild) {
canvas.appendChild(diagram.firstChild);
}
stage.appendChild(canvas);
viewport.appendChild(stage);
const viewer = new DiagramViewer({
viewport,
stage,
canvas,
label: null,
wheelNeedsModifier: true,
});
viewer.title = title;
viewer.element = diagram;
const toolbar = createToolbar((action) => {
if (action === 'fullscreen') {
openFullscreen(viewer);
return;
}
viewer.handleAction(action);
});
viewer.label = toolbar.querySelector('[data-zoom-label]');
viewer.render();
diagram.appendChild(toolbar);
diagram.appendChild(viewport);
diagramViewers.push(viewer);
});
}
document.getElementById('diagram-fullscreen-close').addEventListener('click', closeFullscreen);
document.addEventListener('keydown', (e) => {
const fsOpen = document.getElementById('diagram-fullscreen').classList.contains('is-open');
const viewer = fsOpen
? fullscreenViewer
: diagramViewers.find((v) => {
const rect = v.element.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
});
if (!viewer) return;
if (e.key === 'Escape' && fsOpen) {
closeFullscreen();
return;
}
if (e.key === '+' || e.key === '=') {
e.preventDefault();
viewer.handleAction('zoom-in');
} else if (e.key === '-' || e.key === '_') {
e.preventDefault();
viewer.handleAction('zoom-out');
} else if (e.key === '0') {
e.preventDefault();
viewer.handleAction('zoom-reset');
} else if ((e.key === 'f' || e.key === 'F') && !fsOpen) {
e.preventDefault();
openFullscreen(viewer);
}
});
mermaid.initialize({
startOnLoad: false,
theme: 'neutral',
securityLevel: 'loose',
flowchart: { useMaxWidth: false, htmlLabels: true },
sequence: { useMaxWidth: false },
er: { useMaxWidth: false },
});
wrapDiagrams();
mermaid.run({ querySelector: '.mermaid' }).then(() => {
diagramViewers.forEach((viewer) => viewer.fitToView());
});
</script>
</body>
</html>