- 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>
104 lines
5.3 KiB
JavaScript
104 lines
5.3 KiB
JavaScript
/* Shared components for Pelagia Portal */
|
||
|
||
const { useState, useEffect, useRef, useMemo } = React;
|
||
|
||
/* ─────────── Icons (inline SVG, 14×14) ─────────── */
|
||
const Icon = ({ name, size = 14 }) => {
|
||
const paths = {
|
||
home: <><path d="M2 7l6-5 6 5v7a1 1 0 0 1-1 1h-3v-5H6v5H3a1 1 0 0 1-1-1V7z"/></>,
|
||
file: <><path d="M3 1.5h6l3.5 3.5v9a1 1 0 0 1-1 1h-8.5a1 1 0 0 1-1-1v-12a1 1 0 0 1 1-1z M9 1.5V5h3.5"/></>,
|
||
list: <><path d="M2 4h12 M2 8h12 M2 12h12"/></>,
|
||
check: <><path d="M3.5 8L7 11.5L13 4.5"/></>,
|
||
cart: <><path d="M2 2h2l2 9h8l1.5-6h-9 M6.5 13.5a1 1 0 1 0 0-.001 M12.5 13.5a1 1 0 1 0 0-.001"/></>,
|
||
box: <><path d="M2 4.5l6-2.5 6 2.5v7l-6 2.5-6-2.5v-7z M2 4.5l6 2.5 6-2.5 M8 7v7.5"/></>,
|
||
truck: <><path d="M1 3.5h8.5v7.5h-8.5z M9.5 6h3l2 2.5v2.5h-5z M4 12.5a1.25 1.25 0 1 0 0-.01 M11.5 12.5a1.25 1.25 0 1 0 0-.01"/></>,
|
||
users: <><path d="M5.5 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4z M1.5 14c0-2 1.5-3.5 4-3.5s4 1.5 4 3.5 M10.5 7.5a1.75 1.75 0 1 0 0-3.5 M14.5 13c0-1.6-1.2-2.8-3-2.8"/></>,
|
||
ship: <><path d="M2 11h12l-1 3h-10z M3 11V6l5-2 5 2v5 M8 4v-2 M5.5 8h5"/></>,
|
||
map: <><path d="M5.5 1.5l-3.5 1.5v11l3.5-1.5 5 1.5 3.5-1.5v-11l-3.5 1.5-5-1.5z M5.5 1.5v11 M10.5 3v11"/></>,
|
||
chart: <><path d="M2 13V3 M2 13h12 M5 11V8 M8 11V5 M11 11V7"/></>,
|
||
user: <><path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M2 14c0-3 2.5-5 6-5s6 2 6 5"/></>,
|
||
settings: <><path d="M8 5.5a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5z M8 1v1.5 M8 13.5V15 M2.5 5L4 6 M12 10l1.5 1 M2.5 11L4 10 M12 6l1.5-1 M1 8h1.5 M13.5 8H15"/></>,
|
||
plus: <><path d="M8 3v10 M3 8h10"/></>,
|
||
download: <><path d="M8 2v9 M4 7l4 4 4-4 M2.5 13.5h11"/></>,
|
||
upload: <><path d="M8 11V2 M4 6l4-4 4 4 M2.5 13.5h11"/></>,
|
||
search: <><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></>,
|
||
chevron: <><path d="M5 3l5 5-5 5"/></>,
|
||
star: <><path d="M8 1.5l2 4.5 5 .5-3.7 3.4 1 5-4.3-2.5-4.3 2.5 1-5L1 6.5l5-.5z"/></>,
|
||
edit: <><path d="M11 2l3 3-8.5 8.5h-3v-3z M9.5 3.5l3 3"/></>,
|
||
trash: <><path d="M3 4h10 M5 4V2.5h6V4 M4.5 4l.5 9.5a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1l.5-9.5"/></>,
|
||
bell: <><path d="M8 1.5a4 4 0 0 1 4 4v3l1.5 2h-11l1.5-2v-3a4 4 0 0 1 4-4z M6 11.5a2 2 0 0 0 4 0"/></>,
|
||
eye: <><path d="M1 8s2.5-4.5 7-4.5S15 8 15 8s-2.5 4.5-7 4.5S1 8 1 8z"/><circle cx="8" cy="8" r="2"/></>,
|
||
refresh: <><path d="M13.5 8a5.5 5.5 0 0 1-9.5 3.5 M13.5 4v3.5h-3.5 M2.5 8a5.5 5.5 0 0 1 9.5-3.5 M2.5 12V8.5H6"/></>,
|
||
paperclip: <><path d="M12 7l-5 5a3 3 0 1 1-4.2-4.2L9 1.5a2 2 0 1 1 3 3L5.5 11a1 1 0 1 1-1.5-1.5L10 3.5"/></>,
|
||
arrowRight: <><path d="M3 8h10 M9 4l4 4-4 4"/></>,
|
||
pkg: <><path d="M2 4.5L8 2l6 2.5V11L8 13.5 2 11z M2 4.5L8 7l6-2.5 M8 7v6.5"/></>,
|
||
};
|
||
return (
|
||
<svg className="nav-icon" width={size} height={size} viewBox="0 0 16 16"
|
||
fill="none" stroke="currentColor" strokeWidth="1.4"
|
||
strokeLinecap="round" strokeLinejoin="round">
|
||
{paths[name] || null}
|
||
</svg>
|
||
);
|
||
};
|
||
|
||
/* ─────────── Status Badge ─────────── */
|
||
const STATUS_LABELS = {
|
||
DRAFT: ["draft", "Draft"],
|
||
SUBMITTED: ["submitted", "Submitted"],
|
||
MGR_REVIEW: ["review", "Under Review"],
|
||
VENDOR_ID_PENDING: ["vendor", "Vendor Needed"],
|
||
EDITS_REQUESTED: ["edits", "Edits Requested"],
|
||
MGR_APPROVED: ["approved", "Approved"],
|
||
SENT_FOR_PAYMENT: ["sent", "Payment Sent"],
|
||
PAID_DELIVERED: ["paid", "Paid · Awaiting Receipt"],
|
||
CLOSED: ["closed", "Closed"],
|
||
REJECTED: ["rejected", "Rejected"],
|
||
};
|
||
const Badge = ({ status, children, className = "", noDot }) => {
|
||
if (status) {
|
||
const [cls, label] = STATUS_LABELS[status] || ["draft", status];
|
||
return <span className={`badge ${cls} ${noDot ? "no-dot" : ""} ${className}`}>{label}</span>;
|
||
}
|
||
return <span className={`badge ${className} ${noDot ? "no-dot" : ""}`}>{children}</span>;
|
||
};
|
||
|
||
/* ─────────── Format helpers ─────────── */
|
||
const inr = (n) => "₹" + Number(n).toLocaleString("en-IN", { maximumFractionDigits: 0 });
|
||
const inrFull = (n) => "₹" + Number(n).toLocaleString("en-IN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
|
||
/* ─────────── Card ─────────── */
|
||
const Card = ({ title, action, children, flush, className = "" }) => (
|
||
<div className={`card ${className}`}>
|
||
{(title || action) && (
|
||
<div className="card-head">
|
||
<h3 className="card-title">{title}</h3>
|
||
{action}
|
||
</div>
|
||
)}
|
||
<div className={`card-body ${flush ? "flush" : ""}`}>{children}</div>
|
||
</div>
|
||
);
|
||
|
||
/* ─────────── Stat tile ─────────── */
|
||
const Stat = ({ label, value, sub, onClick }) => (
|
||
<div className={`stat ${onClick ? "clickable" : ""}`} onClick={onClick}>
|
||
<div className="stat-label">{label}</div>
|
||
<div className="stat-value">{value}</div>
|
||
{sub && <div className="stat-sub">{sub}</div>}
|
||
</div>
|
||
);
|
||
|
||
/* ─────────── Crumbs ─────────── */
|
||
const Crumbs = ({ items }) => (
|
||
<div className="crumbs">
|
||
{items.map((it, i) => (
|
||
<React.Fragment key={i}>
|
||
{i > 0 && <span className="sep">/</span>}
|
||
<span style={{ color: i === items.length - 1 ? "var(--ink-2)" : undefined }}>{it}</span>
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
);
|
||
|
||
Object.assign(window, { Icon, Badge, Card, Stat, Crumbs, inr, inrFull, STATUS_LABELS });
|