- 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>
490 lines
24 KiB
JavaScript
490 lines
24 KiB
JavaScript
/* Pelagia Portal — Payments, History, Vendors, Items, Sites, Cart, Users, Vessels, Accounts */
|
|
|
|
/* ═══════════════════ PAYMENTS ═══════════════════ */
|
|
const PaymentsPage = ({ go }) => {
|
|
const ready = ORDERS.filter(o => o.status === "MGR_APPROVED");
|
|
const sent = ORDERS.filter(o => o.status === "SENT_FOR_PAYMENT");
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<Crumbs items={["Purchase Orders", "Payments"]} />
|
|
<h1 className="page-title">Payment queue</h1>
|
|
<div className="page-sub">{ready.length} ready for payment · {sent.length} awaiting confirmation</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 className="section-title">Ready for payment</h2>
|
|
<div className="pay-grid">
|
|
{ready.map(o => (
|
|
<div className="pay-card" key={o.id}>
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
|
|
<div>
|
|
<div className="po-num">{o.id}</div>
|
|
<div style={{ marginTop: 2, fontSize: 13.5, fontWeight: 500 }}>{o.title}</div>
|
|
</div>
|
|
<Badge status={o.status} />
|
|
</div>
|
|
<dl className="kv" style={{ gridTemplateColumns: "90px 1fr", gap: "4px 8px", fontSize: 12 }}>
|
|
<dt>Vessel</dt> <dd>{o.vessel}</dd>
|
|
<dt>Vendor</dt> <dd>{o.vendor || <em className="muted">—</em>}</dd>
|
|
<dt>Submitter</dt> <dd>{o.submitter}</dd>
|
|
<dt>Approved</dt> <dd className="mono">{o.approved || "—"}</dd>
|
|
</dl>
|
|
<div className="amount">{inrFull(o.amount)}</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<button className="btn" style={{ flex: 1 }} onClick={() => go("po-detail", o.id)}><Icon name="eye" /> View</button>
|
|
<button className="btn maritime" style={{ flex: 1.6, justifyContent: "center" }}><Icon name="arrowRight" /> Send for Payment</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<h2 className="section-title">Processing — awaiting confirmation</h2>
|
|
<div className="pay-grid">
|
|
{sent.map(o => (
|
|
<div className="pay-card" key={o.id}>
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
|
|
<div>
|
|
<div className="po-num">{o.id}</div>
|
|
<div style={{ marginTop: 2, fontSize: 13.5, fontWeight: 500 }}>{o.title}</div>
|
|
</div>
|
|
<Badge status={o.status} />
|
|
</div>
|
|
<dl className="kv" style={{ gridTemplateColumns: "90px 1fr", gap: "4px 8px", fontSize: 12 }}>
|
|
<dt>Vessel</dt> <dd>{o.vessel}</dd>
|
|
<dt>Vendor</dt> <dd>{o.vendor}</dd>
|
|
<dt>Submitter</dt> <dd>{o.submitter}</dd>
|
|
<dt>Sent on</dt> <dd className="mono">May 09, 2026</dd>
|
|
</dl>
|
|
<div className="amount">{inrFull(o.amount)}</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<button className="btn" style={{ flex: 1 }} onClick={() => go("po-detail", o.id)}><Icon name="eye" /> View</button>
|
|
<button className="btn maritime" style={{ flex: 1.6, justifyContent: "center" }}><Icon name="check" /> Mark as Paid</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{sent.length === 0 && <div className="empty-state" style={{ gridColumn: "1 / -1" }}>Nothing currently processing.</div>}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
/* ═══════════════════ HISTORY ═══════════════════ */
|
|
const HistoryPage = ({ go }) => (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<Crumbs items={["Purchase Orders", "History"]} />
|
|
<h1 className="page-title">Order history</h1>
|
|
<div className="page-sub">All POs across all statuses · apply filters then export</div>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<button className="btn"><Icon name="download" /> Export PDF</button>
|
|
<button className="btn"><Icon name="download" /> Export CSV</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="filter-bar">
|
|
<div className="field" style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
<span className="muted" style={{ fontSize: 11.5 }}>From</span>
|
|
<input className="input" type="date" defaultValue="2026-04-01" style={{ width: 130 }} />
|
|
</div>
|
|
<div className="field" style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
<span className="muted" style={{ fontSize: 11.5 }}>To</span>
|
|
<input className="input" type="date" defaultValue="2026-05-14" style={{ width: 130 }} />
|
|
</div>
|
|
<select className="select"><option>All vessels</option>{VESSELS.map(v => <option key={v.id}>{v.name}</option>)}</select>
|
|
<select className="select">
|
|
<option>All statuses</option>
|
|
{Object.keys(STATUS_LABELS).map(s => <option key={s}>{STATUS_LABELS[s][1]}</option>)}
|
|
</select>
|
|
<span style={{ flex: 1 }} />
|
|
<span className="faint" style={{ fontSize: 11.5 }}>{ORDERS.length} matching · export uses current filters</span>
|
|
</div>
|
|
|
|
<Card flush>
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>PO Number</th><th>Title</th><th>Vessel</th><th>Submitter</th>
|
|
<th>Status</th><th>Created</th><th className="num">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{ORDERS.map(o => (
|
|
<tr key={o.id} className="clickable" onClick={() => go("po-detail", o.id)}>
|
|
<td><span className="po-num">{o.id}</span></td>
|
|
<td>{o.title}</td>
|
|
<td className="muted">{o.vessel}</td>
|
|
<td className="muted">{o.submitter}</td>
|
|
<td><Badge status={o.status} /></td>
|
|
<td className="muted">{o.submitted || o.created || "—"}</td>
|
|
<td className="num">{inr(o.amount)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
</>
|
|
);
|
|
|
|
/* ═══════════════════ VENDOR REGISTRY ═══════════════════ */
|
|
const VendorsPage = ({ go }) => {
|
|
const [showAdd, setShowAdd] = useS(false);
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<Crumbs items={["Inventory", "Vendors"]} />
|
|
<h1 className="page-title">Vendor registry</h1>
|
|
<div className="page-sub">{VENDORS.length} vendors · {VENDORS.filter(v => v.verified).length} verified via GSTIN lookup</div>
|
|
</div>
|
|
<button className="btn maritime" onClick={() => setShowAdd(true)}><Icon name="plus" /> Add vendor</button>
|
|
</div>
|
|
|
|
{showAdd && <AddVendorPanel onClose={() => setShowAdd(false)} />}
|
|
|
|
<Card flush>
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Vendor ID</th><th>Name</th><th>Contact</th>
|
|
<th className="num">Items</th><th>Verified</th><th>Status</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{VENDORS.map(v => (
|
|
<tr key={v.id} className="clickable" onClick={() => go("vendor-detail", v.id)}>
|
|
<td className="mono" style={{ fontSize: 12 }}>{v.vid || <span className="badge vendor no-dot">Pending</span>}</td>
|
|
<td>
|
|
<div style={{ fontWeight: 500 }}>{v.name}</div>
|
|
<div className="muted" style={{ fontSize: 11.5 }}>{v.city} · GSTIN <span className="mono">{v.gstin}</span></div>
|
|
</td>
|
|
<td>
|
|
<div>{v.contact}</div>
|
|
<div className="muted" style={{ fontSize: 11.5 }}>{v.email}</div>
|
|
</td>
|
|
<td className="num mono">{[42, 18, 64, 7, 22, 31][VENDORS.indexOf(v)]}</td>
|
|
<td>{v.verified
|
|
? <span className="verified-mark"><Icon name="check" size={11} /> Verified</span>
|
|
: <span className="muted" style={{ fontSize: 11.5 }}>—</span>}
|
|
</td>
|
|
<td><Badge className={v.active ? "closed" : "draft"} noDot>{v.active ? "Active" : "Inactive"}</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>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const AddVendorPanel = ({ onClose }) => {
|
|
const [step, setStep] = useS(1); // 1 type GSTIN, 2 captcha, 3 verified
|
|
|
|
return (
|
|
<Card title="Add vendor" action={<button className="btn sm" onClick={onClose}>Close</button>} className="action-panel" style={{ marginBottom: 18 }}>
|
|
<div className="form-row" style={{ gap: 14 }}>
|
|
<div className="alert info" style={{ margin: 0 }}>
|
|
<Icon name="search" />
|
|
<div>Look up the vendor's GSTIN to auto-fill name, address, and pincode. Manual entry is allowed if needed.</div>
|
|
</div>
|
|
|
|
<div className="form-row cols-3" style={{ alignItems: "end" }}>
|
|
<div className="field">
|
|
<label className="field-label">GSTIN <span className="req">*</span></label>
|
|
<input className="input mono" placeholder="15-character GSTIN" defaultValue="29ABCDE1234F1Z5" />
|
|
</div>
|
|
{step === 1 && (
|
|
<button className="btn maritime" onClick={() => setStep(2)} style={{ height: 32 }}>
|
|
<Icon name="search" /> Look up
|
|
</button>
|
|
)}
|
|
{step >= 2 && (
|
|
<div className="field">
|
|
<label className="field-label">Captcha · type code shown</label>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<div className="mono" style={{
|
|
flex: "0 0 110px", height: 32,
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
background: "repeating-linear-gradient(45deg, var(--paper-2) 0 6px, var(--surface) 6px 12px)",
|
|
border: "1px solid var(--line)", borderRadius: 6,
|
|
fontWeight: 600, letterSpacing: "0.15em", fontSize: 15,
|
|
textDecoration: "line-through wavy var(--faint) 1px"
|
|
}}>K4G7AP</div>
|
|
<input className="input mono" placeholder="Enter code" style={{ flex: 1 }} />
|
|
<button className="btn icon" title="Refresh captcha"><Icon name="refresh" size={12} /></button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{step >= 2 && (
|
|
<button className="btn maritime" onClick={() => setStep(3)} style={{ height: 32 }}>
|
|
<Icon name="check" /> Verify
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{step === 3 && (
|
|
<>
|
|
<div className="alert" style={{ background: "oklch(96% 0.04 150)", borderColor: "var(--st-closed-bg)", color: "var(--st-closed-fg)" }}>
|
|
<Icon name="check" />
|
|
<div><strong>Verified.</strong> Legal name, address, and pincode have been pulled from the GST portal.</div>
|
|
</div>
|
|
<div className="form-row cols-2">
|
|
<div className="field"><label className="field-label">Legal name</label><input className="input" defaultValue="Mahalakshmi Marine Stores Pvt Ltd" /></div>
|
|
<div className="field"><label className="field-label">Trade name</label><input className="input" defaultValue="Mahalakshmi Marine Stores" /></div>
|
|
<div className="field" style={{ gridColumn: "1 / -1" }}><label className="field-label">Registered address</label><textarea className="textarea" defaultValue="48/2A Bristow Road, Willingdon Island, Kochi, Kerala — 682003"></textarea></div>
|
|
<div className="field"><label className="field-label">Pincode</label><input className="input mono" defaultValue="682003" /></div>
|
|
<div className="field"><label className="field-label">Geocoded location</label><input className="input mono" defaultValue="9.9489° N · 76.2622° E" readOnly /></div>
|
|
<div className="field"><label className="field-label">Contact name</label><input className="input" defaultValue="R. Subramanian" /></div>
|
|
<div className="field"><label className="field-label">Email</label><input className="input" defaultValue="sales@mahalakshmimarine.in" /></div>
|
|
<div className="field"><label className="field-label">Mobile</label><input className="input mono" defaultValue="+91 98450 12011" /></div>
|
|
</div>
|
|
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
|
|
<button className="btn" onClick={onClose}>Cancel</button>
|
|
<button className="btn maritime" onClick={onClose}>Save vendor</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
/* ═══════════════════ VENDOR DETAIL ═══════════════════ */
|
|
const VendorDetailPage = ({ go, id }) => {
|
|
const v = VENDORS.find(x => x.id === id) || VENDORS[0];
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<Crumbs items={["Inventory", "Vendors", v.name]} />
|
|
<h1 className="page-title">{v.name}</h1>
|
|
<div className="page-sub">
|
|
{v.vid ? <><span className="mono">{v.vid}</span><span className="dot-sep">·</span></> : null}
|
|
{v.verified && <><span className="verified-mark"><Icon name="check" size={11} /> GSTIN verified</span><span className="dot-sep">·</span></>}
|
|
<Badge className={v.active ? "closed" : "draft"} noDot>{v.active ? "Active" : "Inactive"}</Badge>
|
|
</div>
|
|
</div>
|
|
<button className="btn"><Icon name="edit" /> Edit vendor</button>
|
|
</div>
|
|
|
|
<div className="detail-layout">
|
|
<div>
|
|
<h2 className="section-title">Items supplied</h2>
|
|
<Card flush>
|
|
<table className="table">
|
|
<thead><tr><th>Code</th><th>Product</th><th className="num">Last quoted</th><th>Last updated</th><th></th></tr></thead>
|
|
<tbody>
|
|
{PRODUCTS.slice(0, 5).map(p => (
|
|
<tr key={p.id} className="clickable" onClick={() => go("item-detail", p.id)}>
|
|
<td className="mono" style={{ fontSize: 12 }}>{p.code}</td>
|
|
<td>{p.name}</td>
|
|
<td className="num mono">{inrFull(p.lastPrice)}</td>
|
|
<td className="muted">{p.updated}</td>
|
|
<td style={{ textAlign: "right" }}><Icon name="arrowRight" size={11} /></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
|
|
<h2 className="section-title">Recent purchase orders</h2>
|
|
<Card flush>
|
|
<table className="table">
|
|
<thead><tr><th>PO Number</th><th>Status</th><th>Vessel</th><th>Created</th><th className="num">Amount</th></tr></thead>
|
|
<tbody>
|
|
{ORDERS.slice(0, 6).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.vessel}</td>
|
|
<td className="muted">{o.submitted || "—"}</td>
|
|
<td className="num">{inr(o.amount)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
</div>
|
|
|
|
<div>
|
|
<Card title="Vendor info">
|
|
<dl className="kv" style={{ gridTemplateColumns: "90px 1fr", gap: "8px 8px", fontSize: 12 }}>
|
|
<dt>GSTIN</dt> <dd className="mono">{v.gstin}</dd>
|
|
<dt>Pincode</dt> <dd className="mono">{v.pin}</dd>
|
|
<dt>City</dt> <dd>{v.city}</dd>
|
|
<dt>Contact</dt> <dd>{v.contact}</dd>
|
|
<dt>Mobile</dt> <dd className="mono">{v.phone}</dd>
|
|
<dt>Email</dt> <dd style={{ wordBreak: "break-all" }}>{v.email}</dd>
|
|
</dl>
|
|
</Card>
|
|
<div style={{ height: 14 }} />
|
|
<Card title="Address">
|
|
<div style={{ fontSize: 12.5 }}>
|
|
48/2A Bristow Road, Willingdon Island,<br />
|
|
Kochi, Kerala — {v.pin}
|
|
</div>
|
|
<div style={{ marginTop: 10, height: 110, background: "repeating-linear-gradient(135deg, var(--paper-2) 0 10px, var(--surface) 10px 20px)", border: "1px solid var(--line)", borderRadius: 6, position: "relative" }}>
|
|
<div style={{ position: "absolute", left: "40%", top: "45%", width: 12, height: 12, borderRadius: "50%", background: "var(--primary)", border: "2px solid var(--surface)" }} />
|
|
<div style={{ position: "absolute", bottom: 6, left: 8, fontSize: 10.5, color: "var(--muted)", fontFamily: "var(--font-mono)" }}>9.9489° N · 76.2622° E</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
/* ═══════════════════ ITEMS CATALOGUE ═══════════════════ */
|
|
const ItemsPage = ({ go }) => (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<Crumbs items={["Inventory", "Items"]} />
|
|
<h1 className="page-title">Item catalogue</h1>
|
|
<div className="page-sub">{PRODUCTS.length} products · auto-synced when POs are marked as paid</div>
|
|
</div>
|
|
<button className="btn maritime"><Icon name="plus" /> Add product</button>
|
|
</div>
|
|
|
|
<div className="alert info">
|
|
<Icon name="bell" />
|
|
<div>Items are added automatically when a PO is marked as paid. Manual entry is reserved for ADMIN.</div>
|
|
</div>
|
|
|
|
<Card flush>
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Code</th><th>Name</th><th>Description</th>
|
|
<th className="num">Vendors</th><th className="num">Last price</th><th>Last vendor</th><th>Updated</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{PRODUCTS.map(p => (
|
|
<tr key={p.id} className="clickable" onClick={() => go("item-detail", p.id)}>
|
|
<td className="mono" style={{ fontSize: 12 }}>{p.code}</td>
|
|
<td style={{ fontWeight: 500 }}>{p.name}</td>
|
|
<td className="muted">{p.desc}</td>
|
|
<td className="num mono">{p.vendors}</td>
|
|
<td className="num mono">{inr(p.lastPrice)}</td>
|
|
<td className="muted">{p.lastVendor}</td>
|
|
<td className="muted">{p.updated}</td>
|
|
<td><Badge className={p.active ? "closed" : "draft"} noDot>{p.active ? "Active" : "Inactive"}</Badge></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
</>
|
|
);
|
|
|
|
/* ═══════════════════ ITEM DETAIL ═══════════════════ */
|
|
const ItemDetailPage = ({ go, id }) => {
|
|
const p = PRODUCTS.find(x => x.id === id) || PRODUCTS[0];
|
|
const [siteFilter, setSiteFilter] = useS("");
|
|
const vendors = [...ITEM_VENDORS].sort((a, b) => siteFilter ? a.distance - b.distance : 0);
|
|
const max = Math.max(...vendors.map(v => v.price));
|
|
const min = Math.min(...vendors.map(v => v.price));
|
|
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<Crumbs items={["Inventory", "Items", p.name]} />
|
|
<h1 className="page-title">{p.name}</h1>
|
|
<div className="page-sub">
|
|
<span className="mono">{p.code}</span><span className="dot-sep">·</span>
|
|
<Badge className="closed" noDot>Active</Badge><span className="dot-sep">·</span>
|
|
<span>{p.desc}</span>
|
|
</div>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<button className="btn"><Icon name="cart" /> Add to Cart</button>
|
|
<button className="btn"><Icon name="settings" /> Toggle Active</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-grid" style={{ marginBottom: 22 }}>
|
|
<Stat label="Vendors supplying" value={vendors.length} />
|
|
<Stat label="Lowest price" value={inr(min)} sub={<span className="muted">Anchor Supply Traders</span>} />
|
|
<Stat label="Highest price" value={inr(max)} sub={<span className="muted">Konark Industrial Spares</span>} />
|
|
<Stat label="Sites with stock" value="3" sub={<span className="muted">of 4 sites</span>} />
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginBottom: 16 }}>
|
|
<Card title="Price comparison" action={<span className="muted" style={{ fontSize: 11.5 }}>Unit price · INR</span>}>
|
|
<div className="bar-chart" style={{ height: 130, marginBottom: 24 }}>
|
|
{vendors.map((v, i) => (
|
|
<div className="bar" key={i}
|
|
data-label={v.vendor.split(" ")[0]}
|
|
style={{ height: (v.price / max * 120) + "px", background: v.price === min ? "var(--primary)" : "var(--primary-soft)" }}>
|
|
<span className="bar-val">{inr(v.price)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="Stock by site">
|
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
|
<span className="tag-chip has-link">Cochin Port Depot <span className="mono" style={{ color: "var(--muted)" }}>· 18 ea</span></span>
|
|
<span className="tag-chip has-link">Chennai South Dock <span className="mono" style={{ color: "var(--muted)" }}>· 6 ea</span></span>
|
|
<span className="tag-chip has-link">Visakhapatnam Yard <span className="mono" style={{ color: "var(--muted)" }}>· 14 ea</span></span>
|
|
<span className="tag-chip" style={{ color: "var(--muted)" }}>Mumbai BPX Office <span className="mono">· 0 ea</span></span>
|
|
</div>
|
|
<div className="divider" />
|
|
<div className="muted" style={{ fontSize: 12 }}>Inventory is updated when consumption is logged at the site. Click a chip to open site detail.</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }}>
|
|
<h2 className="section-title" style={{ margin: 0 }}>Vendor pricing</h2>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<span className="muted" style={{ fontSize: 11.5 }}>Sort by distance from</span>
|
|
<select className="select" style={{ height: 28, fontSize: 12 }} value={siteFilter} onChange={e => setSiteFilter(e.target.value)}>
|
|
<option value="">— Any site —</option>
|
|
{SITES.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<Card flush>
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Vendor</th><th>Verified</th>
|
|
<th className="num">Unit price</th>
|
|
{siteFilter && <th className="num">Distance</th>}
|
|
<th>Last updated</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{vendors.map((v, i) => (
|
|
<tr key={i}>
|
|
<td>
|
|
{v.closest && siteFilter && <span style={{ color: "oklch(60% 0.15 70)", marginRight: 4 }}>★</span>}
|
|
<span className="has-link" style={{ color: "var(--primary-ink)", cursor: "pointer" }}>{v.vendor}</span>
|
|
</td>
|
|
<td>{v.verified ? <span className="verified-mark"><Icon name="check" size={11} /> Verified</span> : <span className="muted">—</span>}</td>
|
|
<td className="num mono">{inrFull(v.price)}</td>
|
|
{siteFilter && <td className="num mono">{v.distance} km</td>}
|
|
<td className="muted">{v.updated}</td>
|
|
<td style={{ textAlign: "right" }}>
|
|
<button className="btn sm"><Icon name="cart" size={11} /> Add to Cart</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Card>
|
|
</>
|
|
);
|
|
};
|
|
|
|
Object.assign(window, { PaymentsPage, HistoryPage, VendorsPage, VendorDetailPage, ItemsPage, ItemDetailPage });
|