645 lines
27 KiB
HTML
645 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 Portal — System Diagrams</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">
|
||
|
||
/* ─── SVG PRIMITIVES ─────────────────────────────────── */
|
||
|
||
function Actor({ x, y, label, sub }) {
|
||
const lines = label.split('\n');
|
||
return (
|
||
<g>
|
||
<circle cx={x} cy={y} r={11} stroke="#222" strokeWidth={1.8} fill="#fafaf8"/>
|
||
<line x1={x} y1={y+11} x2={x} y2={y+38} stroke="#222" strokeWidth={1.8}/>
|
||
<line x1={x-16} y1={y+22} x2={x+16} y2={y+22} stroke="#222" strokeWidth={1.8}/>
|
||
<line x1={x} y1={y+38} x2={x-14} y2={y+58} stroke="#222" strokeWidth={1.8}/>
|
||
<line x1={x} y1={y+38} x2={x+14} y2={y+58} stroke="#222" strokeWidth={1.8}/>
|
||
{lines.map((l,i) => (
|
||
<text key={i} x={x} y={y+75+i*16} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={13} fontWeight="700" fill="#222">{l}</text>
|
||
))}
|
||
{sub && <text x={x} y={y+75+lines.length*16} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={11} fill="#888" fontStyle="italic">{sub}</text>}
|
||
</g>
|
||
);
|
||
}
|
||
|
||
function UC({ cx, cy, label, rx=88, ry=21, color='#fff', stroke='#222' }) {
|
||
const lines = label.split('\n');
|
||
return (
|
||
<g>
|
||
<ellipse cx={cx} cy={cy} rx={rx} ry={ry} stroke={stroke} strokeWidth={1.8} fill={color}/>
|
||
{lines.map((l,i) => (
|
||
<text key={i} x={cx} y={cy + (i - (lines.length-1)/2)*14 + 4}
|
||
textAnchor="middle" fontFamily="Caveat" fontSize={12} fill="#222">{l}</text>
|
||
))}
|
||
</g>
|
||
);
|
||
}
|
||
|
||
function Arrow({ x1,y1,x2,y2, dashed, label, color='#555', markerEnd=true }) {
|
||
const id = `arr-${Math.abs(x1+y1+x2+y2)|0}`;
|
||
const dx = x2-x1, dy = y2-y1;
|
||
const mx = (x1+x2)/2, my = (y1+y2)/2;
|
||
const angle = Math.atan2(dy,dx)*180/Math.PI;
|
||
return (
|
||
<g>
|
||
<defs>
|
||
<marker id={id} markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill={color}/>
|
||
</marker>
|
||
</defs>
|
||
<line x1={x1} y1={y1} x2={x2} y2={y2}
|
||
stroke={color} strokeWidth={1.4}
|
||
strokeDasharray={dashed ? '5,4' : undefined}
|
||
markerEnd={markerEnd ? `url(#${id})` : undefined}/>
|
||
{label && (
|
||
<text x={mx} y={my-5} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={10} fill={color} fontStyle="italic"
|
||
transform={`rotate(${Math.abs(angle)>90?angle+180:angle},${mx},${my-5})`}
|
||
style={{transformBox:'fill-box'}}>{label}</text>
|
||
)}
|
||
</g>
|
||
);
|
||
}
|
||
|
||
function Conn({ x1,y1,x2,y2, label, card1, card2, color='#444', dashed }) {
|
||
const mx=(x1+x2)/2, my=(y1+y2)/2;
|
||
const dx=x2-x1, dy=y2-y1;
|
||
const len=Math.sqrt(dx*dx+dy*dy);
|
||
const ux=dx/len, uy=dy/len;
|
||
return (
|
||
<g>
|
||
<line x1={x1} y1={y1} x2={x2} y2={y2}
|
||
stroke={color} strokeWidth={1.5}
|
||
strokeDasharray={dashed?'5,4':undefined}/>
|
||
{card1 && <text x={x1+ux*18-uy*10} y={y1+uy*18+ux*10}
|
||
textAnchor="middle" fontFamily="Space Mono" fontSize={11} fontWeight="700" fill={color}>{card1}</text>}
|
||
{card2 && <text x={x2-ux*18-uy*10} y={y2-uy*18+ux*10}
|
||
textAnchor="middle" fontFamily="Space Mono" fontSize={11} fontWeight="700" fill={color}>{card2}</text>}
|
||
{label && <text x={mx-uy*12} y={my+ux*12}
|
||
textAnchor="middle" fontFamily="Caveat" fontSize={11} fill={color} fontStyle="italic">{label}</text>}
|
||
</g>
|
||
);
|
||
}
|
||
|
||
/* ─── ENTITY BOX ─────────────────────────────────────── */
|
||
function EntityBox({ x, y, name, attrs, w=200, headerColor='#222' }) {
|
||
const ROW = 19;
|
||
const HEADER = 28;
|
||
const h = HEADER + attrs.length * ROW + 8;
|
||
return (
|
||
<g>
|
||
<rect x={x} y={y} width={w} height={h} rx={2}
|
||
stroke="#222" strokeWidth={2} fill="#fafaf8"/>
|
||
{/* header */}
|
||
<rect x={x} y={y} width={w} height={HEADER} rx={2}
|
||
stroke="#222" strokeWidth={2} fill={headerColor}/>
|
||
<rect x={x} y={y+HEADER-4} width={w} height={4} fill={headerColor}/>
|
||
<text x={x+w/2} y={y+19} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={14} fontWeight="700" fill="#fff">{name}</text>
|
||
{/* divider */}
|
||
<line x1={x} y1={y+HEADER} x2={x+w} y2={y+HEADER} stroke="#ccc" strokeWidth={1}/>
|
||
{/* attributes */}
|
||
{attrs.map((a,i) => (
|
||
<g key={i}>
|
||
<text x={x+10} y={y+HEADER+14+i*ROW}
|
||
fontFamily="Space Mono" fontSize={10}
|
||
fill={a.pk ? '#3a7cbf' : a.fk ? '#2a8a50' : a.enum ? '#7a4abf' : '#444'}
|
||
fontWeight={a.pk||a.fk ? '700':'400'}>
|
||
{a.pk?'PK ':a.fk?'FK ':a.enum?'∑ ':' '}{a.name}
|
||
</text>
|
||
{a.type && <text x={x+w-8} y={y+HEADER+14+i*ROW}
|
||
textAnchor="end" fontFamily="Space Mono" fontSize={9} fill="#aaa">{a.type}</text>}
|
||
</g>
|
||
))}
|
||
</g>
|
||
);
|
||
}
|
||
|
||
/* ─── USE CASE DIAGRAM ───────────────────────────────── */
|
||
function UseCaseDiagram() {
|
||
const W=930, H=780;
|
||
|
||
// System boundary
|
||
const SX=155, SY=38, SW=610, SH=700;
|
||
|
||
// Use case columns
|
||
const LX=290, RX=620;
|
||
|
||
// Left use cases [y positions]
|
||
const lucs = [
|
||
{ y:100, label:'Login / Authenticate' },
|
||
{ y:188, label:'Create Purchase Order' },
|
||
{ y:268, label:'Add Line Items' },
|
||
{ y:348, label:'Attach Documents' },
|
||
{ y:432, label:'Submit for Approval' },
|
||
{ y:515, label:'View & Track My POs' },
|
||
{ y:598, label:'Edit & Resubmit PO' },
|
||
{ y:680, label:'View PO History' }, // shared with manager
|
||
];
|
||
|
||
// Right use cases
|
||
const rucs = [
|
||
{ y:130, label:'Review Pending\nApprovals' },
|
||
{ y:218, label:'Approve PO' },
|
||
{ y:300, label:'Reject PO' },
|
||
{ y:383, label:'Ask for Edits' },
|
||
{ y:466, label:'Approve with Note' },
|
||
{ y:560, label:'View Payment Queue' },
|
||
{ y:648, label:'Process Payment' },
|
||
];
|
||
|
||
// Actor positions [cx, head_y]
|
||
const actors = {
|
||
tech: { x:72, y:175 },
|
||
manning: { x:72, y:370 },
|
||
admin: { x:72, y:535 },
|
||
manager: { x:858, y:220 },
|
||
accounts:{ x:858, y:500 },
|
||
};
|
||
|
||
const la = actors;
|
||
|
||
return (
|
||
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ background:'#fafaf8', borderRadius:4 }}>
|
||
<defs>
|
||
<marker id="uca" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill="#555"/>
|
||
</marker>
|
||
<marker id="ucb" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L8,3 z" fill="#3a7cbf"/>
|
||
</marker>
|
||
</defs>
|
||
|
||
{/* System boundary */}
|
||
<rect x={SX} y={SY} width={SW} height={SH} rx={4}
|
||
stroke="#222" strokeWidth={2.5} fill="#f9f8f5" strokeDasharray="none"/>
|
||
<text x={SX+SW/2} y={SY-10} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={15} fontWeight="700" fill="#222">
|
||
Pelagia Marine Portal — Purchasing System
|
||
</text>
|
||
|
||
{/* ── Left submitter connections ── */}
|
||
{/* Technical connects to all left UCs */}
|
||
{lucs.map((uc,i) => (
|
||
<line key={`t${i}`}
|
||
x1={la.tech.x+20} y1={la.tech.y+30}
|
||
x2={LX-88} y2={uc.y}
|
||
stroke="#bbb" strokeWidth={1.2}/>
|
||
))}
|
||
{/* Manning connects to UCs 0–6 (not history) */}
|
||
{lucs.slice(0,7).map((uc,i) => (
|
||
<line key={`m${i}`}
|
||
x1={la.manning.x+20} y1={la.manning.y+30}
|
||
x2={LX-88} y2={uc.y}
|
||
stroke="#bbb" strokeWidth={1.2}/>
|
||
))}
|
||
{/* Admin connects to all left UCs */}
|
||
{lucs.map((uc,i) => (
|
||
<line key={`ad${i}`}
|
||
x1={la.admin.x+20} y1={la.admin.y+30}
|
||
x2={LX-88} y2={uc.y}
|
||
stroke="#bbb" strokeWidth={1.2}/>
|
||
))}
|
||
|
||
{/* ── Manager connections ── */}
|
||
{rucs.slice(0,5).map((uc,i) => (
|
||
<line key={`mg${i}`}
|
||
x1={la.manager.x-20} y1={la.manager.y+30}
|
||
x2={RX+88} y2={uc.y}
|
||
stroke="#bbb" strokeWidth={1.2}/>
|
||
))}
|
||
{/* Manager also connects to View PO History (left col) */}
|
||
<line x1={la.manager.x-20} y1={la.manager.y+30} x2={LX+88} y2={680} stroke="#bbb" strokeWidth={1.2}/>
|
||
|
||
{/* ── Accounts connections ── */}
|
||
{rucs.slice(5).map((uc,i) => (
|
||
<line key={`ac${i}`}
|
||
x1={la.accounts.x-20} y1={la.accounts.y+30}
|
||
x2={RX+88} y2={uc.y}
|
||
stroke="#bbb" strokeWidth={1.2}/>
|
||
))}
|
||
{/* Accounts also sees history */}
|
||
<line x1={la.accounts.x-20} y1={la.accounts.y+30} x2={LX+88} y2={680} stroke="#bbb" strokeWidth={1.2}/>
|
||
|
||
{/* ── Include / Extend relationships ── */}
|
||
{/* Create PO <<includes>> Add Line Items */}
|
||
<line x1={LX+30} y1={188} x2={LX+30} y2={255}
|
||
stroke="#3a7cbf" strokeWidth={1.3} strokeDasharray="5,3"
|
||
markerEnd="url(#ucb)"/>
|
||
<text x={LX+40} y={228} fontFamily="Caveat" fontSize={10} fill="#3a7cbf" fontStyle="italic">«includes»</text>
|
||
|
||
{/* Submit <<extends>> Attach Documents */}
|
||
<line x1={LX-30} y1={432} x2={LX-30} y2={362}
|
||
stroke="#3a7cbf" strokeWidth={1.3} strokeDasharray="5,3"
|
||
markerEnd="url(#ucb)"/>
|
||
<text x={LX-110} y={400} fontFamily="Caveat" fontSize={10} fill="#3a7cbf" fontStyle="italic">«extends»</text>
|
||
|
||
{/* Approve PO <<extends>> Approve with Note */}
|
||
<line x1={RX+30} y1={466} x2={RX+30} y2={232}
|
||
stroke="#3a7cbf" strokeWidth={1.3} strokeDasharray="5,3"
|
||
markerEnd="url(#ucb)"/>
|
||
<text x={RX+38} y={356} fontFamily="Caveat" fontSize={10} fill="#3a7cbf" fontStyle="italic">«extends»</text>
|
||
|
||
{/* Edit & Resubmit <<extends>> Submit */}
|
||
<line x1={LX} y1={598} x2={LX} y2={446}
|
||
stroke="#3a7cbf" strokeWidth={1.3} strokeDasharray="5,3"
|
||
markerEnd="url(#ucb)"/>
|
||
<text x={LX+8} y={525} fontFamily="Caveat" fontSize={10} fill="#3a7cbf" fontStyle="italic">«extends»</text>
|
||
|
||
{/* ── Use Cases ── */}
|
||
{lucs.map((uc,i) => (
|
||
<UC key={i} cx={LX} cy={uc.y} label={uc.label} rx={92}
|
||
color={i===7 ? '#d4e9f7' : '#fff'}
|
||
stroke={i===7 ? '#3a7cbf' : '#222'}/>
|
||
))}
|
||
{rucs.map((uc,i) => (
|
||
<UC key={i} cx={RX} cy={uc.y} label={uc.label} rx={92}/>
|
||
))}
|
||
|
||
{/* ── Actors ── */}
|
||
<Actor x={la.tech.x} y={la.tech.y} label={"Technical\nOfficer"} />
|
||
<Actor x={la.manning.x} y={la.manning.y} label={"Manning\nOfficer"} />
|
||
<Actor x={la.admin.x} y={la.admin.y} label="Admin" />
|
||
<Actor x={la.manager.x} y={la.manager.y} label="Manager" />
|
||
<Actor x={la.accounts.x} y={la.accounts.y} label="Accounts" />
|
||
|
||
{/* ── Legend ── */}
|
||
<g transform="translate(158,710)">
|
||
<line x1={0} y1={8} x2={30} y2={8} stroke="#bbb" strokeWidth={1.5}/>
|
||
<text x={36} y={12} fontFamily="Caveat" fontSize={11} fill="#888">Association</text>
|
||
<line x1={110} y1={8} x2={140} y2={8} stroke="#3a7cbf" strokeWidth={1.3} strokeDasharray="5,3"/>
|
||
<text x={146} y={12} fontFamily="Caveat" fontSize={11} fill="#3a7cbf">«includes» / «extends»</text>
|
||
<ellipse cx={330} cy={8} rx={22} ry={9} stroke="#222" strokeWidth={1.5} fill="#d4e9f7"/>
|
||
<text x={360} y={12} fontFamily="Caveat" fontSize={11} fill="#888">Shared use case</text>
|
||
</g>
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/* ─── ER DIAGRAM ─────────────────────────────────────── */
|
||
function ERDiagram() {
|
||
const W=1130, H=890;
|
||
|
||
const entities = {
|
||
user: {
|
||
x:15, y:40, w:185,
|
||
name:'USER', color:'#1a3a5c',
|
||
attrs:[
|
||
{name:'user_id', type:'uuid', pk:true},
|
||
{name:'employee_id', type:'varchar'},
|
||
{name:'full_name', type:'varchar'},
|
||
{name:'email', type:'varchar'},
|
||
{name:'role', type:'enum', enum:true},
|
||
{name:'department', type:'varchar'},
|
||
{name:'created_at', type:'timestamp'},
|
||
]
|
||
},
|
||
vessel: {
|
||
x:220, y:40, w:175,
|
||
name:'VESSEL', color:'#1a4a3a',
|
||
attrs:[
|
||
{name:'vessel_id', type:'uuid', pk:true},
|
||
{name:'name', type:'varchar'},
|
||
{name:'vessel_type', type:'varchar'},
|
||
{name:'flag_state', type:'varchar'},
|
||
]
|
||
},
|
||
account: {
|
||
x:415, y:40, w:185,
|
||
name:'ACCOUNT', color:'#3a1a5c',
|
||
attrs:[
|
||
{name:'account_id', type:'uuid', pk:true},
|
||
{name:'account_code', type:'varchar'},
|
||
{name:'department', type:'varchar'},
|
||
{name:'annual_budget', type:'decimal'},
|
||
{name:'budget_used', type:'decimal'},
|
||
]
|
||
},
|
||
vendor: {
|
||
x:620, y:40, w:185,
|
||
name:'VENDOR', color:'#5c2a5c',
|
||
attrs:[
|
||
{name:'vendor_id', type:'uuid', pk:true},
|
||
{name:'vendor_name', type:'varchar'},
|
||
{name:'contact_name', type:'varchar'},
|
||
{name:'email', type:'varchar'},
|
||
{name:'phone', type:'varchar'},
|
||
{name:'country', type:'varchar'},
|
||
]
|
||
},
|
||
notification: {
|
||
x:825, y:40, w:200,
|
||
name:'NOTIFICATION', color:'#5c3a1a',
|
||
attrs:[
|
||
{name:'notif_id', type:'uuid', pk:true},
|
||
{name:'recipient_id', type:'uuid', fk:true},
|
||
{name:'po_id', type:'uuid', fk:true},
|
||
{name:'message', type:'text'},
|
||
{name:'is_read', type:'boolean'},
|
||
{name:'created_at', type:'timestamp'},
|
||
]
|
||
},
|
||
po: {
|
||
x:290, y:315, w:265,
|
||
name:'PURCHASE_ORDER', color:'#222',
|
||
attrs:[
|
||
{name:'po_id', type:'uuid', pk:true},
|
||
{name:'po_number', type:'varchar'},
|
||
{name:'status', type:'enum', enum:true},
|
||
{name:'priority', type:'enum', enum:true},
|
||
{name:'total_cost', type:'decimal'},
|
||
{name:'notes', type:'text'},
|
||
{name:'vessel_id', type:'uuid', fk:true},
|
||
{name:'account_id', type:'uuid', fk:true},
|
||
{name:'vendor_id', type:'uuid', fk:true},
|
||
{name:'submitted_by', type:'uuid', fk:true},
|
||
{name:'reviewed_by', type:'uuid', fk:true},
|
||
{name:'paid_by', type:'uuid', fk:true},
|
||
{name:'submitted_at', type:'timestamp'},
|
||
{name:'created_at', type:'timestamp'},
|
||
]
|
||
},
|
||
lineitem: {
|
||
x:15, y:650, w:190,
|
||
name:'PO_LINE_ITEM', color:'#2a4a6a',
|
||
attrs:[
|
||
{name:'line_item_id', type:'uuid', pk:true},
|
||
{name:'po_id', type:'uuid', fk:true},
|
||
{name:'item_name', type:'varchar'},
|
||
{name:'item_code', type:'varchar'},
|
||
{name:'quantity', type:'int'},
|
||
{name:'unit_cost', type:'decimal'},
|
||
{name:'total_cost', type:'decimal'},
|
||
]
|
||
},
|
||
action: {
|
||
x:225, y:650, w:205,
|
||
name:'PO_ACTION', color:'#4a2a2a',
|
||
attrs:[
|
||
{name:'action_id', type:'uuid', pk:true},
|
||
{name:'po_id', type:'uuid', fk:true},
|
||
{name:'actor_id', type:'uuid', fk:true},
|
||
{name:'action_type', type:'enum', enum:true},
|
||
{name:'comment', type:'text'},
|
||
{name:'created_at', type:'timestamp'},
|
||
]
|
||
},
|
||
document: {
|
||
x:450, y:650, w:185,
|
||
name:'PO_DOCUMENT', color:'#2a4a2a',
|
||
attrs:[
|
||
{name:'document_id', type:'uuid', pk:true},
|
||
{name:'po_id', type:'uuid', fk:true},
|
||
{name:'filename', type:'varchar'},
|
||
{name:'file_url', type:'varchar'},
|
||
{name:'uploaded_by', type:'uuid', fk:true},
|
||
{name:'uploaded_at', type:'timestamp'},
|
||
]
|
||
},
|
||
receipt: {
|
||
x:655, y:650, w:200,
|
||
name:'RECEIPT', color:'#2a4a5c',
|
||
attrs:[
|
||
{name:'receipt_id', type:'uuid', pk:true},
|
||
{name:'po_id', type:'uuid', fk:true},
|
||
{name:'file_url', type:'varchar'},
|
||
{name:'amount_paid', type:'decimal'},
|
||
{name:'uploaded_by', type:'uuid', fk:true},
|
||
{name:'confirmed_by', type:'uuid', fk:true},
|
||
{name:'confirmed_at', type:'timestamp'},
|
||
]
|
||
},
|
||
};
|
||
|
||
const pt = (e, side) => {
|
||
const ROW=19, HEADER=28;
|
||
const h = HEADER + e.attrs.length * ROW + 8;
|
||
if (side==='top') return [e.x+e.w/2, e.y];
|
||
if (side==='bottom') return [e.x+e.w/2, e.y+h];
|
||
if (side==='left') return [e.x, e.y+h/2];
|
||
if (side==='right') return [e.x+e.w, e.y+h/2];
|
||
return [e.x+e.w/2, e.y+h/2];
|
||
};
|
||
|
||
const E = entities;
|
||
|
||
return (
|
||
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ background:'#fafaf8', borderRadius:4 }}>
|
||
|
||
{/* USER → PO (submitted_by) */}
|
||
<Conn x1={pt(E.user,'bottom')[0]-10} y1={pt(E.user,'bottom')[1]}
|
||
x2={pt(E.po,'left')[0]} y2={pt(E.po,'left')[1]-45}
|
||
card1="1" card2="N" label="submits" color="#1a3a5c"/>
|
||
{/* USER → PO (reviewed_by) */}
|
||
<Conn x1={pt(E.user,'bottom')[0]+10} y1={pt(E.user,'bottom')[1]}
|
||
x2={pt(E.po,'left')[0]} y2={pt(E.po,'left')[1]}
|
||
card1="1" card2="N" label="reviews" color="#c8971a"/>
|
||
{/* USER → PO (paid_by) */}
|
||
<Conn x1={pt(E.user,'right')[0]} y1={pt(E.user,'right')[1]+20}
|
||
x2={pt(E.po,'left')[0]} y2={pt(E.po,'left')[1]+45}
|
||
card1="1" card2="N" label="pays" color="#2a8a50"/>
|
||
|
||
{/* VESSEL → PO */}
|
||
<Conn x1={pt(E.vessel,'bottom')[0]} y1={pt(E.vessel,'bottom')[1]}
|
||
x2={pt(E.po,'top')[0]-50} y2={pt(E.po,'top')[1]}
|
||
card1="1" card2="N" label="assigned to" color="#1a4a3a"/>
|
||
{/* ACCOUNT → PO */}
|
||
<Conn x1={pt(E.account,'bottom')[0]} y1={pt(E.account,'bottom')[1]}
|
||
x2={pt(E.po,'top')[0]} y2={pt(E.po,'top')[1]}
|
||
card1="1" card2="N" label="charged to" color="#3a1a5c"/>
|
||
{/* VENDOR → PO (NEW) */}
|
||
<Conn x1={pt(E.vendor,'bottom')[0]} y1={pt(E.vendor,'bottom')[1]}
|
||
x2={pt(E.po,'top')[0]+55} y2={pt(E.po,'top')[1]}
|
||
card1="1" card2="N" label="supplies" color="#5c2a5c"/>
|
||
|
||
{/* NOTIFICATION → USER */}
|
||
<Conn x1={pt(E.notification,'left')[0]} y1={pt(E.notification,'left')[1]-10}
|
||
x2={pt(E.user,'right')[0]} y2={pt(E.user,'right')[1]-10}
|
||
card1="N" card2="1" label="sent to" color="#5c3a1a" dashed/>
|
||
{/* NOTIFICATION → PO */}
|
||
<Conn x1={pt(E.notification,'bottom')[0]} y1={pt(E.notification,'bottom')[1]}
|
||
x2={pt(E.po,'right')[0]} y2={pt(E.po,'right')[1]-20}
|
||
card1="N" card2="1" label="about" color="#5c3a1a" dashed/>
|
||
|
||
{/* PO → PO_LINE_ITEM */}
|
||
<Conn x1={pt(E.po,'bottom')[0]-50} y1={pt(E.po,'bottom')[1]}
|
||
x2={pt(E.lineitem,'top')[0]} y2={pt(E.lineitem,'top')[1]}
|
||
card1="1" card2="N" label="has" color="#2a4a6a"/>
|
||
{/* PO → PO_ACTION */}
|
||
<Conn x1={pt(E.po,'bottom')[0]-10} y1={pt(E.po,'bottom')[1]}
|
||
x2={pt(E.action,'top')[0]} y2={pt(E.action,'top')[1]}
|
||
card1="1" card2="N" label="audit trail" color="#4a2a2a"/>
|
||
{/* PO → PO_DOCUMENT */}
|
||
<Conn x1={pt(E.po,'bottom')[0]+20} y1={pt(E.po,'bottom')[1]}
|
||
x2={pt(E.document,'top')[0]} y2={pt(E.document,'top')[1]}
|
||
card1="1" card2="N" label="attaches" color="#2a4a2a"/>
|
||
{/* PO → RECEIPT (NEW) */}
|
||
<Conn x1={pt(E.po,'bottom')[0]+55} y1={pt(E.po,'bottom')[1]}
|
||
x2={pt(E.receipt,'top')[0]} y2={pt(E.receipt,'top')[1]}
|
||
card1="1" card2="1" label="has receipt" color="#2a4a5c"/>
|
||
|
||
{/* USER → PO_ACTION */}
|
||
<Conn x1={pt(E.user,'bottom')[0]-20} y1={pt(E.user,'bottom')[1]}
|
||
x2={pt(E.action,'left')[0]} y2={pt(E.action,'left')[1]}
|
||
card1="1" card2="N" label="performs" color="#888" dashed/>
|
||
{/* USER → PO_DOCUMENT */}
|
||
<Conn x1={pt(E.user,'right')[0]} y1={pt(E.user,'right')[1]+40}
|
||
x2={pt(E.document,'top')[0]-20} y2={pt(E.document,'top')[1]}
|
||
card1="1" card2="N" label="uploads" color="#888" dashed/>
|
||
{/* USER → RECEIPT (confirmed_by) (NEW) */}
|
||
<Conn x1={pt(E.user,'right')[0]} y1={pt(E.user,'right')[1]+60}
|
||
x2={pt(E.receipt,'left')[0]} y2={pt(E.receipt,'left')[1]}
|
||
card1="1" card2="N" label="confirms" color="#2a4a5c" dashed/>
|
||
|
||
{/* ── Entities ── */}
|
||
{Object.values(entities).map(e => (
|
||
<EntityBox key={e.name} x={e.x} y={e.y} w={e.w}
|
||
name={e.name} attrs={e.attrs} headerColor={e.color}/>
|
||
))}
|
||
|
||
{/* NEW badges */}
|
||
{[{x:620+92, y:30},{x:655+100, y:640}].map((b,i) => (
|
||
<g key={i}>
|
||
<rect x={b.x-18} y={b.y-8} width={36} height={16} rx={8} fill="#5c2a5c"/>
|
||
<text x={b.x} y={b.y+4} textAnchor="middle" fontFamily="Caveat"
|
||
fontSize={10} fontWeight="700" fill="#fff">NEW</text>
|
||
</g>
|
||
))}
|
||
{/* vendor_id FK badge on PO */}
|
||
<rect x={pt(E.po,'right')[0]+4} y={pt(E.po,'right')[1]-50}
|
||
width={60} height={14} rx={7} fill="#5c2a5c" fillOpacity={0.15}
|
||
stroke="#5c2a5c" strokeWidth={1}/>
|
||
<text x={pt(E.po,'right')[0]+34} y={pt(E.po,'right')[1]-40}
|
||
textAnchor="middle" fontFamily="Caveat" fontSize={9} fill="#5c2a5c">+vendor_id</text>
|
||
|
||
{/* ── Legend ── */}
|
||
<g transform="translate(15,850)">
|
||
<rect x={0} y={-14} width={1100} height={30} rx={2}
|
||
fill="#f0ede6" stroke="#ccc" strokeWidth={1}/>
|
||
<line x1={8} y1={2} x2={40} y2={2} stroke="#444" strokeWidth={1.5}/>
|
||
<text x={46} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Relationship</text>
|
||
<text x={135} y={6} fontFamily="Space Mono" fontSize={10} fontWeight="700" fill="#3a7cbf">PK</text>
|
||
<text x={153} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Primary Key</text>
|
||
<text x={255} y={6} fontFamily="Space Mono" fontSize={10} fontWeight="700" fill="#2a8a50">FK</text>
|
||
<text x={273} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Foreign Key</text>
|
||
<text x={372} y={6} fontFamily="Space Mono" fontSize={10} fontWeight="700" fill="#7a4abf">∑</text>
|
||
<text x={386} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Enum</text>
|
||
<text x={440} y={6} fontFamily="Space Mono" fontSize={10} fontWeight="700" fill="#444">1/N</text>
|
||
<text x={458} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Cardinality</text>
|
||
<line x1={545} y1={2} x2={577} y2={2} stroke="#888" strokeWidth={1.5} strokeDasharray="5,3"/>
|
||
<text x={583} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Indirect ref</text>
|
||
<rect x={670} y={-7} width={32} height={16} rx={8} fill="#5c2a5c"/>
|
||
<text x={686} y={5} textAnchor="middle" fontFamily="Caveat" fontSize={10}
|
||
fontWeight="700" fill="#fff">NEW</text>
|
||
<text x={708} y={6} fontFamily="Caveat" fontSize={11} fill="#555">Added in v2 (Vendor, Receipt)</text>
|
||
</g>
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/* ─── STATUS ENUM REFERENCE ─────────────────────────── */
|
||
function EnumRef() {
|
||
return (
|
||
<svg width={440} height={200} viewBox="0 0 440 200" style={{ background:'#fafaf8', borderRadius:4 }}>
|
||
<text x={220} y={22} textAnchor="middle" fontFamily="Caveat" fontSize={15} fontWeight="700" fill="#222">
|
||
PURCHASE_ORDER.status — Enum Values
|
||
</text>
|
||
|
||
{/* Flow arrow */}
|
||
{[
|
||
{ val:'draft', label:'Draft', x:30, color:'#888' },
|
||
{ val:'submitted', label:'Submitted', x:100, color:'#3a7cbf' },
|
||
{ val:'mgr_review', label:'Mgr. Review', x:185, color:'#c8971a' },
|
||
{ val:'edits_requested', label:'Edits Requested', x:275, color:'#c03030' },
|
||
{ val:'rejected', label:'Rejected', x:365, color:'#c03030' },
|
||
].map((s,i) => (
|
||
<g key={i}>
|
||
<rect x={s.x} y={45} width={65} height={26} rx={3}
|
||
fill={s.color} stroke={s.color} strokeWidth={1.5}/>
|
||
<text x={s.x+32} y={62} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={11} fill="#fff">{s.label}</text>
|
||
{i<4 && <text x={s.x+72} y={62} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={14} fill="#aaa">→</text>}
|
||
</g>
|
||
))}
|
||
|
||
{/* Second row: the approval chain */}
|
||
{[
|
||
{ val:'submitted', label:'Submitted', x:30, color:'#3a7cbf' },
|
||
{ val:'mgr_review', label:'Mgr. Review', x:110, color:'#c8971a' },
|
||
{ val:'approved', label:'Mgr. Approved', x:200, color:'#2a8a50' },
|
||
{ val:'payment', label:'Awaiting Pmt', x:300, color:'#3a7cbf' },
|
||
{ val:'paid', label:'Paid ✓', x:390, color:'#1a5a2a' },
|
||
].map((s,i) => (
|
||
<g key={i}>
|
||
<rect x={s.x} y={100} width={72} height={26} rx={3}
|
||
fill={s.color} stroke={s.color} strokeWidth={1.5}/>
|
||
<text x={s.x+36} y={117} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={11} fill="#fff">{s.label}</text>
|
||
{i<4 && <text x={s.x+78} y={117} textAnchor="middle"
|
||
fontFamily="Caveat" fontSize={14} fill="#aaa">→</text>}
|
||
</g>
|
||
))}
|
||
|
||
<text x={14} y={42} fontFamily="Caveat" fontSize={11} fill="#888" fontStyle="italic">rejection / edit path:</text>
|
||
<text x={14} y={97} fontFamily="Caveat" fontSize={11} fill="#888" fontStyle="italic">approval path:</text>
|
||
|
||
{/* PO_ACTION.action_type enum */}
|
||
<text x={14} y={155} fontFamily="Caveat" fontSize={12} fontWeight="700" fill="#555">PO_ACTION.action_type:</text>
|
||
{['submitted','approved','rejected','edits_requested','approved_with_note','paid'].map((v,i) => (
|
||
<g key={i}>
|
||
<rect x={14+i*72} y={162} width={68} height={22} rx={2}
|
||
fill="#f0ede6" stroke="#ccc" strokeWidth={1.2}/>
|
||
<text x={14+i*72+34} y={177} textAnchor="middle"
|
||
fontFamily="Space Mono" fontSize={8} fill="#555">{v}</text>
|
||
</g>
|
||
))}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/* ─── APP ────────────────────────────────────────────── */
|
||
function App() {
|
||
return (
|
||
<DesignCanvas title="Pelagia Marine Portal — System Diagrams">
|
||
|
||
<DCSection id="s1" title="Use Case Diagram — Purchasing Module">
|
||
<DCArtboard id="ucd" label="Use Case Diagram" width={930} height={780}>
|
||
<UseCaseDiagram/>
|
||
</DCArtboard>
|
||
</DCSection>
|
||
|
||
<DCSection id="s2" title="Entity Relationship Diagram">
|
||
<DCArtboard id="erd" label="ER Diagram (v2 — VENDOR + RECEIPT added)" width={1130} height={890}>
|
||
<ERDiagram/>
|
||
</DCArtboard>
|
||
<DCArtboard id="enums" label="Status Enum Reference" width={440} height={200}>
|
||
<EnumRef/>
|
||
</DCArtboard>
|
||
</DCSection>
|
||
|
||
</DesignCanvas>
|
||
);
|
||
}
|
||
|
||
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
|
||
</script>
|
||
</body>
|
||
</html>
|