/* ===========================================================================
VESTIRÉ STUDIO — admin: dashboard · requests · messages -> window
=========================================================================== */
const { useState, useEffect, useRef, useMemo } = React;
function adminMetrics(requests) {
const active = requests.filter((r) => r.status !== "Cancelled");
const revenue = requests.filter((r) => r.status === "Completed").reduce((a, b) => a + b.total, 0);
const pipeline = active.filter((r) => r.status !== "Completed").reduce((a, b) => a + b.total, 0);
return {
revenue, pipeline,
total: requests.length,
pending: requests.filter((r) => r.status === "Pending").length,
processing: requests.filter((r) => r.status === "Processing").length,
completed: requests.filter((r) => r.status === "Completed").length,
cancelled: requests.filter((r) => r.status === "Cancelled").length
};
}
function AdminPageHead({ kicker, title, children }) {
return (
{kicker}
{title}
{children}
);
}
/* ---- DASHBOARD ----------------------------------------------------------- */
function ADashboard({ ctx }) {
const m = adminMetrics(ctx.requests);
const recent = [...ctx.requests].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 6);
const unread = ctx.messages.filter((x) => !x.isRead);
const lowStock = ctx.gowns.filter((g) => g.stock <= 1);
const months = ["Feb", "Mar", "Apr", "May", "Jun"];
const byMonth = { Feb: 8, Mar: 22, Apr: 31, May: 27, Jun: 42 };
const maxM = Math.max(...Object.values(byMonth));
return (
ctx.nav("messages")}>Inbox{unread.length ? " · " + unread.length : ""}
ctx.nav("gownEdit", { id: null })}>Add gown
{/* recent requests */}
Recent requests
ctx.nav("requests")}>Manage all
| Request | Customer | Items | Status | Total |
{recent.map((r) => (
ctx.nav("requests", { focus: r.id })}>
{r.code}{fmtDate(r.createdAt)} |
{r.customer.name} |
{r.items.length} · {r.type} |
|
{VS.peso(r.total)} |
))}
{/* side column */}
Revenue trend
{months.map((mo) => (
))}
Jun to date{VS.peso(42000)}
Unread messages
{unread.length}
{unread.slice(0, 3).map((x) => (
ctx.nav("messages", { focus: x.id })}>
{initials(x.name)}
{x.subject}{x.body}
))}
{unread.length === 0 &&
You're all caught up.}
ctx.nav("messages")}>Open inbox
);
}
/* ---- REQUESTS (admin) ---------------------------------------------------- */
const NEXT_ACTION = {
Pending: [["Approve → Processing", "Processing", "accent", "check"], ["Decline", "Cancelled", "danger", "x"]],
Processing: [["Mark completed", "Completed", "accent", "check"], ["Cancel", "Cancelled", "danger", "x"]],
Completed: [["Reopen", "Processing", "soft", "arrowL"]],
Cancelled: [["Restore → Pending", "Pending", "soft", "arrowL"]]
};
function ARequests({ ctx }) {
const focus = (ctx.route.params && ctx.route.params.focus) || null;
const [status, setStatus] = useState("All");
const [q, setQ] = useState("");
const [open, setOpen] = useState(focus);
const STAT = ["All", "Pending", "Processing", "Completed", "Cancelled"];
const list = ctx.requests
.filter((r) => (status === "All" || r.status === status) && (q === "" || r.code.toLowerCase().includes(q.toLowerCase()) || r.customer.name.toLowerCase().includes(q.toLowerCase())))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
return (
setQ(e.target.value)} />
{STAT.map((s) => {
const n = s === "All" ? ctx.requests.length : ctx.requests.filter((r) => r.status === s).length;
return ;
})}
{list.map((r) => {
const isOpen = open === r.id;
return (
setOpen(isOpen ? null : r.id)}>
{initials(r.customer.name)}
{r.code}{r.type}
{r.customer.name} · {fmtDate(r.createdAt)} · {r.items.length} piece{r.items.length !== 1 ? "s" : ""}
{VS.peso(r.total)}
{isOpen && (
Line items
{r.items.map((it, i) => (
{it.name}{it.lineType} · size {it.size} · qty {it.qty} · {VS.peso(it.unitPrice)} each
{VS.peso(it.subtotal)}
))}
Total{VS.peso(r.total)}
{r.type === "rental" &&
Pickup date{fmtDate(r.rentalStart)}
Rental days{r.rentalDays}
Payment{r.payment}
}
Customer
{r.customer.name}
{r.customer.email}
{r.customer.phone}
{r.customer.address}
Update status
{(NEXT_ACTION[r.status] || []).map(([lbl, to, variant, icon]) => (
ctx.setRequestStatus(r.id, to)}>{lbl}
))}
)}
);
})}
{list.length === 0 &&
}
);
}
/* ---- MESSAGES (admin inbox) --------------------------------------------- */
function AMessages({ ctx }) {
const focus = (ctx.route.params && ctx.route.params.focus) || null;
const sorted = [...ctx.messages].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
const [sel, setSel] = useState(focus || (sorted[0] && sorted[0].id));
const [filter, setFilter] = useState("All");
const list = sorted.filter((x) => filter === "All" || (filter === "Unread" ? !x.isRead : x.isRead));
const current = ctx.messages.find((x) => x.id === sel);
const unread = ctx.messages.filter((x) => !x.isRead).length;
function openMsg(id) { setSel(id); ctx.markRead(id); }
return (
{["All", "Unread", "Read"].map((f) => )}
{list.map((x) => (
openMsg(x.id)} className="row gap12" style={{
padding: "18px 20px", cursor: "pointer", borderTop: "1px solid var(--line-2)",
background: sel === x.id ? "var(--accent-soft)" : "transparent"
}}>
{initials(x.name)}
{!x.isRead && }
{x.name}{String(x.createdAt).slice(5, 10)}
{x.subject}
{x.body}
))}
{list.length === 0 &&
Nothing here.
}
{current ? (
{initials(current.name)}
{current.name}
{current.email}
{fmtDate(String(current.createdAt).slice(0, 10))} · {String(current.createdAt).slice(11)}
{current.subject}
{current.body}
ctx.showToast("Reply sent to " + current.name.split(" ")[0])}>Send reply
ctx.toggleRead(current.id)}>{current.isRead ? "Mark unread" : "Mark read"}
) :
Select a message.
}
);
}
Object.assign(window, { adminMetrics, AdminPageHead, ADashboard, ARequests, AMessages });