fix(admin): theme, American English, and inventory/reservation tooling (#27)
SonarQube Analysis / sonarqube (pull_request) Successful in 4m23s
Tests / backend-unit (pull_request) Successful in 1m6s
Tests / backend-integration (pull_request) Failing after 4m55s
Tests / frontend-e2e (pull_request) Failing after 12m41s

Seven reported items, of which the first four had two root causes.

The active tab was invisible in dark mode because colorPrimary was
hardcoded to #1a1a1a in both themes. The accent now inverts with the
theme, and colorTextLightSolid inverts with it, or a near-white accent
would get antd's default white label and disappear.

The Category tab, Tag tab, and item-form category selector ignored the
theme entirely. antd declares main: lib/index.js and module: es/index.js,
so importing from 'antd' resolves to the ES build while 'antd/lib/...'
loads the CommonJS one — two copies, two React contexts, and no
ConfigProvider for anything deep-imported. Switching those files to
antd/es/* keeps the deep-import convention and shares the instance. This
was introduced by my own use of the lib path; es is correct under Vite.
Two storefront components had the same latent bug.

"Colour" is now "Color".

The Customers tab shows how many items each customer is holding, as a
link opening the item list with a Release button. Release mirrors the
customer's own cart removal — drop the cart row, return the item to
available, guarded on 'reserved' so it can never resurrect a sold item —
and deliberately sends no email about an action the customer did not
take. The count is a subquery rather than another join, which would have
multiplied rows and inflated order_count and total_spent_cents.

The Inventory tab filters by category, tags, price, and status, reusing
the storefront's parser and query builder so the two cannot drift.
Reserved is one option in a Status filter rather than a standalone toggle.

Also fixes two defects the screenshots exposed: the reserved-count link
bubbled to the row handler and opened the customer drawer behind the
dialog, and .admin-category-node had no CSS at all, so the tree node name,
item count, and actions ran together as one string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:49:05 -05:00
co-authored by Claude Opus 5
parent b6ef2bb63c
commit f537314259
22 changed files with 1027 additions and 65 deletions
+20 -2
View File
@@ -20,6 +20,8 @@ import Settings from './Settings';
import Categories from './Categories';
import Tags from './Tags';
import CategoryTreeSelect from './CategoryTreeSelect';
import InventoryFilters from './InventoryFilters';
import { ItemFilters, EMPTY_FILTERS } from '../filters';
const { Header, Content } = Layout;
const { Title } = Typography;
@@ -34,9 +36,10 @@ function Inventory() {
const [categories, setCategories] = useState<Category[]>([]);
const [tags, setTags] = useState<TagRecord[]>([]);
const [saving, setSaving] = useState(false);
const [filters, setFilters] = useState<ItemFilters>(EMPTY_FILTERS);
const { mode } = useThemeMode();
const load = () => fetchAdminItems().then(setItems);
const load = (active: ItemFilters = filters) => fetchAdminItems(active).then(setItems);
// The item form needs the current category tree and tag list; both change
// from the sibling tabs, so they're refetched whenever the modal opens.
@@ -45,7 +48,13 @@ function Inventory() {
fetchAdminTags().then(setTags)
]);
useEffect(() => { load(); loadOptions(); }, []);
// Refetch whenever the filters change — filtering is server-side so the
// result stays correct regardless of how many items exist.
useEffect(() => { load(filters); }, [filters]);
useEffect(() => { loadOptions(); }, []);
function applyFilters(next: ItemFilters) { setFilters(next); }
function clearFilters() { setFilters(EMPTY_FILTERS); }
function openNew() {
setEditingItem(null);
@@ -193,6 +202,15 @@ function Inventory() {
<Title level={4} style={{ margin: 0 }}>Inventory</Title>
<Button type="primary" onClick={openNew}>Add Item</Button>
</div>
<InventoryFilters
categories={categories}
tags={tags}
filters={filters}
onChange={applyFilters}
onClear={clearFilters}
/>
<Table rowKey="id" dataSource={items} columns={columns} scroll={{ x: true }} />
<Modal title={editingItem ? 'Edit Item' : 'Add Item'} open={modalOpen} onOk={handleOk} confirmLoading={saving} onCancel={() => setModalOpen(false)} destroyOnHidden width={720}>