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

502 lines
25 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 — Approvals, PO Detail, New PO */
/* ═══════════════════ APPROVAL QUEUE ═══════════════════ */
const ApprovalsPage = ({ go }) => (
<>
<div className="page-head">
<div>
<Crumbs items={["Purchase Orders", "Approvals"]} />
<h1 className="page-title">Approval queue
<span style={{ color: "var(--muted)", fontWeight: 400, marginLeft: 10, fontSize: 16 }}>· {APPROVAL_QUEUE.length} pending</span>
</h1>
<div className="page-sub">POs awaiting manager decision · oldest first</div>
</div>
</div>
<div className="filter-bar">
<div style={{ display: "flex", alignItems: "center", gap: 6, padding: "0 10px", height: 28, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: 6, flex: "0 0 240px" }}>
<Icon name="search" size={12} />
<input className="mono" style={{ border: 0, outline: 0, flex: 1, background: "transparent", fontSize: 11.5 }} placeholder="Search PO #, submitter, title" />
</div>
<select className="select"><option>All vessels</option>{VESSELS.map(v => <option key={v.id}>{v.name}</option>)}</select>
<select className="select"><option>Any submitter</option>{["Rajesh Pillai","Fatima Sheikh","Dev Shah"].map(s => <option key={s}>{s}</option>)}</select>
<input className="input" type="date" defaultValue="2026-05-01" style={{ width: 140 }} />
<span className="faint" style={{ fontSize: 11.5, marginLeft: "auto" }}>Sorted by submitted date</span>
</div>
<Card flush>
<table className="table">
<thead>
<tr>
<th>PO Number</th><th>Title</th><th>Submitter</th><th>Vessel</th><th>Submitted</th><th className="num">Amount</th><th></th>
</tr>
</thead>
<tbody>
{APPROVAL_QUEUE.map(o => (
<tr key={o.id} className="clickable" onClick={() => go("approval-detail", o.id)}>
<td><span className="po-num">{o.id}</span></td>
<td>{o.title}</td>
<td>{o.submitter}</td>
<td className="muted">{o.vessel}</td>
<td className="muted">{o.submitted}</td>
<td className="num">{inr(o.amount)}</td>
<td style={{ textAlign: "right" }}>
<span style={{ color: "var(--primary-ink)", fontSize: 12 }}>Review </span>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</>
);
/* ═══════════════════ PO DETAIL (shared) ═══════════════════ */
const ItemsTable = ({ items }) => {
if (!items?.length) return <div className="empty-state">No line items.</div>;
const taxable = items.reduce((s, i) => s + i.qty * i.price, 0);
const gst = items.reduce((s, i) => s + i.qty * i.price * i.gst / 100, 0);
return (
<>
<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>
{items.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">{inrFull(i.price)}</td>
<td className="num muted">{i.gst}%</td>
<td className="num">{inrFull(i.qty * i.price * (1 + i.gst/100))}</td>
</tr>
))}
</tbody>
</table>
<div style={{ display: "flex", justifyContent: "flex-end", padding: "12px 16px", gap: 32, borderTop: "1px solid var(--line)", background: "var(--paper-2)" }}>
<div>
<div className="muted" style={{ fontSize: 11 }}>TAXABLE</div>
<div className="mono" style={{ fontSize: 14 }}>{inrFull(taxable)}</div>
</div>
<div>
<div className="muted" style={{ fontSize: 11 }}>GST</div>
<div className="mono" style={{ fontSize: 14 }}>{inrFull(gst)}</div>
</div>
<div>
<div className="muted" style={{ fontSize: 11 }}>GRAND TOTAL</div>
<div className="mono" style={{ fontSize: 16, fontWeight: 500 }}>{inrFull(taxable + gst)}</div>
</div>
</div>
</>
);
};
const PODetailBase = ({ order, go, role, isApproval }) => {
const isOwner = order.submitter === "Rajesh Pillai" && role === "TECHNICAL";
const isManager = role === "MANAGER" || role === "SUPERUSER";
const showEdit = ["DRAFT", "EDITS_REQUESTED"].includes(order.status) && (isOwner || role === "SUPERUSER");
const showReceipt = order.status === "PAID_DELIVERED" && (isOwner || role === "SUPERUSER");
const showVendorPicker = order.status === "VENDOR_ID_PENDING";
return (
<>
<div className="page-head">
<div>
<Crumbs items={[isApproval ? "Approvals" : "Purchase Orders", order.id]} />
<h1 className="page-title">{order.title}</h1>
<div className="page-sub">
Created by {order.submitter} · {order.created || order.submitted}
</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button className="btn"><Icon name="download" /> Export PDF</button>
{showEdit && <button className="btn"><Icon name="edit" /> Edit</button>}
{!isApproval && order.status === "DRAFT" && <button className="btn danger"><Icon name="trash" /> Discard</button>}
</div>
</div>
<div className="detail-band">
<div className="detail-band-left">
<div className="po-id">{order.id}</div>
<Badge status={order.status} />
{order.project && <span className="role-badge">{order.project}</span>}
</div>
<div style={{ textAlign: "right" }}>
<div className="muted" style={{ fontSize: 11 }}>GRAND TOTAL</div>
<div className="mono" style={{ fontSize: 22, letterSpacing: "-0.01em" }}>{inr(order.amount)}</div>
</div>
</div>
{order.managerNote && (
<div className="alert">
<Icon name="bell" />
<div>
<strong>Manager note · Anjali Krishnan:</strong> {order.managerNote}
</div>
</div>
)}
<div className="detail-layout">
{/* MAIN */}
<div>
{isApproval && (
<Card title="Manager actions" className="action-panel" style={{ marginBottom: 16 }}>
<ApprovalActions />
</Card>
)}
{showVendorPicker && (
<Card title="Vendor selection required" style={{ marginBottom: 16 }}>
<div className="muted" style={{ marginBottom: 10, fontSize: 12.5 }}>
The manager has requested that a vendor be selected before approval.
</div>
<div className="form-row cols-2" style={{ alignItems: "end" }}>
<div className="field">
<label className="field-label">Vendor</label>
<select className="select">
<option>Select vendor</option>
{VENDORS.filter(v => v.active).map(v => <option key={v.id}>{v.name}</option>)}
</select>
</div>
<button className="btn maritime">Submit & return to manager</button>
</div>
</Card>
)}
<h2 className="section-title">Summary</h2>
<Card>
<dl className="kv">
<dt>Vessel</dt> <dd>{order.vessel}</dd>
<dt>Account</dt> <dd className="mono">{order.account}</dd>
<dt>Vendor</dt> <dd>{order.vendor || <em className="muted">Not yet assigned</em>}</dd>
<dt>Project code</dt> <dd>{order.project ? <span className="mono">{order.project}</span> : <em className="muted"></em>}</dd>
<dt>Date required</dt> <dd>{order.required || ""}</dd>
<dt>Currency</dt> <dd>INR ()</dd>
</dl>
</Card>
<h2 className="section-title">Line items</h2>
<Card flush>
<ItemsTable items={order.items?.length ? order.items : ORDERS[0].items} />
</Card>
<h2 className="section-title">Terms & conditions</h2>
<Card>
<dl className="kv">
<dt>Delivery</dt> <dd>{order.terms?.delivery || ORDERS[0].terms.delivery}</dd>
<dt>Dispatch</dt> <dd>{order.terms?.dispatch || ORDERS[0].terms.dispatch}</dd>
<dt>Inspection</dt> <dd>{order.terms?.inspection || ORDERS[0].terms.inspection}</dd>
<dt>Transit insurance</dt> <dd>{order.terms?.insurance || ORDERS[0].terms.insurance}</dd>
<dt>Payment terms</dt> <dd>{order.terms?.payment || ORDERS[0].terms.payment}</dd>
<dt>Other</dt> <dd>{order.terms?.others || ORDERS[0].terms.others}</dd>
</dl>
</Card>
<h2 className="section-title">Documents</h2>
<Card>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{(order.docs || ORDERS[0].docs).map((d, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", background: "var(--paper-2)", borderRadius: 6 }}>
<Icon name="paperclip" />
<span style={{ fontSize: 12.5 }}>{d.name}</span>
<span className="muted" style={{ fontSize: 11.5 }}>{d.size}</span>
<span style={{ flex: 1 }} />
<button className="btn sm"><Icon name="download" size={11} /> Download</button>
</div>
))}
</div>
</Card>
{showReceipt && (
<>
<h2 className="section-title">Confirm receipt</h2>
<Card>
<div className="muted" style={{ marginBottom: 12, fontSize: 12.5 }}>
Goods have been marked as paid and dispatched. Upload the delivery receipt to close this PO.
</div>
<div className="form-row" style={{ gap: 10 }}>
<div style={{ padding: 22, border: "1.5px dashed var(--line-2)", borderRadius: 8, textAlign: "center", color: "var(--muted)" }}>
<Icon name="upload" /> &nbsp; Drop delivery receipt file or <span style={{ color: "var(--primary-ink)" }}>browse</span>
</div>
<textarea className="textarea" placeholder="Notes (optional) — note any damage or discrepancy"></textarea>
<button className="btn maritime" style={{ alignSelf: "flex-end" }}>Confirm receipt</button>
</div>
</Card>
</>
)}
</div>
{/* SIDEBAR */}
<div>
<Card title="Timestamps">
<dl className="kv" style={{ gridTemplateColumns: "100px 1fr", gap: "6px 8px", fontSize: 12 }}>
<dt>Created</dt> <dd className="mono">{order.created || "May 10, 2026"}</dd>
<dt>Submitted</dt> <dd className="mono">{order.submitted || ""}</dd>
<dt>Approved</dt> <dd className="mono">{order.approved || ""}</dd>
<dt>Paid</dt> <dd className="mono">{order.paid || ""}</dd>
<dt>Closed</dt> <dd className="mono">{order.closed || ""}</dd>
</dl>
</Card>
<div style={{ height: 14 }} />
<Card title="Audit trail">
<div>
{(order.audit || ORDERS[0].audit).map((a, i) => (
<div className="timeline-stop done" key={i}>
<div className="dot" />
<div>
<div className="actor">{a.who}</div>
<div className="action">{a.what}</div>
</div>
<div className="when">{a.when.split(" · ")[1] || a.when}</div>
</div>
))}
</div>
</Card>
</div>
</div>
</>
);
};
const ApprovalActions = () => {
const [mode, setMode] = useS(null); // null | edits | reject | note | vendor
if (mode === null) {
return (
<div className="action-row">
<button className="btn maritime"><Icon name="check" /> Approve</button>
<button className="btn" onClick={() => setMode("note")}>Approve with Note</button>
<button className="btn" onClick={() => setMode("edits")}>Request Edits</button>
<button className="btn" onClick={() => setMode("vendor")}>Request Vendor ID</button>
<span style={{ flex: 1 }} />
<button className="btn danger" onClick={() => setMode("reject")}>Reject</button>
</div>
);
}
const titles = {
edits: ["Request edits", "Describe what needs to change before approval.", "var(--st-edits-fg)", "Send back to submitter"],
reject: ["Reject PO", "Provide a reason — this terminates the PO.", "var(--st-rejected-fg)", "Reject"],
note: ["Approve with note", "Optional note visible to the submitter.", "var(--primary-ink)", "Approve"],
vendor: ["Request vendor selection", "Note for submitter (optional). PO returns here once a vendor is chosen.", "var(--st-vendor-fg)", "Send back for vendor"],
};
const [title, sub, color, cta] = titles[mode];
return (
<div className="form-row" style={{ gap: 10 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div>
<div style={{ fontWeight: 600, fontSize: 13 }}>{title}</div>
<div className="muted" style={{ fontSize: 12 }}>{sub}</div>
</div>
<button className="btn sm" onClick={() => setMode(null)}>Cancel</button>
</div>
<textarea className="textarea" placeholder={mode === "vendor" ? "e.g. Please use the verified vendor in Kochi for faster delivery." : "Required reason / note…"}></textarea>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<button className="btn" style={{ background: color, color: "var(--paper)", borderColor: color }}>{cta}</button>
</div>
</div>
);
};
/* ═══════════════════ NEW PO ═══════════════════ */
const NewPOPage = ({ go }) => {
const [rows, setRows] = useS([
{ name: "Marine Bearing 6310-2RS", desc: "50×110×27mm, sealed", qty: 24, unit: "ea", size: "", price: 4250, gst: 18, suggest: false },
{ name: "Cylinder Head Gasket Set", desc: "MAN B&W L28/32H", qty: 4, unit: "set", size: "", price: 18750, gst: 18, suggest: false },
]);
const [suggestIdx, setSuggestIdx] = useS(-1);
const taxable = rows.reduce((s, r) => s + (Number(r.qty) || 0) * (Number(r.price) || 0), 0);
const gst = rows.reduce((s, r) => s + (Number(r.qty) || 0) * (Number(r.price) || 0) * (Number(r.gst) || 0) / 100, 0);
const addRow = () => setRows([...rows, { name: "", desc: "", qty: 1, unit: "ea", size: "", price: 0, gst: 18 }]);
const updateRow = (idx, key, val) => setRows(rows.map((r, i) => i === idx ? { ...r, [key]: val } : r));
const removeRow = (idx) => setRows(rows.filter((_, i) => i !== idx));
return (
<>
<div className="page-head">
<div>
<Crumbs items={["Purchase Orders", "New PO"]} />
<h1 className="page-title">New purchase order</h1>
<div className="page-sub">Fill the four sections below. You can save as draft at any time.</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button className="btn" onClick={() => go("my-orders")}>Cancel</button>
<button className="btn">Save as Draft</button>
<button className="btn maritime"><Icon name="check" /> Submit for Approval</button>
</div>
</div>
<h2 className="section-title">1 · Header</h2>
<Card>
<div className="form-row" style={{ gap: 14 }}>
<div className="field">
<label className="field-label">Title <span className="req">*</span></label>
<input className="input" defaultValue="Engine room bearings — Q2 replenishment" />
</div>
<div className="form-row cols-3">
<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 className="field">
<label className="field-label">Vendor (optional)</label>
<select className="select"><option>Mahalakshmi Marine Stores</option>{VENDORS.slice(1).map(v => <option key={v.id}>{v.name}</option>)}<option>+ Add new vendor</option></select>
</div>
</div>
<div className="form-row cols-3">
<div className="field">
<label className="field-label">Date required</label>
<input className="input" type="date" defaultValue="2026-05-28" />
</div>
<div className="field">
<label className="field-label">Project code</label>
<input className="input mono" defaultValue="ER-MAINT-2026" />
</div>
<div className="field">
<label className="field-label">Currency</label>
<select className="select"><option>INR ()</option><option>USD ($)</option><option>EUR ()</option></select>
</div>
</div>
<div className="field">
<label className="field-label">Description / remarks</label>
<textarea className="textarea" defaultValue="Second-quarter replenishment of bearings and gasket sets for the engine room. Includes spares for the L28/32H mains and auxiliaries."></textarea>
</div>
</div>
</Card>
<h2 className="section-title">2 · Line items</h2>
<Card flush>
<table className="table">
<thead>
<tr>
<th style={{ width: "26%" }}>Name</th>
<th>Description</th>
<th className="num" style={{ width: 70 }}>Qty</th>
<th style={{ width: 60 }}>Unit</th>
<th className="num" style={{ width: 110 }}>Unit price</th>
<th className="num" style={{ width: 70 }}>GST %</th>
<th className="num" style={{ width: 110 }}>Total</th>
<th style={{ width: 30 }}></th>
</tr>
</thead>
<tbody>
{rows.map((r, idx) => (
<React.Fragment key={idx}>
<tr>
<td>
<input className="input" style={{ height: 28, fontSize: 12.5 }} value={r.name}
onChange={e => updateRow(idx, "name", e.target.value)}
onFocus={() => setSuggestIdx(idx)}
onBlur={() => setTimeout(() => setSuggestIdx(-1), 200)} />
</td>
<td>
<input className="input" style={{ height: 28, fontSize: 12.5 }} value={r.desc}
onChange={e => updateRow(idx, "desc", e.target.value)} />
</td>
<td><input className="input num mono" style={{ height: 28, fontSize: 12.5, textAlign: "right" }} value={r.qty} onChange={e => updateRow(idx, "qty", e.target.value)} /></td>
<td><input className="input" style={{ height: 28, fontSize: 12.5 }} value={r.unit} onChange={e => updateRow(idx, "unit", e.target.value)} /></td>
<td><input className="input mono" style={{ height: 28, fontSize: 12.5, textAlign: "right" }} value={r.price} onChange={e => updateRow(idx, "price", e.target.value)} /></td>
<td><input className="input mono" style={{ height: 28, fontSize: 12.5, textAlign: "right" }} value={r.gst} onChange={e => updateRow(idx, "gst", e.target.value)} /></td>
<td className="num mono">{inr((Number(r.qty)||0) * (Number(r.price)||0) * (1 + (Number(r.gst)||0)/100))}</td>
<td><button className="btn icon sm" onClick={() => removeRow(idx)} title="Remove"><Icon name="trash" size={11} /></button></td>
</tr>
{suggestIdx === idx && r.name && (
<tr>
<td colSpan="8" style={{ padding: "0 16px 8px", background: "var(--paper-2)" }}>
<div style={{ fontSize: 11, color: "var(--muted)", padding: "6px 0" }}>
<strong style={{ color: "var(--ink)" }}>Last seen at:</strong>
<span className="dot-sep">·</span> Mahalakshmi Marine Stores <span className="mono">4,250</span>
<span className="dot-sep">·</span> Coastline Engineering <span className="mono">4,380</span>
<span className="dot-sep">·</span> Konark Industrial Spares <span className="mono">4,520</span>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
<div style={{ padding: 12, borderTop: "1px solid var(--line)", display: "flex", gap: 12, alignItems: "center" }}>
<button className="btn sm" onClick={addRow}><Icon name="plus" size={11} /> Add line item</button>
<span className="faint" style={{ fontSize: 11.5 }}>Tip: start typing a product name to see vendor price hints</span>
</div>
<div style={{ display: "flex", justifyContent: "flex-end", padding: "12px 16px", gap: 32, borderTop: "1px solid var(--line)", background: "var(--paper-2)" }}>
<div>
<div className="muted" style={{ fontSize: 11 }}>TAXABLE</div>
<div className="mono" style={{ fontSize: 14 }}>{inrFull(taxable)}</div>
</div>
<div>
<div className="muted" style={{ fontSize: 11 }}>GST</div>
<div className="mono" style={{ fontSize: 14 }}>{inrFull(gst)}</div>
</div>
<div>
<div className="muted" style={{ fontSize: 11 }}>GRAND TOTAL</div>
<div className="mono" style={{ fontSize: 17, fontWeight: 500 }}>{inrFull(taxable + gst)}</div>
</div>
</div>
</Card>
<h2 className="section-title">3 · Terms & conditions</h2>
<Card>
<div className="form-row cols-2" style={{ gap: 14 }}>
{[
["Delivery", "FOB Cochin Port. Vendor responsible until cargo cleared at gate."],
["Dispatch", "Within 14 days of approval."],
["Inspection", "Joint inspection at vendor warehouse prior to dispatch."],
["Transit insurance", "Transit insurance included in vendor scope."],
["Payment terms", "30 days from receipt of goods and invoice."],
["Other", "All items to be packed with desiccant and shrink-wrapped."],
].map(([k, v]) => (
<div className="field" key={k}>
<label className="field-label">{k}</label>
<textarea className="textarea" style={{ minHeight: 56 }} defaultValue={v}></textarea>
</div>
))}
</div>
</Card>
<h2 className="section-title">4 · Documents</h2>
<Card>
<div style={{ padding: 22, border: "1.5px dashed var(--line-2)", borderRadius: 8, textAlign: "center", color: "var(--muted)" }}>
<Icon name="upload" /> &nbsp; Drop files here or <span style={{ color: "var(--primary-ink)" }}>browse</span>
<div className="faint" style={{ fontSize: 11.5, marginTop: 4 }}>Quotations, technical sheets, requisitions. Max 25 MB per file.</div>
</div>
<div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 6 }}>
{[
["Vendor Quotation Q-2841.pdf", "412 KB"],
["Engine Inspection Report.pdf", "1.8 MB"],
].map(([n, s]) => (
<div key={n} style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 10px", background: "var(--paper-2)", borderRadius: 6, fontSize: 12.5 }}>
<Icon name="paperclip" />
<span>{n}</span>
<span className="muted" style={{ fontSize: 11.5 }}>{s}</span>
<span style={{ flex: 1 }} />
<button className="btn sm icon"><Icon name="trash" size={11} /></button>
</div>
))}
</div>
</Card>
<div style={{ marginTop: 22, display: "flex", justifyContent: "flex-end", gap: 8 }}>
<button className="btn">Save as Draft</button>
<button className="btn maritime"><Icon name="check" /> Submit for Approval</button>
</div>
</>
);
};
Object.assign(window, { ApprovalsPage, PODetailBase, NewPOPage });