diff --git a/docs/README.md b/docs/README.md
index 24b6be8..36bc9fa 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -24,6 +24,7 @@ This directory contains practical project documentation. Root-level rules still
## Guides
- `guides/api-documentation.md`: REST API reference. Verify against current code before changing APIs.
- `guides/frontend-readme.md`: frontend development notes.
+- `guides/management-modal-patterns.md`: reusable modal patterns for managing scoped item/list records.
- `guides/MOBILE_RESPONSIVE_AUDIT.md`: mobile design and audit checklist.
- `guides/setup-checklist.md`: older setup checklist; prefer `DEVELOPMENT.md` for current commands.
diff --git a/docs/guides/management-modal-patterns.md b/docs/guides/management-modal-patterns.md
new file mode 100644
index 0000000..61060b8
--- /dev/null
+++ b/docs/guides/management-modal-patterns.md
@@ -0,0 +1,42 @@
+# Management Modal Patterns
+
+Use this guide for modals that manage scoped lists of app-owned records, such as store items now and store zones or locations later.
+
+## Purpose
+- Management modals should keep users in the current workflow while they inspect, edit, add, or remove records for a scoped parent.
+- The parent scope must be obvious in the title, for example `Costco Items`.
+- Modals should avoid repeating table labels inside every row. Use row layout, grouping, and the edit surface for detail.
+
+## Structure
+- Header: title, one short description when the scope is not obvious, and a close button.
+- Primary toolbar: search input plus the primary create action inline.
+- Bulk toolbar: destructive or multi-select actions above the list, separate from search/create controls.
+- List: compact rows with the record's primary identity and any essential visual affordance.
+- Editor: clicking or tapping a row opens the edit/settings modal for that record.
+- Confirmation: destructive actions must use `ConfirmSlideModal`, not browser dialogs.
+
+## Row Behavior
+- Normal mode: the entire row opens settings for that record.
+- Delete mode: the row toggles selected/unselected state and does not open settings.
+- Selection state must be visible on the row and must not rely only on color.
+- Avoid per-row action buttons when the same action applies to every row.
+
+## Bulk Delete Pattern
+- Show a `Delete Items` button above the list for users with delete permission.
+- Clicking `Delete Items` enters delete mode, clears any previous selection, and changes the button to `Confirm Delete (# selected)`.
+- Show a `Cancel` button while delete mode is active.
+- Disable confirm while zero items are selected.
+- Clicking confirm opens `ConfirmSlideModal`; only the slide confirmation performs the mutation.
+- On success, exit delete mode, clear selection, refresh the list, and show a toast.
+- On failure, keep the modal open and show a toast with the API error summary.
+
+## Permission Rules
+- Keep authorization server-side. Client visibility only improves UX.
+- Members can open item settings when the API allows them to manage item details.
+- Delete controls should be shown only to owners/admins when deletion is admin-scoped.
+
+## Accessibility
+- Modal containers should use dialog semantics when practical.
+- Rows that perform actions should be keyboard reachable.
+- Delete-mode rows should expose selected state with `aria-pressed` or an equivalent state.
+- Buttons must have stable labels that describe the action in the current mode.
diff --git a/frontend/src/components/manage/StoreAvailableItemsManager.jsx b/frontend/src/components/manage/StoreAvailableItemsManager.jsx
index a31c8c5..bcd6c42 100644
--- a/frontend/src/components/manage/StoreAvailableItemsManager.jsx
+++ b/frontend/src/components/manage/StoreAvailableItemsManager.jsx
@@ -31,7 +31,12 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
const [loading, setLoading] = useState(false);
const [editorItem, setEditorItem] = useState(null);
const [showEditor, setShowEditor] = useState(false);
- const [pendingDeleteItem, setPendingDeleteItem] = useState(null);
+ const [deleteMode, setDeleteMode] = useState(false);
+ const [selectedDeleteIds, setSelectedDeleteIds] = useState(() => new Set());
+ const [pendingDeleteItems, setPendingDeleteItems] = useState([]);
+
+ const selectedDeleteItems = items.filter((item) => selectedDeleteIds.has(item.item_id));
+ const selectedDeleteCount = selectedDeleteItems.length;
const loadItems = useCallback(async (search = query) => {
if (!householdId || !store?.id) {
@@ -82,7 +87,9 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
const closeManager = () => {
setIsOpen(false);
- setPendingDeleteItem(null);
+ setDeleteMode(false);
+ setSelectedDeleteIds(new Set());
+ setPendingDeleteItems([]);
};
const handleUpdate = async (payload) => {
@@ -115,19 +122,64 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
}
};
+ const openEditor = (item) => {
+ setEditorItem(item);
+ setShowEditor(true);
+ };
+
+ const toggleDeleteSelection = (itemId) => {
+ setSelectedDeleteIds((currentIds) => {
+ const nextIds = new Set(currentIds);
+
+ if (nextIds.has(itemId)) {
+ nextIds.delete(itemId);
+ } else {
+ nextIds.add(itemId);
+ }
+
+ return nextIds;
+ });
+ };
+
+ const startDeleteMode = () => {
+ setDeleteMode(true);
+ setSelectedDeleteIds(new Set());
+ };
+
+ const cancelDeleteMode = () => {
+ setDeleteMode(false);
+ setSelectedDeleteIds(new Set());
+ };
+
+ const confirmSelectedDelete = () => {
+ if (selectedDeleteCount === 0) {
+ return;
+ }
+
+ setPendingDeleteItems(selectedDeleteItems);
+ };
+
const handleDeleteConfirm = async () => {
- if (!pendingDeleteItem) {
+ if (pendingDeleteItems.length === 0) {
return;
}
try {
- await deleteAvailableItem(householdId, store.id, pendingDeleteItem.item_id);
- toast.success("Deleted store item", `Deleted ${pendingDeleteItem.item_name} from ${store.display_name || store.name}`);
- setPendingDeleteItem(null);
+ await Promise.all(
+ pendingDeleteItems.map((item) => deleteAvailableItem(householdId, store.id, item.item_id))
+ );
+ const count = pendingDeleteItems.length;
+ toast.success(
+ count === 1 ? "Deleted store item" : "Deleted store items",
+ `Deleted ${count} ${count === 1 ? "item" : "items"} from ${store.display_name || store.name}`
+ );
+ setPendingDeleteItems([]);
+ setDeleteMode(false);
+ setSelectedDeleteIds(new Set());
await loadItems(query);
} catch (error) {
const message = getApiErrorMessage(error, "Failed to delete store item");
- toast.error("Delete store item failed", `Delete store item failed: ${message}`);
+ toast.error("Delete store items failed", `Delete store items failed: ${message}`);
}
};
@@ -186,6 +238,28 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
+ {isAdmin && catalogReady && items.length > 0 ? (
+
+
+ {deleteMode ? (
+
+ ) : null}
+
+ ) : null}
+
{!catalogReady ? (
Run the latest database migrations to enable store item management.