Skip to content

Commit 679640d

Browse files
committed
UPDATE - shop and andmin shop panel UI
1 parent 82b103b commit 679640d

5 files changed

Lines changed: 385 additions & 59 deletions

File tree

client/src/components/AdminShopOrdersPage.jsx

Lines changed: 165 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22
import "./AdminShopPage.css";
33

4+
function formatUsd(value) {
5+
return `$${Number(value ?? 0).toFixed(2)}`;
6+
}
7+
8+
function formatDate(value) {
9+
if (!value) return "Unknown date";
10+
return new Date(value).toLocaleString();
11+
}
12+
13+
function orderStatus(order) {
14+
if (order.rejected) return "rejected";
15+
if (order.fulfilled) return "fulfilled";
16+
return "pending";
17+
}
18+
19+
function orderUsdTotals(order) {
20+
const quantity = Number(order.quantity ?? 1);
21+
const shipping = Number(order.shippingTaxUsd ?? 0);
22+
const itemUsd = order.itemPriceUsd != null ? Number(order.itemPriceUsd) : Number(order.itemBricks ?? 0) / 10;
23+
const itemsOnly = itemUsd * quantity;
24+
return {
25+
itemsOnly,
26+
shipping,
27+
total: itemsOnly + shipping,
28+
};
29+
}
30+
31+
function groupLabel(groupBy, key) {
32+
if (groupBy === "user") return `Customer: ${key}`;
33+
if (groupBy === "item") return `Item: ${key}`;
34+
return "Flat list";
35+
}
36+
437
export function AdminShopOrdersPage() {
538
const [orders, setOrders] = useState([]);
639
const [loading, setLoading] = useState(true);
740
const [error, setError] = useState("");
41+
const [statusFilter, setStatusFilter] = useState("pending");
42+
const [groupBy, setGroupBy] = useState("flat");
43+
const [sortOrder, setSortOrder] = useState("newest");
844

945
useEffect(() => {
1046
async function loadOrders() {
@@ -40,16 +76,48 @@ export function AdminShopOrdersPage() {
4076
}
4177
}
4278

43-
const groupedOrders = orders.reduce((acc, order) => {
44-
const key = order.email;
45-
if (!acc[key]) {
46-
acc[key] = [];
79+
async function rejectOrder(order) {
80+
const ok = window.confirm(`Reject order #${order.id} and refund ${Number(order.totalBricks ?? 0)} bricks?`);
81+
if (!ok) return;
82+
83+
try {
84+
const response = await fetch(`/api/admin/shop/orders/${order.id}/reject`, {
85+
method: "POST",
86+
credentials: "include",
87+
});
88+
const data = await response.json();
89+
if (!response.ok) throw new Error(data.error || "Failed to reject order.");
90+
91+
setOrders((prev) =>
92+
prev.map((current) => (current.id === order.id ? { ...current, rejected: true } : current))
93+
);
94+
} catch (err) {
95+
alert(`Error: ${err.message}`);
4796
}
48-
acc[key].push(order);
49-
return acc;
50-
}, {});
97+
}
98+
99+
const visibleOrders = useMemo(() => {
100+
const filtered = orders.filter((order) => statusFilter === "all" || orderStatus(order) === statusFilter);
101+
return [...filtered].sort((a, b) => {
102+
if (sortOrder === "oldest") return new Date(a.createdAt) - new Date(b.createdAt);
103+
if (sortOrder === "bricks-high") return Number(b.totalBricks ?? 0) - Number(a.totalBricks ?? 0);
104+
if (sortOrder === "usd-high") return orderUsdTotals(b).total - orderUsdTotals(a).total;
105+
return new Date(b.createdAt) - new Date(a.createdAt);
106+
});
107+
}, [orders, sortOrder, statusFilter]);
108+
109+
const groupedOrders = useMemo(() => {
110+
if (groupBy === "flat") return [["all", visibleOrders]];
51111

52-
const sortedEmails = Object.keys(groupedOrders).sort();
112+
const groups = visibleOrders.reduce((acc, order) => {
113+
const key = groupBy === "item" ? order.itemName || "Unknown item" : order.email || "Unknown customer";
114+
if (!acc.has(key)) acc.set(key, []);
115+
acc.get(key).push(order);
116+
return acc;
117+
}, new Map());
118+
119+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
120+
}, [groupBy, visibleOrders]);
53121

54122
return (
55123
<main className="admin-shop-page">
@@ -65,53 +133,95 @@ export function AdminShopOrdersPage() {
65133
{!loading && !error && orders.length === 0 && <p>No orders yet.</p>}
66134

67135
{!loading && !error && orders.length > 0 && (
68-
<div className="admin-shop-orders">
69-
{sortedEmails.map((email) => (
70-
<div key={email} className="admin-shop-order-group">
71-
<h2>{email}</h2>
72-
<table className="admin-shop-orders-table">
73-
<thead>
74-
<tr>
75-
<th>Item</th>
76-
<th>Quantity</th>
77-
<th>Total Cost</th>
78-
<th>Date</th>
79-
<th>Status</th>
80-
<th>Action</th>
81-
</tr>
82-
</thead>
83-
<tbody>
84-
{groupedOrders[email].map((order) => (
85-
<tr key={order.id} className={order.fulfilled ? "fulfilled" : ""}>
86-
<td>{order.itemName}</td>
87-
<td className="text-center">{order.quantity}</td>
88-
<td className="text-right">
89-
{order.totalbricks} bricks
90-
{order.shippingTaxUsd ? ` (+$${order.shippingTaxUsd})` : ""}
91-
</td>
92-
<td>{new Date(order.createdAt).toLocaleDateString()}</td>
93-
<td className="text-center">
94-
<span className={`status-badge ${order.fulfilled ? "fulfilled" : "pending"}`}>
95-
{order.fulfilled ? "Fulfilled" : "Pending"}
96-
</span>
97-
</td>
98-
<td className="text-center">
99-
{!order.fulfilled && (
100-
<button
101-
className="admin-shop-fulfill-btn"
102-
onClick={() => markAsFulfilled(order.id)}
103-
>
104-
Mark Fulfilled
136+
<>
137+
<div className="admin-shop-order-controls">
138+
<label>
139+
Status
140+
<select value={statusFilter} onChange={(event) => setStatusFilter(event.target.value)}>
141+
<option value="pending">Pending queue</option>
142+
<option value="fulfilled">Fulfilled</option>
143+
<option value="rejected">Rejected/refunded</option>
144+
<option value="all">All orders</option>
145+
</select>
146+
</label>
147+
<label>
148+
Group by
149+
<select value={groupBy} onChange={(event) => setGroupBy(event.target.value)}>
150+
<option value="flat">Flat list</option>
151+
<option value="user">Customer</option>
152+
<option value="item">Item</option>
153+
</select>
154+
</label>
155+
<label>
156+
Queue order
157+
<select value={sortOrder} onChange={(event) => setSortOrder(event.target.value)}>
158+
<option value="newest">Newest first</option>
159+
<option value="oldest">Oldest first</option>
160+
<option value="bricks-high">Most bricks first</option>
161+
<option value="usd-high">Highest $ first</option>
162+
</select>
163+
</label>
164+
<span className="admin-shop-order-count">
165+
{visibleOrders.length} cards · {orders.length} DB rows
166+
</span>
167+
</div>
168+
169+
{visibleOrders.length === 0 ? <p>No orders match this view.</p> : null}
170+
171+
<div className="admin-shop-orders">
172+
{groupedOrders.map(([key, groupOrders]) => (
173+
<section key={key} className="admin-shop-order-group">
174+
{groupBy !== "flat" ? <h2>{groupLabel(groupBy, key)}</h2> : null}
175+
<div className="admin-shop-order-cards">
176+
{groupOrders.map((order) => {
177+
const usd = orderUsdTotals(order);
178+
const status = orderStatus(order);
179+
const isActionable = status === "pending";
180+
return (
181+
<article key={order.id} className={`admin-shop-order-card is-${status}`}>
182+
<div className="admin-shop-order-card-main">
183+
<span className={`status-badge ${status}`}>{status}</span>
184+
<h2>{order.itemName || "Unknown item"}</h2>
185+
<p>
186+
Qty: {order.quantity} · Total: {Number(order.totalBricks ?? 0)} bricks
187+
</p>
188+
189+
<div className="admin-shop-order-money">
190+
<span>Total: {formatUsd(usd.total)}</span>
191+
<span>Items-only: {formatUsd(usd.itemsOnly)}</span>
192+
<span>Shipping/tax: {formatUsd(usd.shipping)}</span>
193+
<span>Item price: {formatUsd(order.itemPriceUsd ?? Number(order.itemBricks ?? 0) / 10)}</span>
194+
</div>
195+
196+
<div className="admin-shop-order-customer">
197+
<strong>Customer</strong>
198+
<span>Name: {order.name || "Unknown"}</span>
199+
<span>Email: {order.email || "Unknown"}</span>
200+
<span>User ID: #{order.userId}</span>
201+
</div>
202+
203+
<footer>
204+
<span>DB row: #{order.id}</span>
205+
<span>Created: {formatDate(order.createdAt)}</span>
206+
</footer>
207+
</div>
208+
209+
<div className="admin-shop-order-card-actions">
210+
<button type="button" disabled={!isActionable} onClick={() => markAsFulfilled(order.id)}>
211+
✓ Mark fulfilled
212+
</button>
213+
<button type="button" disabled={!isActionable} className="danger" onClick={() => rejectOrder(order)}>
214+
× Reject (refund bricks)
105215
</button>
106-
)}
107-
</td>
108-
</tr>
109-
))}
110-
</tbody>
111-
</table>
112-
</div>
113-
))}
114-
</div>
216+
</div>
217+
</article>
218+
);
219+
})}
220+
</div>
221+
</section>
222+
))}
223+
</div>
224+
</>
115225
)}
116226
</section>
117227
</main>

0 commit comments

Comments
 (0)