pelagia-portal/design_handoff_pelagia_portal/app.jsx
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

227 lines
8.2 KiB
JavaScript

/* Pelagia Portal — main app with router, sidebar, header */
const NAV_BY_ROLE = {
TECHNICAL: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["po-new", "New PO", "plus"],
["my-orders", "My Orders", "list", 3],
],
Inventory: [
["items", "Items", "box"],
["cart", "Cart", "cart", 3],
],
},
MANNING: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["po-new", "New PO", "plus"],
["my-orders", "My Orders", "list", 2],
],
Inventory: [
["items", "Items", "box"],
["cart", "Cart", "cart"],
],
},
MANAGER: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["po-new", "New PO", "plus"],
["my-orders", "My Orders", "list"],
["approvals", "Approvals", "check", 6],
["import-po", "Import PO", "upload"],
["history", "History / Export", "file"],
],
Inventory: [
["vendors", "Vendors", "users"],
["items", "Items", "box"],
["vessels", "Vessels", "ship"],
["sites", "Sites", "map"],
["cart", "Cart", "cart"],
],
},
ACCOUNTS: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["payments", "Payments", "truck", 4],
["history", "History / Export", "file"],
],
Inventory: [
["vendors", "Vendors", "users"],
],
},
SUPERUSER: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["po-new", "New PO", "plus"],
["my-orders", "My Orders", "list"],
["approvals", "Approvals", "check", 6],
["payments", "Payments", "truck", 4],
["import-po", "Import PO", "upload"],
["history", "History / Export", "file"],
],
Inventory: [
["vendors", "Vendors", "users"],
["items", "Items", "box"],
["vessels", "Vessels", "ship"],
["sites", "Sites", "map"],
["cart", "Cart", "cart"],
],
},
AUDITOR: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["history", "History / Export", "file"],
],
},
ADMIN: {
main: [["dashboard", "Dashboard", "home"]],
"Purchase Orders": [
["import-po", "Import PO", "upload"],
["history", "History / Export", "file"],
],
Inventory: [
["vendors", "Vendors", "users"],
["items", "Items", "box"],
["vessels", "Vessels", "ship"],
["sites", "Sites", "map"],
],
Administration: [
["users", "Users", "user"],
["accounts", "Accounts", "settings"],
],
},
};
const USER_BY_ROLE = {
TECHNICAL: { name: "Rajesh Pillai", short: "RP" },
MANNING: { name: "Fatima Sheikh", short: "FS" },
MANAGER: { name: "Anjali Krishnan", short: "AK" },
ACCOUNTS: { name: "Vikram Iyer", short: "VI" },
SUPERUSER: { name: "Dev Shah", short: "DS" },
AUDITOR: { name: "Lakshmi Rao", short: "LR" },
ADMIN: { name: "System Admin", short: "SA" },
};
const Sidebar = ({ role, route, navigate }) => {
const nav = NAV_BY_ROLE[role] || NAV_BY_ROLE.MANAGER;
return (
<aside className="sidebar">
{nav.main && nav.main.map(([key, label, icon, count]) => (
<div key={key} className={`nav-item ${route.page === key ? "active" : ""}`} onClick={() => navigate(key)}>
<Icon name={icon} />
<span>{label}</span>
{count != null && <span className="nav-count">{count}</span>}
</div>
))}
{Object.entries(nav).filter(([k]) => k !== "main").map(([groupName, items]) => (
<div className="nav-group" key={groupName}>
<div className="nav-group-title">{groupName}</div>
{items.map(([key, label, icon, count]) => (
<div key={key} className={`nav-item ${route.page === key ? "active" : ""}`} onClick={() => navigate(key)}>
<Icon name={icon} />
<span>{label}</span>
{count != null && <span className="nav-count">{count}</span>}
</div>
))}
</div>
))}
</aside>
);
};
const Header = ({ role, setRole, navigate }) => {
const u = USER_BY_ROLE[role];
return (
<header className="app-header">
<div className="brand" onClick={() => navigate("dashboard")} style={{ cursor: "pointer" }}>
<div className="brand-mark" />
<div className="brand-name">Pelagia Portal</div>
</div>
<div className="header-search">
<Icon name="search" size={12} />
<input placeholder="Search POs, vendors, items…" />
<span className="kbd">K</span>
</div>
<div className="header-spacer" />
<div className="role-pill" title="Switch role to preview navigation (prototype only)">
<span className="muted">Role</span>
<select value={role} onChange={e => setRole(e.target.value)}>
{Object.keys(NAV_BY_ROLE).map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<button className="btn icon" title="Notifications"><Icon name="bell" size={13} /></button>
<div className="user-chip">
<div className="user-avatar">{u.short}</div>
<div>
<div style={{ fontWeight: 500 }}>{u.name}</div>
<div className="muted" style={{ fontSize: 11 }}>{role.toLowerCase()}</div>
</div>
</div>
</header>
);
};
const App = () => {
const [authed, setAuthed] = useS(() => sessionStorage.getItem("pelagia.authed") === "1");
const [role, setRole] = useS(() => sessionStorage.getItem("pelagia.role") || "MANAGER");
const [route, setRoute] = useS(() => parseHash() || { page: "dashboard" });
useE(() => {
const onHash = () => setRoute(parseHash() || { page: "dashboard" });
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
useE(() => sessionStorage.setItem("pelagia.role", role), [role]);
useE(() => sessionStorage.setItem("pelagia.authed", authed ? "1" : "0"), [authed]);
const navigate = (page, id) => {
window.location.hash = id ? `#${page}/${id}` : `#${page}`;
};
if (!authed) return <LoginPage onLogin={() => setAuthed(true)} />;
const page = route.page;
const id = route.id;
let body;
switch (page) {
case "dashboard": body = <Dashboard role={role} go={navigate} />; break;
case "my-orders": body = <MyOrders go={navigate} />; break;
case "approvals": body = <ApprovalsPage go={navigate} />; break;
case "approval-detail": body = <PODetailBase isApproval order={ORDERS.find(o => o.id === id) || APPROVAL_QUEUE.find(o => o.id === id) || ORDERS[0]} go={navigate} role={role} />; break;
case "po-detail": body = <PODetailBase order={ORDERS.find(o => o.id === id) || ORDERS[0]} go={navigate} role={role} />; break;
case "po-new": body = <NewPOPage go={navigate} />; break;
case "payments": body = <PaymentsPage go={navigate} />; break;
case "history": body = <HistoryPage go={navigate} />; break;
case "vendors": body = <VendorsPage go={navigate} />; break;
case "vendor-detail": body = <VendorDetailPage go={navigate} id={id} />; break;
case "items": body = <ItemsPage go={navigate} />; break;
case "item-detail": body = <ItemDetailPage go={navigate} id={id} />; break;
case "sites": body = <SitesPage go={navigate} />; break;
case "site-detail": body = <SiteDetailPage go={navigate} id={id} />; break;
case "vessels": body = <VesselsPage />; break;
case "accounts": body = <AccountsPage />; break;
case "users": body = <UsersPage />; break;
case "import-po": body = <ImportPOPage go={navigate} />; break;
case "cart": body = <CartPage go={navigate} />; break;
default: body = <Dashboard role={role} go={navigate} />;
}
return (
<div className="app">
<Header role={role} setRole={setRole} navigate={navigate} />
<Sidebar role={role} route={route} navigate={navigate} />
<main className="main" key={page + (id || "")}>{body}</main>
</div>
);
};
function parseHash() {
const h = window.location.hash.replace(/^#/, "");
if (!h) return null;
const [page, id] = h.split("/");
return { page, id };
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);