diff --git a/frontend/src/components/maps/LocationMapBottomSheet.jsx b/frontend/src/components/maps/LocationMapBottomSheet.jsx
index 0d5c095..7e05662 100644
--- a/frontend/src/components/maps/LocationMapBottomSheet.jsx
+++ b/frontend/src/components/maps/LocationMapBottomSheet.jsx
@@ -44,6 +44,7 @@ export default function LocationMapBottomSheet({
onDeleteObject,
onShowHiddenAreas,
onSelectZoneItem,
+ onSelectUnmappedItem,
}) {
const selectedTitle = getMapObjectDisplayLabel(selectedObject);
const selectedZoneId = objectZoneId(selectedObject);
@@ -292,6 +293,7 @@ export default function LocationMapBottomSheet({
hiddenUnmappedItemCount={hiddenUnmappedItemCount}
hasHiddenUnmappedItems={hasHiddenUnmappedItems}
onShowUnmappedItems={showUnmappedItems}
+ onSelectUnmappedItem={onSelectUnmappedItem}
/>
) : null}
diff --git a/frontend/src/components/maps/LocationMapBottomSheetSections.jsx b/frontend/src/components/maps/LocationMapBottomSheetSections.jsx
index 7325668..1576c1b 100644
--- a/frontend/src/components/maps/LocationMapBottomSheetSections.jsx
+++ b/frontend/src/components/maps/LocationMapBottomSheetSections.jsx
@@ -13,6 +13,27 @@ export const MAP_DISPLAY_CONTROLS = [
const UNMAPPED_PREVIEW_LIMIT = 8;
+function MapItemActionRow({ item, onSelectItem }) {
+ const isBought = Boolean(item.bought);
+
+ return (
+
+
+
+ );
+}
+
export function LocationMapLayerPanel({ filters, setFilters }) {
return (
@@ -139,20 +160,7 @@ export function SelectedZoneItemsPanel({
{selectedZoneItems.map((item) => (
- -
-
-
+
))}
{hasHiddenSelectedItems ? (
@@ -174,6 +182,7 @@ export function UnmappedItemsPanel({
hiddenUnmappedItemCount,
hasHiddenUnmappedItems,
onShowUnmappedItems,
+ onSelectUnmappedItem,
}) {
if (visibleUnmappedItems.length === 0 && hasHiddenUnmappedItems) {
return (
@@ -203,7 +212,7 @@ export function UnmappedItemsPanel({
Unmapped Items
{unmappedItemPreview.map((item) => (
- - {item.item_name}
+
))}
{overflowUnmappedItemCount > 0 ? (
- unmappedItems(mapItems, filters, username),
[filters, mapItems, username]
);
+ const buyModalItems = useMemo(
+ () => (selectedObject ? selectedZoneItems : visibleUnmappedItems),
+ [selectedObject, selectedZoneItems, visibleUnmappedItems]
+ );
const { visibleAssignedItemCount, unmappedItemCount } = useMemo(() => {
let nextVisibleAssignedItemCount = 0;
let nextUnmappedItemCount = 0;
@@ -166,17 +170,17 @@ export default function LocationMapManager() {
return;
}
- const item = selectedZoneItems.find((candidate) => candidate.id === buyModalItem.id) || buyModalItem;
+ const item = buyModalItems.find((candidate) => candidate.id === buyModalItem.id) || buyModalItem;
try {
- const currentIndex = selectedZoneItems.findIndex((candidate) => candidate.id === item.id);
+ const currentIndex = buyModalItems.findIndex((candidate) => candidate.id === item.id);
const resolvedIndex = currentIndex >= 0 ? currentIndex : 0;
await markBought(activeHousehold.id, locationId, item.item_name, quantity, true);
if (quantity >= item.quantity) {
updateMapItems((currentItems) => currentItems.filter((candidate) => candidate.id !== item.id));
- setBuyModalItem(getNextMapBuyItem(selectedZoneItems, resolvedIndex, item.id));
+ setBuyModalItem(getNextMapBuyItem(buyModalItems, resolvedIndex, item.id));
} else {
const response = await getItemByName(activeHousehold.id, locationId, item.item_name);
const updatedItem = response.data || {
@@ -195,7 +199,7 @@ export default function LocationMapManager() {
const message = getApiErrorMessage(error, "Failed to mark item as bought");
toast.error("Mark item bought failed", `Mark item bought failed: ${message}`);
}
- }, [activeHousehold?.id, buyModalItem, locationId, selectedZoneItems, toast, updateMapItems]);
+ }, [activeHousehold?.id, buyModalItem, buyModalItems, locationId, toast, updateMapItems]);
const mapSize = useMemo(
() => ({
@@ -435,6 +439,7 @@ export default function LocationMapManager() {
onDeleteObject={requestDeleteObject}
onShowHiddenAreas={handleShowHiddenAreas}
onSelectZoneItem={setBuyModalItem}
+ onSelectUnmappedItem={setBuyModalItem}
/>
>
)}
@@ -443,7 +448,7 @@ export default function LocationMapManager() {
{buyModalItem ? (
{
+ await mockMapShell(page);
+
+ const unmappedMapItems = [
+ {
+ id: 4201,
+ item_name: "loose batteries",
+ quantity: 1,
+ bought: false,
+ zone_id: null,
+ zone: null,
+ added_by_users: ["map-user"],
+ },
+ {
+ id: 4202,
+ item_name: "paper plates",
+ quantity: 2,
+ bought: false,
+ zone_id: null,
+ zone: null,
+ added_by_users: ["map-user"],
+ },
+ ];
+ const mapState = {
+ ...publishedMapState([], true),
+ items: unmappedMapItems,
+ unmapped_count: unmappedMapItems.length,
+ };
+ const boughtPayloads: Array> = [];
+
+ await page.route("**/households/1/locations/10/map", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(mapState),
+ });
+ });
+
+ await page.route("**/households/1/locations/10/list/item", async (route) => {
+ boughtPayloads.push(await route.request().postDataJSON());
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ success: true }),
+ });
+ });
+
+ await page.goto("/stores/100/locations/10/map");
+ await page.getByRole("button", { name: "Layers" }).click();
+ await page.getByRole("button", { name: "Unmapped" }).click();
+
+ await page.getByRole("button", { name: "Mark loose batteries bought" }).click();
+ await expect(page.locator(".confirm-buy-item-name")).toHaveText("loose batteries");
+ await page.getByRole("button", { name: "Mark as Bought" }).click();
+
+ await expect.poll(() => boughtPayloads.length).toBe(1);
+ expect(boughtPayloads[0]).toMatchObject({
+ item_name: "loose batteries",
+ bought: true,
+ quantity_bought: 1,
+ });
+ await expect(page.locator(".confirm-buy-item-name")).toHaveText("paper plates");
+ await expect(page.getByText("loose batteries")).toHaveCount(0);
+ await expect(page.getByLabel("Map summary").locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("1");
+
+ await page.getByRole("button", { name: "Mark as Bought" }).click();
+
+ await expect.poll(() => boughtPayloads.length).toBe(2);
+ expect(boughtPayloads[1]).toMatchObject({
+ item_name: "paper plates",
+ bought: true,
+ quantity_bought: 2,
+ });
+ await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
+ await expect(page.getByText("paper plates")).toHaveCount(0);
+ await expect(page.getByLabel("Map summary").locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("0");
+});
+
test("viewer hides unrelated unmapped items while a zone is selected", async ({ page }) => {
await mockMapShell(page);