diff --git a/frontend/src/components/store/StoreTabs.jsx b/frontend/src/components/store/StoreTabs.jsx
index ff65582..0884d48 100644
--- a/frontend/src/components/store/StoreTabs.jsx
+++ b/frontend/src/components/store/StoreTabs.jsx
@@ -1,7 +1,10 @@
import { useContext, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import { StoreContext } from '../../context/StoreContext';
import '../../styles/components/StoreTabs.css';
+const DEFAULT_LOCATION_NAME = "Default Location";
+
function getStoreKey(store) {
return String(store?.household_store_id ?? store?.store_id ?? store?.id ?? "");
}
@@ -10,6 +13,15 @@ function getStoreName(store) {
return store?.name || store?.display_name || "Store";
}
+function getLocationName(store) {
+ if (!store) return "Location";
+ const locationName = store.location_name || "";
+ if (!locationName || locationName === DEFAULT_LOCATION_NAME) {
+ return "Default";
+ }
+ return locationName;
+}
+
function chooseStoreLocation(locations) {
if (!locations || locations.length === 0) return null;
return (
@@ -42,12 +54,18 @@ function buildStoreOptions(stores) {
export default function StoreTabs() {
const { stores, activeStore, setActiveStore, loading } = useContext(StoreContext);
- const [isPickerOpen, setIsPickerOpen] = useState(false);
+ const navigate = useNavigate();
+ const [activePicker, setActivePicker] = useState(null);
const storeOptions = useMemo(() => buildStoreOptions(stores || []), [stores]);
const activeStoreKey = getStoreKey(activeStore);
const selectedStoreOption =
storeOptions.find((option) => option.key === activeStoreKey) || storeOptions[0];
+ const selectedLocations = selectedStoreOption?.locations || [];
+ const selectedLocation =
+ selectedLocations.find((location) => String(location.id) === String(activeStore?.id)) ||
+ selectedStoreOption?.store;
const canPickStore = storeOptions.length > 1 && !loading;
+ const canPickLocation = selectedLocations.length > 1 && !loading;
if (!stores || stores.length === 0 || storeOptions.length === 0) {
return (
@@ -59,14 +77,29 @@ export default function StoreTabs() {
);
}
- const handleTriggerClick = () => {
+ const handleStoreTriggerClick = () => {
if (!canPickStore) return;
- setIsPickerOpen(true);
+ setActivePicker("store");
+ };
+
+ const handleLocationTriggerClick = () => {
+ if (!canPickLocation) return;
+ setActivePicker("location");
};
const handleStoreSelect = (store) => {
setActiveStore(store);
- setIsPickerOpen(false);
+ setActivePicker(null);
+ };
+
+ const handleLocationSelect = (location) => {
+ setActiveStore(location);
+ setActivePicker(null);
+ };
+
+ const handleManageMap = () => {
+ const locationQuery = selectedLocation?.id ? `&locationId=${selectedLocation.id}` : "";
+ navigate(`/manage?tab=stores${locationQuery}`);
};
return (
@@ -75,18 +108,41 @@ export default function StoreTabs() {
+
+
+
+
- {isPickerOpen ? (
-
setIsPickerOpen(false)}>
+ {activePicker === "store" ? (
+
setActivePicker(null)}>
) : null}
+
+ {activePicker === "location" ? (
+
setActivePicker(null)}>
+
event.stopPropagation()}
+ >
+
Locations
+
+ {selectedLocations.map((location) => (
+
+ ))}
+
+
+
+ ) : null}
);
}
diff --git a/frontend/src/context/StoreContext.jsx b/frontend/src/context/StoreContext.jsx
index 458b4de..7a7f22f 100644
--- a/frontend/src/context/StoreContext.jsx
+++ b/frontend/src/context/StoreContext.jsx
@@ -30,6 +30,22 @@ function chooseStoreByHouseholdStoreId(stores, householdStoreId) {
return chooseStoreLocation(matchingLocations);
}
+function getLocationStorageKey(householdId, householdStoreId) {
+ return `activeStoreLocationId_${householdId}_${householdStoreId}`;
+}
+
+function chooseLocationByStoredIds(stores, householdId, householdStoreId) {
+ const storageKey = getLocationStorageKey(householdId, householdStoreId);
+ const savedLocationId = localStorage.getItem(storageKey);
+ if (!savedLocationId) return null;
+
+ return stores.find(
+ (store) =>
+ String(store.id) === String(savedLocationId) &&
+ getHouseholdStoreKey(store) === String(householdStoreId)
+ );
+}
+
export const StoreContext = createContext({
stores: [],
activeStore: null,
@@ -74,7 +90,9 @@ export const StoreProvider = ({ children }) => {
const savedHouseholdStoreId = localStorage.getItem(householdStoreStorageKey);
if (savedHouseholdStoreId) {
- const store = chooseStoreByHouseholdStoreId(stores, savedHouseholdStoreId);
+ const store =
+ chooseLocationByStoredIds(stores, activeHousehold.id, savedHouseholdStoreId) ||
+ chooseStoreByHouseholdStoreId(stores, savedHouseholdStoreId);
if (store) {
console.log('[StoreContext] Found saved household store:', store);
setActiveStoreState(store);
@@ -85,11 +103,16 @@ export const StoreProvider = ({ children }) => {
const savedLocationId = localStorage.getItem(legacyLocationStorageKey);
if (savedLocationId) {
const savedLocation = stores.find(s => String(s.id) === String(savedLocationId));
- const store = chooseStoreByHouseholdStoreId(stores, getHouseholdStoreKey(savedLocation));
+ const store = savedLocation || chooseStoreByHouseholdStoreId(stores, getHouseholdStoreKey(savedLocation));
if (store) {
console.log('[StoreContext] Migrated saved location to store:', store);
+ const householdStoreId = getHouseholdStoreKey(store);
setActiveStoreState(store);
- localStorage.setItem(householdStoreStorageKey, getHouseholdStoreKey(store));
+ localStorage.setItem(householdStoreStorageKey, householdStoreId);
+ localStorage.setItem(
+ getLocationStorageKey(activeHousehold.id, householdStoreId),
+ String(store.id)
+ );
return;
}
}
@@ -101,7 +124,12 @@ export const StoreProvider = ({ children }) => {
);
console.log('[StoreContext] Using store:', defaultStore);
setActiveStoreState(defaultStore);
- localStorage.setItem(householdStoreStorageKey, getHouseholdStoreKey(defaultStore));
+ const householdStoreId = getHouseholdStoreKey(defaultStore);
+ localStorage.setItem(householdStoreStorageKey, householdStoreId);
+ localStorage.setItem(
+ getLocationStorageKey(activeHousehold.id, householdStoreId),
+ String(defaultStore.id)
+ );
}, [stores, activeHousehold]);
useEffect(() => {
@@ -142,7 +170,12 @@ export const StoreProvider = ({ children }) => {
setActiveStoreState(store);
if (store && activeHousehold) {
const storageKey = `activeHouseholdStoreId_${activeHousehold.id}`;
- localStorage.setItem(storageKey, getHouseholdStoreKey(store));
+ const householdStoreId = getHouseholdStoreKey(store);
+ localStorage.setItem(storageKey, householdStoreId);
+ localStorage.setItem(
+ getLocationStorageKey(activeHousehold.id, householdStoreId),
+ String(store.id)
+ );
}
};
diff --git a/frontend/src/styles/components/StoreTabs.css b/frontend/src/styles/components/StoreTabs.css
index c7072f8..97cf777 100644
--- a/frontend/src/styles/components/StoreTabs.css
+++ b/frontend/src/styles/components/StoreTabs.css
@@ -6,7 +6,10 @@
.store-tabs-container {
display: flex;
+ flex-direction: column;
+ align-items: center;
justify-content: center;
+ gap: 0.5rem;
padding: 0.5rem 0 0;
width: 100%;
}
@@ -46,6 +49,37 @@
cursor: not-allowed;
}
+.location-selector-trigger {
+ min-height: 40px;
+ font-size: 0.95rem;
+}
+
+.store-map-button {
+ width: min(100%, 420px);
+ min-height: 38px;
+ padding: 0.6rem 1rem;
+ border: 1px solid var(--color-border-light);
+ border-radius: 8px;
+ background: var(--color-bg-elevated);
+ color: var(--color-text-primary);
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.store-map-button:hover,
+.store-map-button:focus-visible {
+ border-color: var(--color-primary);
+ background: var(--color-bg-hover);
+ outline: none;
+}
+
+.store-map-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
.store-name {
font-weight: 500;
min-width: 0;
diff --git a/frontend/tests/store-selector.spec.ts b/frontend/tests/store-selector.spec.ts
index e9c8e4f..f5102fb 100644
--- a/frontend/tests/store-selector.spec.ts
+++ b/frontend/tests/store-selector.spec.ts
@@ -84,6 +84,14 @@ test("grocery store selector shows one selected store and picks from a modal", a
display_name: "Costco - Eastvale",
is_default: true,
},
+ {
+ id: 13,
+ household_store_id: 100,
+ name: "Costco",
+ location_name: "Fontana",
+ display_name: "Costco - Fontana",
+ is_default: false,
+ },
{
id: 11,
household_store_id: 200,
@@ -104,14 +112,18 @@ test("grocery store selector shows one selected store and picks from a modal", a
await page.goto("/");
- const selector = page.locator(".store-selector-trigger");
- await expect(selector).toBeVisible();
- await expect(selector).toContainText("Costco");
- await expect(page.locator(".store-tabs")).not.toContainText("Eastvale");
+ const storeSelector = page.locator(".store-selector-trigger").first();
+ const locationSelector = page.locator(".location-selector-trigger");
+ await expect(storeSelector).toBeVisible();
+ await expect(storeSelector).toContainText("Costco");
+ await expect(locationSelector).toBeVisible();
+ await expect(locationSelector).toContainText("Eastvale");
+ await expect(page.getByRole("button", { name: "Manage Map" })).toBeVisible();
+ await expect(page.locator(".store-tabs")).not.toContainText("Costco - Eastvale");
await expect(page.locator(".store-tabs")).not.toContainText("99 Ranch");
await expect(page.locator(".store-tabs")).not.toContainText("Stater Bros");
- await selector.click();
+ await storeSelector.click();
const modal = page.getByRole("dialog", { name: "Select store" });
await expect(modal).toBeVisible();
await expect(modal.getByRole("button", { name: "Costco" })).toBeVisible();
@@ -122,13 +134,34 @@ test("grocery store selector shows one selected store and picks from a modal", a
await modal.getByRole("button", { name: "99 Ranch" }).click();
await expect(modal).toHaveCount(0);
- await expect(selector).toContainText("99 Ranch");
+ await expect(storeSelector).toContainText("99 Ranch");
+ await expect(locationSelector).toContainText("Eastvale");
await expect.poll(() => page.evaluate(() => localStorage.getItem("activeHouseholdStoreId_1"))).toBe("200");
+ await expect.poll(() => page.evaluate(() => localStorage.getItem("activeStoreLocationId_1_200"))).toBe("11");
- await selector.click();
+ await storeSelector.click();
await expect(modal).toBeVisible();
await page.locator(".store-selector-modal-overlay").click({ position: { x: 4, y: 4 } });
await expect(modal).toHaveCount(0);
+
+ await storeSelector.click();
+ await modal.getByRole("button", { name: "Costco" }).click();
+ await locationSelector.click();
+ const locationModal = page.getByRole("dialog", { name: "Select location" });
+ await expect(locationModal).toBeVisible();
+ await expect(locationModal.getByRole("button", { name: "Eastvale" })).toBeVisible();
+ await expect(locationModal.getByRole("button", { name: "Fontana" })).toBeVisible();
+ await expect(locationModal).not.toContainText("Costco - Eastvale");
+
+ await locationModal.getByRole("button", { name: "Fontana" }).click();
+ await expect(locationModal).toHaveCount(0);
+ await expect(storeSelector).toContainText("Costco");
+ await expect(locationSelector).toContainText("Fontana");
+ await expect.poll(() => page.evaluate(() => localStorage.getItem("activeHouseholdStoreId_1"))).toBe("100");
+ await expect.poll(() => page.evaluate(() => localStorage.getItem("activeStoreLocationId_1_100"))).toBe("13");
+
+ await page.getByRole("button", { name: "Manage Map" }).click();
+ await expect(page).toHaveURL(/\/manage\?tab=stores&locationId=13$/);
});
test("single grocery store selector click does not open picker modal", async ({ page }) => {
@@ -145,9 +178,14 @@ test("single grocery store selector click does not open picker modal", async ({
await page.goto("/");
- const selector = page.locator(".store-selector-trigger");
- await expect(selector).toBeVisible();
- await expect(selector).toContainText("Costco");
- await selector.click();
+ const storeSelector = page.locator(".store-selector-trigger").first();
+ const locationSelector = page.locator(".location-selector-trigger");
+ await expect(storeSelector).toBeVisible();
+ await expect(storeSelector).toContainText("Costco");
+ await expect(locationSelector).toBeVisible();
+ await expect(locationSelector).toContainText("Default");
+ await storeSelector.click();
await expect(page.getByRole("dialog", { name: "Select store" })).toHaveCount(0);
+ await locationSelector.click();
+ await expect(page.getByRole("dialog", { name: "Select location" })).toHaveCount(0);
});