656 lines
27 KiB
HTML
656 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Pelagia Marine — System Diagrams v2</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
<script type="text/babel" src="design-canvas.jsx"></script>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { background: #f5f0e8; font-family: 'Caveat', cursive; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="text/babel">
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
DIAGRAM 1 — PO STATE MACHINE
|
|
══════════════════════════════════════════════════════ */
|
|
function StateMachineDiagram() {
|
|
const W = 960, H = 800;
|
|
|
|
// State colors
|
|
const C = {
|
|
draft: '#7a7a7a',
|
|
submitted: '#2a6ab5',
|
|
mgr: '#c8871a',
|
|
vendor: '#b87010',
|
|
edits: '#b83030',
|
|
approved: '#1a7a48',
|
|
rejected: '#8a1818',
|
|
payment: '#1a4abf',
|
|
paid: '#155c35',
|
|
closed: '#1a1a2e',
|
|
};
|
|
|
|
// State box helper (cx, cy = center)
|
|
function SBox({ cx, cy, label, color, w=178, h=36, terminal=false }) {
|
|
const lines = label.split('\n');
|
|
return (
|
|
<g>
|
|
{terminal && <ellipse cx={cx} cy={cy} rx={w/2+6} ry={h/2+6}
|
|
fill="none" stroke={color} strokeWidth={2} strokeDasharray="4,3"/>}
|
|
<rect x={cx-w/2} y={cy-h/2} width={w} height={h} rx={h/2}
|
|
fill={color} stroke="rgba(0,0,0,0.25)" strokeWidth={1.5}/>
|
|
{lines.map((l,i) => (
|
|
<text key={i} x={cx} y={cy + (i-(lines.length-1)/2)*15 + 4}
|
|
textAnchor="middle" fontFamily="Space Mono" fontSize={10.5}
|
|
fontWeight="700" fill="#fff">{l}</text>
|
|
))}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
// Arrow marker defs
|
|
const mkr = (id, color) => (
|
|
<marker key={id} id={id} markerWidth="8" markerHeight="8"
|
|
refX="6" refY="3" orient="auto">
|
|
<path d="M0,0 L0,6 L8,3 z" fill={color}/>
|
|
</marker>
|
|
);
|
|
|
|
// Arrow path with label and optional notification badge
|
|
function Arr({ d, label, labelX, labelY, color='#555', notif, notifX, notifY, dashed }) {
|
|
const id = `m${btoa(d).slice(0,8).replace(/[^a-zA-Z0-9]/g,'')}`;
|
|
return (
|
|
<g>
|
|
<defs>{mkr(id, color)}</defs>
|
|
<path d={d} fill="none" stroke={color} strokeWidth={1.6}
|
|
strokeDasharray={dashed ? '5,3' : undefined}
|
|
markerEnd={`url(#${id})`}/>
|
|
{label && <text x={labelX} y={labelY} textAnchor="middle"
|
|
fontFamily="Caveat" fontSize={11} fill={color} fontStyle="italic">{label}</text>}
|
|
{notif && (
|
|
<g transform={`translate(${notifX},${notifY})`}>
|
|
<rect x={-24} y={-9} width={48} height={18} rx={9}
|
|
fill={color} opacity={0.15} stroke={color} strokeWidth={1}/>
|
|
<text x={0} y={4} textAnchor="middle" fontSize={9}
|
|
fontFamily="Caveat" fill={color} fontWeight="700">✉ {notif}</text>
|
|
</g>
|
|
)}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
// Envelope icon
|
|
function Env({ x, y, to, color }) {
|
|
return (
|
|
<g transform={`translate(${x},${y})`}>
|
|
<rect x={-28} y={-11} width={56} height={20} rx={10}
|
|
fill={color} fillOpacity={0.12} stroke={color} strokeWidth={1.2}/>
|
|
<text x={0} y={4} textAnchor="middle" fontFamily="Caveat"
|
|
fontSize={10} fill={color} fontWeight="700">✉ {to}</text>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ background:'#fafaf8', borderRadius:4 }}>
|
|
|
|
{/* Title */}
|
|
<text x={W/2} y={22} textAnchor="middle" fontFamily="Caveat"
|
|
fontSize={16} fontWeight="700" fill="#222">Purchase Order — State Machine</text>
|
|
|
|
{/* ── ARROWS (drawn behind states) ── */}
|
|
|
|
{/* draft → submitted */}
|
|
<Arr d="M470,73 L470,128" color={C.submitted} label="User submits" labelX={520} labelY={104}/>
|
|
<Env x={590} y={100} to="User" color={C.submitted}/>
|
|
|
|
{/* submitted → mgr_review */}
|
|
<Arr d="M470,164 L470,232" color={C.mgr} label="Auto-routed" labelX={524} labelY={201}/>
|
|
<Env x={590} y={198} to="Manager" color={C.mgr}/>
|
|
|
|
{/* mgr_review → vendor_pending (right side, no vendor ID) */}
|
|
<Arr d="M559,253 C650,253 760,295 760,318"
|
|
color={C.vendor} label="No Vendor ID" labelX={682} labelY={273}/>
|
|
|
|
{/* vendor_pending → mgr_review (back, slightly offset) */}
|
|
<Arr d="M740,318 C700,270 560,243 558,247"
|
|
color={C.vendor} dashed={true} label="ID provided" labelX={660} labelY={262}/>
|
|
<Env x={760} y={255} to="Manager" color={C.vendor}/>
|
|
|
|
{/* mgr_review → edits_requested (left side) */}
|
|
<Arr d="M381,255 C270,255 168,300 168,317"
|
|
color={C.edits} label="Ask for Edits" labelX={258} labelY={270}/>
|
|
|
|
{/* edits_requested → submitted (back up, far left curve) */}
|
|
<Arr d="M100,335 C40,335 40,148 383,148"
|
|
color={C.edits} dashed={true} label="Resubmit" labelX={48} labelY={242}/>
|
|
<Env x={52} y={195} to="Manager" color={C.edits}/>
|
|
|
|
{/* mgr_review → approved (straight down) */}
|
|
<Arr d="M470,268 L470,423" color={C.approved}
|
|
label="Approve / Approve+Note" labelX={536} labelY={349}/>
|
|
<Env x={620} y={343} to="User + Accounts" color={C.approved}/>
|
|
|
|
{/* mgr_review → rejected (right, same y as approved) */}
|
|
<Arr d="M557,262 C680,280 760,395 760,423"
|
|
color={C.rejected} label="Reject" labelX={684} labelY={330}/>
|
|
<Env x={840} y={328} to="User" color={C.rejected}/>
|
|
|
|
{/* approved → payment */}
|
|
<Arr d="M470,459 L470,513" color={C.payment}
|
|
label="Accounts notified (PO attached)" labelX={356} labelY={490}/>
|
|
|
|
{/* payment → paid */}
|
|
<Arr d="M470,549 L470,603" color={C.paid}
|
|
label="Accounts marks paid" labelX={358} labelY={579}/>
|
|
<Env x={240} y={578} to="User" color={C.paid}/>
|
|
|
|
{/* paid → closed */}
|
|
<Arr d="M470,639 L470,683" color={C.closed}
|
|
label="User confirms receipt" labelX={360} labelY={665}/>
|
|
|
|
{/* ── STATES (drawn on top) ── */}
|
|
<SBox cx={470} cy={55} label="DRAFT" color={C.draft}/>
|
|
<SBox cx={470} cy={146} label="SUBMITTED" color={C.submitted}/>
|
|
<SBox cx={470} cy={250} label="MGR_REVIEW" color={C.mgr}/>
|
|
<SBox cx={760} cy={335} label="VENDOR_ID\nPENDING" color={C.vendor} w={160}/>
|
|
<SBox cx={168} cy={335} label="EDITS\nREQUESTED" color={C.edits} w={148}/>
|
|
<SBox cx={470} cy={441} label="MGR_APPROVED" color={C.approved}/>
|
|
<SBox cx={760} cy={441} label="REJECTED" color={C.rejected} terminal={true} w={148}/>
|
|
<SBox cx={470} cy={531} label="SENT_FOR_PAYMENT" color={C.payment}/>
|
|
<SBox cx={470} cy={621} label="PAID_DELIVERED" color={C.paid}/>
|
|
<SBox cx={470} cy={701} label="CLOSED" color={C.closed} terminal={true}/>
|
|
|
|
{/* Notes on rejected */}
|
|
<text x={845} y={458} fontFamily="Caveat" fontSize={11} fill={C.rejected} fontStyle="italic">saved to rejected DB</text>
|
|
|
|
{/* Edits notification */}
|
|
<Env x={250} y={335} to="User" color={C.edits}/>
|
|
|
|
{/* Legend */}
|
|
<g transform="translate(18,760)">
|
|
<rect x={0} y={-14} width={920} height={28} rx={2} fill="#eee" stroke="#ccc" strokeWidth={1}/>
|
|
{[
|
|
{ color:C.draft, label:'Draft — not yet submitted' },
|
|
{ color:C.mgr, label:'Under manager review' },
|
|
{ color:C.approved, label:'Approval / payment path' },
|
|
{ color:C.edits, label:'Returned for changes' },
|
|
{ color:C.rejected, label:'Terminal — rejected' },
|
|
{ color:C.closed, label:'Terminal — complete' },
|
|
].map((l,i) => (
|
|
<g key={i} transform={`translate(${i*155+10},0)`}>
|
|
<rect x={0} y={-7} width={14} height={14} rx={7} fill={l.color}/>
|
|
<text x={20} y={5} fontFamily="Caveat" fontSize={11} fill="#555">{l.label}</text>
|
|
</g>
|
|
))}
|
|
</g>
|
|
|
|
{/* Envelope legend */}
|
|
<text x={W-12} y={22} textAnchor="end" fontFamily="Caveat"
|
|
fontSize={11} fill="#888" fontStyle="italic">✉ = email notification triggered</text>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
DIAGRAM 2 — NOTIFICATION FLOW (swim lanes)
|
|
══════════════════════════════════════════════════════ */
|
|
function NotificationFlowDiagram() {
|
|
const W = 900, H = 660;
|
|
const LANES = [
|
|
{ label:'Submitter\n(Technical/Manning)', color:'#2a6ab5', x:110 },
|
|
{ label:'System\n(Auto-email)', color:'#555', x:310 },
|
|
{ label:'Manager', color:'#c8871a', x:510 },
|
|
{ label:'Accounts', color:'#1a4abf', x:710 },
|
|
];
|
|
const LW = 180;
|
|
|
|
const events = [
|
|
{
|
|
y:130, label:'Creates PO',
|
|
actor:0,
|
|
notifs:[
|
|
{ from:0, to:1, label:'confirmation' },
|
|
{ from:1, to:0, label:'PO-#### created' },
|
|
]
|
|
},
|
|
{
|
|
y:195, label:'Submits PO',
|
|
actor:0,
|
|
notifs:[
|
|
{ from:0, to:1, label:'submit trigger' },
|
|
{ from:1, to:2, label:'New PO for review' },
|
|
{ from:1, to:0, label:'Submitted ✓' },
|
|
]
|
|
},
|
|
{
|
|
y:270, label:'No Vendor ID —\nUser asked to provide',
|
|
actor:2,
|
|
notifs:[
|
|
{ from:2, to:1, label:'vendor ID request' },
|
|
{ from:1, to:0, label:'Action needed:\nprovide Vendor ID' },
|
|
]
|
|
},
|
|
{
|
|
y:340, label:'User provides\nVendor ID',
|
|
actor:0,
|
|
notifs:[
|
|
{ from:0, to:1, label:'vendor ID update' },
|
|
{ from:1, to:2, label:'Vendor ID added' },
|
|
]
|
|
},
|
|
{
|
|
y:405, label:'Asks for Edits',
|
|
actor:2,
|
|
notifs:[
|
|
{ from:2, to:1, label:'edits trigger + comment' },
|
|
{ from:1, to:0, label:'Edits requested\n+ comment' },
|
|
]
|
|
},
|
|
{
|
|
y:468, label:'Approves PO\n(or Approve+Note)',
|
|
actor:2,
|
|
notifs:[
|
|
{ from:2, to:1, label:'approval trigger' },
|
|
{ from:1, to:0, label:'PO Approved ✓' },
|
|
{ from:1, to:3, label:'PO attached\nfor payment' },
|
|
]
|
|
},
|
|
{
|
|
y:545, label:'Rejects PO',
|
|
actor:2,
|
|
notifs:[
|
|
{ from:2, to:1, label:'reject + reason' },
|
|
{ from:1, to:0, label:'PO Rejected\n+ reason' },
|
|
]
|
|
},
|
|
{
|
|
y:605, label:'Processes Payment\n→ User confirms receipt',
|
|
actor:3,
|
|
notifs:[
|
|
{ from:3, to:1, label:'payment trigger' },
|
|
{ from:1, to:0, label:'Payment sent ✓\nPlease confirm' },
|
|
]
|
|
},
|
|
];
|
|
|
|
const laneX = (i) => LANES[i].x;
|
|
|
|
return (
|
|
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ background:'#fafaf8', borderRadius:4 }}>
|
|
|
|
{/* Title */}
|
|
<text x={W/2} y={22} textAnchor="middle" fontFamily="Caveat"
|
|
fontSize={16} fontWeight="700" fill="#222">Email Notification Triggers</text>
|
|
|
|
{/* Lane headers */}
|
|
{LANES.map((l,i) => (
|
|
<g key={i}>
|
|
<rect x={l.x-LW/2} y={35} width={LW} height={50} rx={3}
|
|
fill={l.color} stroke="none"/>
|
|
{l.label.split('\n').map((line,j) => (
|
|
<text key={j} x={l.x} y={55+j*16} textAnchor="middle"
|
|
fontFamily="Caveat" fontSize={12} fontWeight="700" fill="#fff">{line}</text>
|
|
))}
|
|
{/* Vertical lane line */}
|
|
<line x1={l.x} y1={85} x2={l.x} y2={H-20}
|
|
stroke={l.color} strokeWidth={1} strokeOpacity={0.2} strokeDasharray="4,4"/>
|
|
</g>
|
|
))}
|
|
|
|
{/* Lane dividers */}
|
|
{[210, 410, 610].map(x => (
|
|
<line key={x} x1={x} y1={35} x2={x} y2={H-20}
|
|
stroke="#ddd" strokeWidth={1}/>
|
|
))}
|
|
|
|
{/* Events */}
|
|
{events.map((ev, ei) => {
|
|
const actorColor = LANES[ev.actor].color;
|
|
return (
|
|
<g key={ei}>
|
|
{/* Event marker on actor lane */}
|
|
<circle cx={laneX(ev.actor)} cy={ev.y} r={7}
|
|
fill={actorColor} stroke="white" strokeWidth={1.5}/>
|
|
{/* Event label */}
|
|
{ev.label.split('\n').map((line,j) => (
|
|
<text key={j} x={laneX(ev.actor)} y={ev.y + 18 + j*14}
|
|
textAnchor="middle" fontFamily="Caveat" fontSize={11}
|
|
fontWeight="700" fill={actorColor}>{line}</text>
|
|
))}
|
|
{/* Notification arrows */}
|
|
{ev.notifs.map((n,ni) => {
|
|
const x1 = laneX(n.from), x2 = laneX(n.to);
|
|
const yOff = ei*2 + ni*14;
|
|
const ny = ev.y + yOff;
|
|
const color = LANES[n.to === 1 ? n.from : n.to].color;
|
|
const mid = (x1+x2)/2;
|
|
const isSystem = n.from === 1 || n.to === 1;
|
|
return (
|
|
<g key={ni}>
|
|
<defs>
|
|
<marker id={`an${ei}${ni}`} markerWidth="6" markerHeight="6"
|
|
refX="5" refY="3" orient="auto">
|
|
<path d="M0,0 L0,6 L6,3 z" fill={color}/>
|
|
</marker>
|
|
</defs>
|
|
<line x1={x1+(x1<x2?7:-7)} y1={ny} x2={x2+(x1<x2?-7:7)} y2={ny}
|
|
stroke={color} strokeWidth={1.3} strokeDasharray={isSystem ? '4,3' : undefined}
|
|
markerEnd={`url(#an${ei}${ni})`}/>
|
|
{n.label && (
|
|
<text x={mid} y={ny-3} textAnchor="middle"
|
|
fontFamily="Caveat" fontSize={9.5} fill={color} fontStyle="italic">
|
|
{n.label.split('\n')[0]}
|
|
</text>
|
|
)}
|
|
</g>
|
|
);
|
|
})}
|
|
</g>
|
|
);
|
|
})}
|
|
|
|
{/* Row separators */}
|
|
{events.slice(0,-1).map((ev,i) => (
|
|
<line key={i} x1={20} y1={(ev.y + events[i+1].y)/2}
|
|
x2={W-20} y2={(ev.y + events[i+1].y)/2}
|
|
stroke="#eee" strokeWidth={1}/>
|
|
))}
|
|
|
|
{/* Legend */}
|
|
<g transform="translate(20,640)">
|
|
<line x1={0} y1={6} x2={28} y2={6} stroke="#555" strokeWidth={1.3} strokeDasharray="4,3"/>
|
|
<text x={34} y={10} fontFamily="Caveat" fontSize={11} fill="#888">System-triggered email</text>
|
|
<line x1={160} y1={6} x2={188} y2={6} stroke="#2a6ab5" strokeWidth={1.3}/>
|
|
<text x={194} y={10} fontFamily="Caveat" fontSize={11} fill="#888">Direct action</text>
|
|
<circle cx={280} cy={6} r={5} fill="#c8871a"/>
|
|
<text x={290} y={10} fontFamily="Caveat" fontSize={11} fill="#888">Actor initiating event</text>
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
DIAGRAM 3 — ROLES & PERMISSIONS MATRIX
|
|
══════════════════════════════════════════════════════ */
|
|
function PermissionsMatrix() {
|
|
const roles = [
|
|
{ id:'tech', label:'Technical', color:'#2a6ab5' },
|
|
{ id:'mann', label:'Manning', color:'#2a6ab5' },
|
|
{ id:'accts', label:'Accounts', color:'#1a4abf' },
|
|
{ id:'mgr', label:'Manager', color:'#c8871a' },
|
|
{ id:'super', label:'SuperUser', color:'#7a2abf' },
|
|
{ id:'audit', label:'Auditor', color:'#555' },
|
|
{ id:'admin', label:'Admin', color:'#1a1a2e' },
|
|
];
|
|
|
|
// ✓ = always | ○ = opt-in | — = never
|
|
const sections = [
|
|
{
|
|
title:'PO — Creation',
|
|
rows:[
|
|
{ label:'Create PO', perms:['✓','✓','—','—','✓','—','✓'] },
|
|
{ label:'Add Line Items', perms:['✓','✓','—','—','✓','—','✓'] },
|
|
{ label:'Attach Documents', perms:['✓','✓','—','—','✓','—','✓'] },
|
|
{ label:'Submit PO', perms:['✓','✓','—','—','✓','—','✓'] },
|
|
{ label:'Edit Own PO (draft)',perms:['✓','✓','—','—','✓','—','✓'] },
|
|
{ label:'Provide Vendor ID', perms:['✓','✓','—','—','✓','—','✓'] },
|
|
{ label:'Confirm Receipt', perms:['✓','✓','—','—','✓','—','✓'] },
|
|
]
|
|
},
|
|
{
|
|
title:'PO — Tracking',
|
|
rows:[
|
|
{ label:'View Own POs', perms:['✓','✓','✓','✓','✓','✓','✓'] },
|
|
{ label:'View All POs', perms:['○','○','✓','✓','✓','✓','✓'] },
|
|
{ label:'View Vessel Report', perms:['○','○','○','✓','✓','✓','✓'] },
|
|
{ label:'View Project Report',perms:['○','○','○','✓','✓','✓','✓'] },
|
|
{ label:'Export Reports', perms:['—','—','✓','✓','✓','✓','✓'] },
|
|
]
|
|
},
|
|
{
|
|
title:'PO — Approval (Manager)',
|
|
rows:[
|
|
{ label:'Approve PO', perms:['—','—','—','✓','✓','—','✓'] },
|
|
{ label:'Reject PO', perms:['—','—','—','✓','✓','—','✓'] },
|
|
{ label:'Ask for Edits', perms:['—','—','—','✓','✓','—','✓'] },
|
|
{ label:'Approve with Note', perms:['—','—','—','✓','✓','—','✓'] },
|
|
{ label:'Review Vendor ID', perms:['—','—','—','✓','✓','—','✓'] },
|
|
]
|
|
},
|
|
{
|
|
title:'PO — Payment (Accounts)',
|
|
rows:[
|
|
{ label:'View Payment Queue', perms:['—','—','✓','○','✓','—','✓'] },
|
|
{ label:'Process Payment', perms:['—','—','✓','—','✓','—','✓'] },
|
|
{ label:'Mark Paid + Upload Receipt',perms:['—','—','✓','—','✓','—','✓'] },
|
|
]
|
|
},
|
|
{
|
|
title:'Administration',
|
|
rows:[
|
|
{ label:'Manage Users', perms:['—','—','—','—','✓','—','✓'] },
|
|
{ label:'Set Opt-in Perms', perms:['—','—','—','—','✓','—','✓'] },
|
|
{ label:'View Audit Log', perms:['—','—','—','○','✓','✓','✓'] },
|
|
{ label:'System Config', perms:['—','—','—','—','—','—','✓'] },
|
|
]
|
|
},
|
|
];
|
|
|
|
const COL_W = 68, ROW_H = 22, LABEL_W = 178;
|
|
const totalRows = sections.reduce((a,s) => a + s.rows.length + 1, 0) + 1;
|
|
const W = LABEL_W + roles.length * COL_W + 20;
|
|
const H = 60 + totalRows * ROW_H + 40;
|
|
|
|
const permColor = (p) => p === '✓' ? '#1a7a48' : p === '○' ? '#c8871a' : '#ccc';
|
|
const permBg = (p) => p === '✓' ? '#d8f0e0' : p === '○' ? '#fdf3d0' : 'transparent';
|
|
|
|
let rowIdx = 0;
|
|
|
|
return (
|
|
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ background:'#fafaf8', borderRadius:4 }}>
|
|
|
|
<text x={W/2} y={22} textAnchor="middle" fontFamily="Caveat"
|
|
fontSize={16} fontWeight="700" fill="#222">Roles & Permissions Matrix</text>
|
|
|
|
{/* Role headers */}
|
|
{roles.map((r,i) => (
|
|
<g key={i}>
|
|
<rect x={LABEL_W + i*COL_W + 2} y={34} width={COL_W-4} height={30} rx={3}
|
|
fill={r.color}/>
|
|
<text x={LABEL_W + i*COL_W + COL_W/2} y={50} textAnchor="middle"
|
|
fontFamily="Caveat" fontSize={11} fontWeight="700" fill="#fff"
|
|
transform={`rotate(-20,${LABEL_W+i*COL_W+COL_W/2},50)`}>{r.label}</text>
|
|
</g>
|
|
))}
|
|
|
|
{/* Sections and rows */}
|
|
{(() => {
|
|
const elems = [];
|
|
let y = 68;
|
|
sections.forEach((sec, si) => {
|
|
// Section header
|
|
elems.push(
|
|
<g key={`sec${si}`}>
|
|
<rect x={0} y={y} width={W} height={ROW_H} fill="#e8e4dc"/>
|
|
<text x={8} y={y+15} fontFamily="Caveat" fontSize={12}
|
|
fontWeight="700" fill="#444">{sec.title}</text>
|
|
</g>
|
|
);
|
|
y += ROW_H;
|
|
|
|
sec.rows.forEach((row, ri) => {
|
|
const bg = (si+ri) % 2 === 0 ? '#fafaf8' : '#f5f2ee';
|
|
elems.push(
|
|
<g key={`r${si}${ri}`}>
|
|
<rect x={0} y={y} width={W} height={ROW_H} fill={bg}/>
|
|
<text x={10} y={y+15} fontFamily="Caveat" fontSize={12} fill="#333">{row.label}</text>
|
|
<line x1={0} y1={y+ROW_H} x2={W} y2={y+ROW_H} stroke="#e8e4dc" strokeWidth={1}/>
|
|
{row.perms.map((p,pi) => {
|
|
const cx = LABEL_W + pi*COL_W + COL_W/2;
|
|
const cy = y + ROW_H/2;
|
|
return (
|
|
<g key={pi}>
|
|
{p !== '—' && <rect x={cx-14} y={cy-9} width={28} height={18} rx={9}
|
|
fill={permBg(p)} stroke={permColor(p)} strokeWidth={1.2}/>}
|
|
<text x={cx} y={cy+5} textAnchor="middle"
|
|
fontFamily="Caveat" fontSize={13} fontWeight="700"
|
|
fill={permColor(p)}>{p}</text>
|
|
</g>
|
|
);
|
|
})}
|
|
</g>
|
|
);
|
|
y += ROW_H;
|
|
});
|
|
});
|
|
// Legend
|
|
elems.push(
|
|
<g key="legend" transform={`translate(8,${y+12})`}>
|
|
{[
|
|
{ sym:'✓', color:'#1a7a48', bg:'#d8f0e0', label:'Always allowed' },
|
|
{ sym:'○', color:'#c8871a', bg:'#fdf3d0', label:'Opt-in (can be granted)' },
|
|
{ sym:'—', color:'#ccc', bg:'transparent', label:'Not permitted' },
|
|
].map((l,i) => (
|
|
<g key={i} transform={`translate(${i*200},0)`}>
|
|
<rect x={0} y={-10} width={20} height={18} rx={9}
|
|
fill={l.bg} stroke={l.color} strokeWidth={1.2}/>
|
|
<text x={10} y={5} textAnchor="middle" fontFamily="Caveat"
|
|
fontSize={13} fontWeight="700" fill={l.color}>{l.sym}</text>
|
|
<text x={28} y={5} fontFamily="Caveat" fontSize={12} fill="#666">{l.label}</text>
|
|
</g>
|
|
))}
|
|
</g>
|
|
);
|
|
return elems;
|
|
})()}
|
|
|
|
{/* Column dividers */}
|
|
{roles.map((_,i) => (
|
|
<line key={i} x1={LABEL_W+i*COL_W} y1={34} x2={LABEL_W+i*COL_W} y2={H-20}
|
|
stroke="#ddd" strokeWidth={1}/>
|
|
))}
|
|
<line x1={LABEL_W} y1={34} x2={LABEL_W} y2={H-20} stroke="#bbb" strokeWidth={1.5}/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
DIAGRAM 4 — GAP ANALYSIS CARD
|
|
══════════════════════════════════════════════════════ */
|
|
function GapAnalysis() {
|
|
const W = 860, H = 480;
|
|
|
|
const gaps = [
|
|
{ cat:'Roles', color:'#7a2abf', items:[
|
|
'Add SuperUser role (all permissions + user management)',
|
|
'Add Auditor role (read-only + audit log access)',
|
|
'Distinguish "User" vs "Admin" vs "SuperUser" in account types',
|
|
]},
|
|
{ cat:'PO States', color:'#c8871a', items:[
|
|
'Add VENDOR_ID_PENDING state before MGR_REVIEW completes',
|
|
'Add PAID_DELIVERED state (separate from SENT_FOR_PAYMENT)',
|
|
'Add CLOSED state — requires user receipt confirmation',
|
|
'Rejected POs archived to separate rejected_orders store',
|
|
]},
|
|
{ cat:'Entities', color:'#2a6ab5', items:[
|
|
'Add VENDOR entity (vendor_id, name, contact) linked to PO',
|
|
'Add RECEIPT entity (po_id, file_url, confirmed_by, confirmed_at)',
|
|
'Update PO status enum with all new states',
|
|
]},
|
|
{ cat:'Notifications', color:'#1a7a48', items:[
|
|
'Email on every state transition (not just approval)',
|
|
'Mail to Accounts must include PO document attached',
|
|
'Email on vendor ID request and provision',
|
|
'Closure email to all stakeholders',
|
|
]},
|
|
{ cat:'Features', color:'#1a4abf', items:[
|
|
'PO Insights: vessel-wise spend view',
|
|
'PO Insights: project-wise spend view',
|
|
'Opt-in permissions model (admin-configurable per user)',
|
|
]},
|
|
];
|
|
|
|
let y = 45;
|
|
return (
|
|
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ background:'#fffef8', borderRadius:4, border:'2px dashed #c8971a' }}>
|
|
<text x={W/2} y={22} textAnchor="middle" fontFamily="Caveat" fontSize={16}
|
|
fontWeight="700" fill="#c8971a">⚠ Gap Analysis — Changes vs. Original Design</text>
|
|
|
|
{(() => {
|
|
const elems = [];
|
|
let cy = 40;
|
|
const colW = W/2 - 20;
|
|
gaps.forEach((g, gi) => {
|
|
const col = gi < 3 ? 0 : 1;
|
|
const x = col * (colW + 20) + 14;
|
|
elems.push(
|
|
<g key={gi}>
|
|
<rect x={x} y={cy-2} width={colW} height={16+g.items.length*20} rx={2}
|
|
fill={g.color} fillOpacity={0.06} stroke={g.color} strokeWidth={1.5}/>
|
|
<rect x={x} y={cy-2} width={110} height={18} rx={2}
|
|
fill={g.color}/>
|
|
<text x={x+8} y={cy+11} fontFamily="Caveat" fontSize={12}
|
|
fontWeight="700" fill="#fff">{g.cat}</text>
|
|
{g.items.map((item,ii) => (
|
|
<g key={ii}>
|
|
<circle cx={x+14} cy={cy+26+ii*20} r={3} fill={g.color}/>
|
|
<text x={x+22} y={cy+30+ii*20} fontFamily="Caveat"
|
|
fontSize={11.5} fill="#333">{item}</text>
|
|
</g>
|
|
))}
|
|
</g>
|
|
);
|
|
if (gi === 2) cy = 40; // reset for second column
|
|
else cy += 28 + g.items.length*20;
|
|
});
|
|
return elems;
|
|
})()}
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
APP
|
|
══════════════════════════════════════════════════════ */
|
|
function App() {
|
|
return (
|
|
<DesignCanvas title="Pelagia Marine — System Diagrams v2">
|
|
|
|
<DCSection id="s1" title="PO State Machine">
|
|
<DCArtboard id="sm" label="Purchase Order State Machine" width={960} height={800}>
|
|
<StateMachineDiagram/>
|
|
</DCArtboard>
|
|
</DCSection>
|
|
|
|
<DCSection id="s2" title="Notifications & Permissions">
|
|
<DCArtboard id="notif" label="Email Notification Triggers" width={900} height={660}>
|
|
<NotificationFlowDiagram/>
|
|
</DCArtboard>
|
|
<DCArtboard id="perms" label="Roles & Permissions Matrix" width={660} height={620}>
|
|
<PermissionsMatrix/>
|
|
</DCArtboard>
|
|
</DCSection>
|
|
|
|
<DCSection id="s3" title="Gap Analysis — Notes vs. Current Diagrams">
|
|
<DCArtboard id="gaps" label="What needs updating" width={860} height={480}>
|
|
<GapAnalysis/>
|
|
</DCArtboard>
|
|
</DCSection>
|
|
|
|
</DesignCanvas>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
|
|
</script>
|
|
</body>
|
|
</html>
|