- 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>
227 lines
8.2 KiB
JavaScript
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 />);
|