pelagia-portal/design_handoff_pelagia_portal/pages-4.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

451 lines
21 KiB
JavaScript
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.

/* Pelagia Portal — Sites, Cart, Vessels, Accounts, Users, Import PO */
/* ═══════════════════ SITES ═══════════════════ */
const SitesPage = ({ go }) => (
<>
<div className="page-head">
<div>
<Crumbs items={["Inventory", "Sites"]} />
<h1 className="page-title">Sites</h1>
<div className="page-sub">{SITES.length} ports, depots, and offices that hold inventory</div>
</div>
<button className="btn maritime"><Icon name="plus" /> Add site</button>
</div>
<Card flush>
<table className="table">
<thead>
<tr>
<th>Name</th><th>Code</th><th>Address</th>
<th className="num">Vessels</th><th className="num">Items</th>
<th>Location</th><th>Status</th><th></th>
</tr>
</thead>
<tbody>
{SITES.map(s => (
<tr key={s.id} className="clickable" onClick={() => go("site-detail", s.id)}>
<td style={{ fontWeight: 500 }}>{s.name}</td>
<td className="mono" style={{ fontSize: 12 }}>{s.code}</td>
<td className="muted">{s.address}</td>
<td className="num mono">{s.vessels}</td>
<td className="num mono">{s.items}</td>
<td className="mono" style={{ fontSize: 11.5 }}>{["9.95° N","19.04° N","13.07° N","17.69° N"][SITES.indexOf(s)]} · {["76.26° E","72.85° E","80.26° E","83.21° E"][SITES.indexOf(s)]}</td>
<td><Badge className="closed" noDot>Active</Badge></td>
<td style={{ textAlign: "right" }}><button className="btn sm icon" onClick={e => e.stopPropagation()}><Icon name="edit" size={11} /></button></td>
</tr>
))}
</tbody>
</table>
</Card>
</>
);
/* ═══════════════════ SITE DETAIL ═══════════════════ */
const SiteDetailPage = ({ go, id }) => {
const s = SITES.find(x => x.id === id) || SITES[0];
const max = Math.max(...SITE_INVENTORY.map(i => i.qty));
return (
<>
<div className="page-head">
<div>
<Crumbs items={["Inventory", "Sites", s.name]} />
<h1 className="page-title">{s.name}</h1>
<div className="page-sub">
<span className="mono">{s.code}</span><span className="dot-sep">·</span>
{s.address}<span className="dot-sep">·</span>
<span className="mono">9.9489° N · 76.2622° E</span>
</div>
</div>
<button className="btn"><Icon name="edit" /> Edit site</button>
</div>
<div className="stat-grid" style={{ marginBottom: 22 }}>
<Stat label="Vessels at site" value={s.vessels} />
<Stat label="Items tracked" value={s.items} />
<Stat label="Inventory value" value="₹18.6L" sub={<span className="muted">last calculated 12:30</span>} />
<Stat label="Consumption · 30d" value="412 ea" sub={<span className="muted">across all items</span>} />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginBottom: 16 }}>
<Card title="Current stock" action={<span className="muted" style={{ fontSize: 11.5 }}>Quantity on hand</span>}>
<div style={{ paddingTop: 4 }}>
{SITE_INVENTORY.map((it, i) => (
<div className="hbar-row" key={i}>
<div className="name">{it.name}</div>
<div className="track"><div className="fill" style={{ width: (it.qty / max * 100) + "%" }} /></div>
<div className="v">{it.qty} ea</div>
</div>
))}
</div>
</Card>
<Card title="Consumption · last 12 days" action={<span className="muted" style={{ fontSize: 11.5 }}>Daily draw-down</span>}>
<div style={{ height: 160, position: "relative", paddingTop: 4 }}>
<svg viewBox="0 0 360 140" width="100%" height="140" preserveAspectRatio="none">
{/* grid lines */}
{[0, 1, 2, 3].map(i => (
<line key={i} x1="0" x2="360" y1={20 + i * 30} y2={20 + i * 30}
stroke="var(--line)" strokeWidth="1" strokeDasharray="2 3" />
))}
{[0, 1, 2, 3, 4].map(seriesIdx => {
const stroke = ["var(--primary)", "oklch(55% 0.09 30)", "oklch(50% 0.06 150)", "oklch(50% 0.07 280)", "oklch(55% 0.08 60)"][seriesIdx];
const pts = CONSUMPTION.map((d, i) => {
const x = (i / (CONSUMPTION.length - 1)) * 350 + 5;
const y = 130 - (d.vals[seriesIdx] / 4) * 110;
return `${x},${y}`;
}).join(" ");
return (
<polyline key={seriesIdx}
points={pts}
fill="none"
stroke={stroke}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.85"
/>
);
})}
</svg>
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px 12px", marginTop: 6, fontSize: 11 }}>
{SITE_INVENTORY.slice(0, 5).map((it, i) => (
<span key={i} style={{ display: "inline-flex", alignItems: "center", gap: 5, color: "var(--muted)" }}>
<span style={{ width: 10, height: 2, background: ["var(--primary)", "oklch(55% 0.09 30)", "oklch(50% 0.06 150)", "oklch(50% 0.07 280)", "oklch(55% 0.08 60)"][i] }} />
{it.name.split("—")[0].trim().split(" ").slice(0,2).join(" ")}
</span>
))}
</div>
</div>
</Card>
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<div>
<h2 className="section-title">Inventory</h2>
<Card flush>
<table className="table">
<thead><tr><th>Product</th><th className="num">Qty on hand</th><th>Last updated</th><th></th></tr></thead>
<tbody>
{SITE_INVENTORY.map((it, i) => (
<tr key={i} className="clickable" onClick={() => go("item-detail", "p1")}>
<td>{it.name}</td>
<td className="num mono">{it.qty} ea</td>
<td className="muted">{it.updated}</td>
<td style={{ textAlign: "right" }}><Icon name="arrowRight" size={11} /></td>
</tr>
))}
</tbody>
</table>
</Card>
<h2 className="section-title">Recent POs for this site</h2>
<Card flush>
<table className="table">
<thead><tr><th>PO Number</th><th>Status</th><th>Vendor</th><th>Created</th><th className="num">Amount</th></tr></thead>
<tbody>
{ORDERS.slice(0, 5).map(o => (
<tr key={o.id} className="clickable" onClick={() => go("po-detail", o.id)}>
<td><span className="po-num">{o.id}</span></td>
<td><Badge status={o.status} /></td>
<td className="muted">{o.vendor || "—"}</td>
<td className="muted">{o.submitted || "—"}</td>
<td className="num">{inr(o.amount)}</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
<div>
<Card title="Log consumption">
<div className="form-row" style={{ gap: 10 }}>
<div className="field">
<label className="field-label">Product</label>
<select className="select">{SITE_INVENTORY.map((i, idx) => <option key={idx}>{i.name}</option>)}</select>
</div>
<div className="form-row cols-2">
<div className="field">
<label className="field-label">Date</label>
<input className="input" type="date" defaultValue="2026-05-12" />
</div>
<div className="field">
<label className="field-label">Quantity</label>
<input className="input mono" defaultValue="3" />
</div>
</div>
<div className="field">
<label className="field-label">Note (optional)</label>
<textarea className="textarea" style={{ minHeight: 56 }} placeholder="e.g. issued to MV Pelagia Voyager"></textarea>
</div>
<button className="btn maritime">Log consumption</button>
</div>
</Card>
<div style={{ height: 14 }} />
<Card title="Assigned vessels">
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
<span className="tag-chip has-link">MV Pelagia Voyager</span>
<span className="tag-chip has-link">MV Coral Crescent</span>
</div>
</Card>
</div>
</div>
</>
);
};
/* ═══════════════════ VESSELS ═══════════════════ */
const VesselsPage = () => (
<>
<div className="page-head">
<div>
<Crumbs items={["Inventory", "Vessels"]} />
<h1 className="page-title">Vessels</h1>
<div className="page-sub">{VESSELS.length} vessels in service</div>
</div>
<button className="btn maritime"><Icon name="plus" /> Add vessel</button>
</div>
<Card flush>
<table className="table">
<thead><tr><th>Name</th><th>IMO Number</th><th>Assigned site</th><th>Status</th><th></th></tr></thead>
<tbody>
{VESSELS.map(v => (
<tr key={v.id}>
<td style={{ fontWeight: 500 }}>{v.name}</td>
<td className="mono">{v.imo}</td>
<td className="muted">{["Cochin Port Depot","Mumbai BPX Office","Visakhapatnam Yard","Chennai South Dock","Cochin Port Depot"][VESSELS.indexOf(v)]}</td>
<td><Badge className="closed" noDot>Active</Badge></td>
<td style={{ textAlign: "right" }}><button className="btn sm icon"><Icon name="edit" size={11} /></button></td>
</tr>
))}
</tbody>
</table>
</Card>
</>
);
/* ═══════════════════ ACCOUNTS ═══════════════════ */
const AccountsPage = () => (
<>
<div className="page-head">
<div>
<Crumbs items={["Administration", "Accounts"]} />
<h1 className="page-title">Accounts / Cost centres</h1>
<div className="page-sub">{ACCOUNTS.length} active cost centres</div>
</div>
<button className="btn maritime"><Icon name="plus" /> Add account</button>
</div>
<Card flush>
<table className="table">
<thead><tr><th>Code</th><th>Name</th><th>Description</th><th>Status</th><th></th></tr></thead>
<tbody>
{ACCOUNTS.map(a => (
<tr key={a.id}>
<td className="mono" style={{ fontSize: 12 }}>{a.code}</td>
<td style={{ fontWeight: 500 }}>{a.name}</td>
<td className="muted">{a.desc}</td>
<td><Badge className="closed" noDot>Active</Badge></td>
<td style={{ textAlign: "right" }}><button className="btn sm icon"><Icon name="edit" size={11} /></button></td>
</tr>
))}
</tbody>
</table>
</Card>
</>
);
/* ═══════════════════ USERS ═══════════════════ */
const UsersPage = () => (
<>
<div className="page-head">
<div>
<Crumbs items={["Administration", "Users"]} />
<h1 className="page-title">Users</h1>
<div className="page-sub">{USERS.length} active users · 7 roles</div>
</div>
<button className="btn maritime"><Icon name="plus" /> Add user</button>
</div>
<Card flush>
<table className="table">
<thead><tr><th>Employee ID</th><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th>Created</th><th></th></tr></thead>
<tbody>
{USERS.map(u => (
<tr key={u.id}>
<td className="mono" style={{ fontSize: 12 }}>{u.emp}</td>
<td style={{ fontWeight: 500 }}>{u.name}</td>
<td className="muted">{u.email}</td>
<td><span className="role-badge">{u.role}</span></td>
<td><Badge className="closed" noDot>Active</Badge></td>
<td className="muted">{u.created}</td>
<td style={{ textAlign: "right" }}><button className="btn sm icon"><Icon name="edit" size={11} /></button></td>
</tr>
))}
</tbody>
</table>
</Card>
</>
);
/* ═══════════════════ CART ═══════════════════ */
const CartPage = ({ go }) => {
const [items, setItems] = useS([
{ id: 1, name: "Marine Bearing 6310-2RS", desc: "50×110×27mm sealed", vendor: "Mahalakshmi Marine Stores", price: 4250, qty: 24, gst: 18 },
{ id: 2, name: "Lube Oil Filter — Element", desc: "Cellulose, 10µ", vendor: "Coastline Engineering Co.", price: 1180, qty: 48, gst: 18 },
{ id: 3, name: "CO₂ Fire Extinguisher 9kg", desc: "Marine-grade, BIS certified", vendor: "Sealine Maritime Pvt Ltd", price: 8400, qty: 4, gst: 18 },
]);
const taxable = items.reduce((s, i) => s + i.price * i.qty, 0);
const gst = items.reduce((s, i) => s + i.price * i.qty * i.gst / 100, 0);
return (
<>
<div className="page-head">
<div>
<Crumbs items={["Inventory", "Cart"]} />
<h1 className="page-title">Cart</h1>
<div className="page-sub">{items.length} items · saved locally to this device</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button className="btn" onClick={() => setItems([])}>Clear cart</button>
<button className="btn maritime" onClick={() => go("po-new")}><Icon name="plus" /> Create PO from cart</button>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Card>
{items.length === 0 ? <div className="empty-state">Cart is empty. Add items from the Item catalogue.</div> : (
<>
<div style={{ display: "grid", gridTemplateColumns: "1fr 80px 100px 80px 24px", gap: 14, padding: "0 0 8px", fontSize: 11, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.06em", borderBottom: "1px solid var(--line)" }}>
<div>Item · Vendor</div>
<div style={{ textAlign: "right" }}>Unit price</div>
<div style={{ textAlign: "center" }}>Quantity</div>
<div style={{ textAlign: "right" }}>Subtotal</div>
<div></div>
</div>
{items.map(it => (
<div className="cart-line" key={it.id}>
<div>
<div style={{ fontWeight: 500 }}>{it.name}</div>
<div className="muted" style={{ fontSize: 11.5 }}>{it.desc} · <span style={{ color: "var(--primary-ink)" }}>{it.vendor}</span></div>
</div>
<div className="num mono">{inrFull(it.price)}</div>
<div style={{ display: "flex", justifyContent: "center" }}>
<input className="input mono" value={it.qty}
onChange={e => setItems(items.map(x => x.id === it.id ? { ...x, qty: Number(e.target.value) || 0 } : x))}
style={{ height: 28, width: 64, textAlign: "right", fontSize: 12 }} />
</div>
<div className="num mono">{inrFull(it.price * it.qty)}</div>
<button className="btn sm icon" onClick={() => setItems(items.filter(x => x.id !== it.id))}><Icon name="trash" size={11} /></button>
</div>
))}
</>
)}
</Card>
<div>
<Card title="Order summary">
<dl className="kv" style={{ gridTemplateColumns: "1fr auto", fontSize: 12.5 }}>
<dt>Taxable</dt> <dd className="mono">{inrFull(taxable)}</dd>
<dt>GST</dt> <dd className="mono">{inrFull(gst)}</dd>
</dl>
<div className="divider" />
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<span style={{ fontSize: 12, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.06em" }}>Grand total</span>
<span className="mono" style={{ fontSize: 20, fontWeight: 500 }}>{inrFull(taxable + gst)}</span>
</div>
</Card>
<div style={{ height: 14 }} />
<Card title="Delivery site">
<div className="field">
<select className="select">
{SITES.map(s => <option key={s.id}>{s.name}</option>)}
</select>
</div>
<div className="muted" style={{ fontSize: 11.5, marginTop: 6 }}>
The selected site pre-fills as place of delivery on the new PO.
</div>
</Card>
</div>
</div>
</>
);
};
/* ═══════════════════ IMPORT PO ═══════════════════ */
const ImportPOPage = ({ go }) => (
<>
<div className="page-head">
<div>
<Crumbs items={["Purchase Orders", "Import PO"]} />
<h1 className="page-title">Import PO from Excel</h1>
<div className="page-sub">Upload a file in Pelagia's standard PO template format</div>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1.6fr 1fr", gap: 16, marginBottom: 16 }}>
<Card title="1 · Upload file">
<div style={{ padding: 28, border: "1.5px dashed var(--line-2)", borderRadius: 8, textAlign: "center", color: "var(--muted)" }}>
<Icon name="upload" /> &nbsp; Drop .xlsx file here or <span style={{ color: "var(--primary-ink)" }}>browse</span>
<div className="faint" style={{ fontSize: 11.5, marginTop: 4 }}>Template downloadable from /docs/po-template.xlsx</div>
</div>
<div style={{ marginTop: 12, padding: "8px 10px", background: "var(--paper-2)", borderRadius: 6, fontSize: 12.5, display: "flex", alignItems: "center", gap: 10 }}>
<Icon name="check" />
<span><strong>Q-Mahalakshmi-2841.xlsx</strong> · 84 KB · parsed</span>
<span style={{ flex: 1 }} />
<span className="muted" style={{ fontSize: 11.5 }}>5 line items detected</span>
</div>
</Card>
<Card title="2 · Pelagia metadata">
<div className="form-row" style={{ gap: 12 }}>
<div className="field">
<label className="field-label">Vessel <span className="req">*</span></label>
<select className="select"><option>MV Pelagia Voyager</option>{VESSELS.slice(1).map(v => <option key={v.id}>{v.name}</option>)}</select>
</div>
<div className="field">
<label className="field-label">Account <span className="req">*</span></label>
<select className="select"><option>ENGINE Engine Department</option>{ACCOUNTS.slice(0, 4).map(a => <option key={a.id}>{a.code} {a.name}</option>)}</select>
</div>
</div>
</Card>
</div>
<h2 className="section-title">3 · Extracted line items review before saving</h2>
<Card flush>
<table className="table">
<thead>
<tr>
<th>Item</th><th>Description</th><th className="num">Qty</th><th>Unit</th>
<th className="num">Unit price</th><th className="num">GST</th><th className="num">Total</th>
</tr>
</thead>
<tbody>
{ORDERS[0].items.concat([
{ name: "Marine Grease — EP2", desc: "Lithium complex, 18kg pail", qty: 6, unit: "ea", price: 4800, gst: 18 },
{ name: "O-Ring Set — FKM", desc: "Assorted sizes 40-pc kit", qty: 2, unit: "set", price: 2400, gst: 18 },
]).map((i, idx) => (
<tr key={idx}>
<td>{i.name}</td>
<td className="muted">{i.desc}</td>
<td className="num">{i.qty}</td>
<td className="muted">{i.unit}</td>
<td className="num mono">{inrFull(i.price)}</td>
<td className="num muted">{i.gst}%</td>
<td className="num mono">{inrFull(i.qty * i.price * (1 + i.gst / 100))}</td>
</tr>
))}
</tbody>
</table>
</Card>
<div style={{ marginTop: 18, display: "flex", justifyContent: "flex-end", gap: 8 }}>
<button className="btn" onClick={() => go("my-orders")}>Cancel</button>
<button className="btn maritime">Save as Draft</button>
</div>
</>
);
Object.assign(window, { SitesPage, SiteDetailPage, VesselsPage, AccountsPage, UsersPage, CartPage, ImportPOPage });