pelagia-portal/Prototype/Pelagia Diagrams.html
Hardik d769cae71e chore(inventory): remove item detail page; move SiteSelect to shared components
- Delete /inventory/items/[id] — items expand inline in the list
- Move SiteSelect from deleted [id] folder to components/inventory/site-select
- Fix admin product detail page import to use new shared path
- Fix items-table: Fragment key prop, restore Link import, plain text item names
- Fix vendor-items-table: remove broken link to deleted item detail page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:07:04 +05:30

646 lines
27 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>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 06 (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:'imo_number', 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>