Add store location map manager #20

Open
nalalangan wants to merge 206 commits from feature/location-map-manager into feature/store-selector-modal
3 changed files with 102 additions and 63 deletions
Showing only changes of commit b96cb65d6f - Show all commits

View File

@ -108,6 +108,20 @@ export function LocationMapOverview({ ariaLabel, rows }) {
); );
} }
function InlineStateAction({ label, value, actionLabel, actionText = "Show", onAction }) {
return (
<div className="location-map-inline-state">
<span>{label}</span>
<strong>{value}</strong>
{onAction ? (
<button type="button" onClick={onAction} aria-label={actionLabel}>
{actionText}
</button>
) : null}
</div>
);
}
export function SelectedMapObjectForm({ export function SelectedMapObjectForm({
selectedObject, selectedObject,
zones = [], zones = [],
@ -180,16 +194,16 @@ export function SelectedZoneItemsPanel({
return ( return (
<div className="location-map-zone-items"> <div className="location-map-zone-items">
<div className="location-map-zone-empty"> <div className="location-map-zone-empty">
<p className="location-map-muted">
{hasHiddenSelectedItems
? "Items are assigned here, but hidden by layer filters."
: "No items assigned to this zone yet."}
</p>
{hasHiddenSelectedItems ? ( {hasHiddenSelectedItems ? (
<button type="button" onClick={onShowSelectedZoneItems}> <InlineStateAction
Show Zone Items label="Hidden"
</button> value={hiddenSelectedItemCount}
) : null} actionLabel="Show Zone Items"
onAction={onShowSelectedZoneItems}
/>
) : (
<InlineStateAction label="Items" value="0" />
)}
</div> </div>
</div> </div>
); );
@ -204,12 +218,13 @@ export function SelectedZoneItemsPanel({
/> />
{hasHiddenSelectedItems ? ( {hasHiddenSelectedItems ? (
<div className="location-map-zone-hidden-note"> <div className="location-map-zone-hidden-note">
<p className="location-map-muted"> <InlineStateAction
{hiddenSelectedItemCount} more hidden by layer filters. label="Hidden"
</p> value={hiddenSelectedItemCount}
<button type="button" onClick={onShowSelectedZoneItems}> actionLabel="Show All Zone Items"
Show All Zone Items actionText="Show All"
</button> onAction={onShowSelectedZoneItems}
/>
</div> </div>
) : null} ) : null}
</div> </div>
@ -228,12 +243,12 @@ export function UnmappedItemsPanel({
<div className="location-map-unmapped-list"> <div className="location-map-unmapped-list">
<strong>Unmapped Items</strong> <strong>Unmapped Items</strong>
<div className="location-map-zone-empty"> <div className="location-map-zone-empty">
<p className="location-map-muted"> <InlineStateAction
Unmapped items are hidden by layer filters. label="Hidden"
</p> value={hiddenUnmappedItemCount}
<button type="button" onClick={onShowUnmappedItems}> actionLabel="Show Unmapped Items"
Show Unmapped Items onAction={onShowUnmappedItems}
</button> />
</div> </div>
</div> </div>
); );
@ -253,12 +268,13 @@ export function UnmappedItemsPanel({
/> />
{hiddenUnmappedItemCount > 0 ? ( {hiddenUnmappedItemCount > 0 ? (
<div className="location-map-zone-hidden-note"> <div className="location-map-zone-hidden-note">
<p className="location-map-muted"> <InlineStateAction
{hiddenUnmappedItemCount} more hidden by layer filters. label="Hidden"
</p> value={hiddenUnmappedItemCount}
<button type="button" onClick={onShowUnmappedItems}> actionLabel="Show All Unmapped Items"
Show All Unmapped Items actionText="Show All"
</button> onAction={onShowUnmappedItems}
/>
</div> </div>
) : null} ) : null}
</div> </div>

View File

@ -717,37 +717,45 @@
.location-map-zone-empty { .location-map-zone-empty {
display: grid; display: grid;
gap: 0.55rem; gap: 0.35rem;
}
.location-map-zone-empty button {
min-height: 40px;
width: fit-content;
padding: 0.45rem 0.7rem;
border: 1px solid var(--color-border-light);
border-radius: 8px;
background: #15283b;
color: #f8fafc;
font-weight: 800;
cursor: pointer;
} }
.location-map-zone-hidden-note { .location-map-zone-hidden-note {
display: grid;
gap: 0.45rem;
margin-top: 0.55rem; margin-top: 0.55rem;
} }
.location-map-zone-hidden-note p { .location-map-inline-state {
margin: 0; min-height: 42px;
} display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
.location-map-zone-hidden-note button { align-items: center;
min-height: 40px; gap: 0.5rem;
width: fit-content; padding: 0.25rem 0.35rem 0.25rem 0.55rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--color-border-light); border: 1px solid var(--color-border-light);
border-radius: 8px; border-radius: 8px;
background: rgba(15, 23, 34, 0.56);
color: #f8fafc;
}
.location-map-inline-state span {
min-width: 0;
color: #9fb3c8;
font-size: 0.82rem;
font-weight: 800;
}
.location-map-inline-state strong {
color: #f8fafc;
font-weight: 900;
white-space: nowrap;
}
.location-map-inline-state button {
min-height: 34px;
min-width: 64px;
padding: 0.35rem 0.6rem;
border: 1px solid var(--color-border-light);
border-radius: 999px;
background: #15283b; background: #15283b;
color: #f8fafc; color: #f8fafc;
font-weight: 800; font-weight: 800;

View File

@ -2914,7 +2914,10 @@ test("viewer can buy selected zone items from the map", async ({ page }) => {
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0); await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown"); await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown");
await expect(bakeryArea.locator(".location-map-count")).toHaveText("0 shown"); await expect(bakeryArea.locator(".location-map-count")).toHaveText("0 shown");
await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toBeVisible(); const hiddenZoneState = page.locator(".location-map-zone-items .location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenZoneState).toContainText("2");
await expect(hiddenZoneState.getByRole("button", { name: "Show Zone Items" })).toHaveText("Show");
await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Layers" }).click(); await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Bought" }).click(); await page.getByRole("button", { name: "Bought" }).click();
@ -3196,7 +3199,7 @@ test("viewer shows completed zone items without rebuy actions", async ({ page })
await expect(newlyBoughtRow.locator("small")).toHaveText("Bought"); await expect(newlyBoughtRow.locator("small")).toHaveText("Bought");
}); });
test("viewer explains when unmapped items are hidden by filters", async ({ page }) => { test("viewer shows compact state when unmapped items are hidden by filters", async ({ page }) => {
await mockMapShell(page); await mockMapShell(page);
const mapState = { const mapState = {
@ -3228,13 +3231,15 @@ test("viewer explains when unmapped items are hidden by filters", async ({ page
await page.getByRole("button", { name: "Unmapped" }).click(); await page.getByRole("button", { name: "Unmapped" }).click();
await expect(page.getByText("other batteries")).toHaveCount(0); await expect(page.getByText("other batteries")).toHaveCount(0);
await expect(page.getByText("Unmapped items are hidden by layer filters.")).toBeVisible(); const hiddenUnmappedState = page.locator(".location-map-unmapped-list .location-map-inline-state", { hasText: "Hidden" });
await expect(page.getByRole("button", { name: "Show Unmapped Items" })).toBeVisible(); await expect(hiddenUnmappedState).toContainText("1");
await expect(hiddenUnmappedState.getByRole("button", { name: "Show Unmapped Items" })).toHaveText("Show");
await expect(page.getByText("Unmapped items are hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Show Unmapped Items" }).click(); await page.getByRole("button", { name: "Show Unmapped Items" }).click();
await expect(page.getByText("other batteries")).toBeVisible(); await expect(page.getByText("other batteries")).toBeVisible();
await expect(page.getByText("Unmapped items are hidden by layer filters.")).toHaveCount(0); await expect(hiddenUnmappedState).toHaveCount(0);
await expect(page.getByRole("button", { name: "Layers, 3 changed" })).toBeVisible(); await expect(page.getByRole("button", { name: "Layers, 3 changed" })).toBeVisible();
}); });
@ -3280,16 +3285,18 @@ test("viewer marks partial unmapped counts when filters hide some items", async
await expect(page.getByText("visible batteries")).toBeVisible(); await expect(page.getByText("visible batteries")).toBeVisible();
await expect(page.getByText("hidden batteries")).toHaveCount(0); await expect(page.getByText("hidden batteries")).toHaveCount(0);
await expect(page.getByText("1 more hidden by layer filters.")).toBeVisible(); const hiddenUnmappedState = page.locator(".location-map-unmapped-list .location-map-inline-state", { hasText: "Hidden" });
await expect(page.getByRole("button", { name: "Show All Unmapped Items" })).toBeVisible(); await expect(hiddenUnmappedState).toContainText("1");
await expect(hiddenUnmappedState.getByRole("button", { name: "Show All Unmapped Items" })).toHaveText("Show All");
await expect(page.getByText("1 more hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Show All Unmapped Items" }).click(); await page.getByRole("button", { name: "Show All Unmapped Items" }).click();
await expect(page.getByText("hidden batteries")).toBeVisible(); await expect(page.getByText("hidden batteries")).toBeVisible();
await expect(page.getByText("1 more hidden by layer filters.")).toHaveCount(0); await expect(hiddenUnmappedState).toHaveCount(0);
}); });
test("viewer explains when selected zone items are hidden by filters", async ({ page }) => { test("viewer shows compact state when selected zone items are hidden by filters", async ({ page }) => {
await mockMapShell(page); await mockMapShell(page);
const mapState = publishedMapState([ const mapState = publishedMapState([
@ -3327,7 +3334,10 @@ test("viewer explains when selected zone items are hidden by filters", async ({
await frozenArea.locator("rect").first().click(); await frozenArea.locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown"); await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown");
await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toBeVisible(); const hiddenZoneState = page.locator(".location-map-zone-items .location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenZoneState).toContainText("1");
await expect(hiddenZoneState.getByRole("button", { name: "Show Zone Items" })).toHaveText("Show");
await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toHaveCount(0);
await expect(page.getByText("frozen salmon")).toHaveCount(0); await expect(page.getByText("frozen salmon")).toHaveCount(0);
await page.getByRole("button", { name: "Show Zone Items" }).click(); await page.getByRole("button", { name: "Show Zone Items" }).click();
@ -3398,8 +3408,10 @@ test("viewer marks partial zone counts when filters hide some items", async ({ p
await expect(page.locator(".location-map-sheet-count")).toHaveText("1 shown"); await expect(page.locator(".location-map-sheet-count")).toHaveText("1 shown");
await expect(page.getByText("visible bread")).toBeVisible(); await expect(page.getByText("visible bread")).toBeVisible();
await expect(page.getByText("hidden baguette")).toHaveCount(0); await expect(page.getByText("hidden baguette")).toHaveCount(0);
await expect(page.getByText("1 more hidden by layer filters.")).toBeVisible(); const hiddenZoneState = page.locator(".location-map-zone-items .location-map-inline-state", { hasText: "Hidden" });
await expect(page.getByRole("button", { name: "Show All Zone Items" })).toBeVisible(); await expect(hiddenZoneState).toContainText("1");
await expect(hiddenZoneState.getByRole("button", { name: "Show All Zone Items" })).toHaveText("Show All");
await expect(page.getByText("1 more hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Show All Zone Items" }).click(); await page.getByRole("button", { name: "Show All Zone Items" }).click();
@ -3472,7 +3484,10 @@ test("viewer handles empty and many-item zone panels without default pins", asyn
await page.locator('[data-object-key="1562"]').locator("rect").first().click(); await page.locator('[data-object-key="1562"]').locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("0 items"); await expect(page.locator(".location-map-sheet-count")).toHaveText("0 items");
await expect(page.getByText("No items assigned to this zone yet.")).toBeVisible(); const emptyZoneState = page.locator(".location-map-zone-items .location-map-inline-state", { hasText: "Items" });
await expect(emptyZoneState).toContainText("0");
await expect(emptyZoneState.getByRole("button")).toHaveCount(0);
await expect(page.getByText("No items assigned to this zone yet.")).toHaveCount(0);
await page.locator('[data-object-key="1561"]').locator("rect").first().click(); await page.locator('[data-object-key="1561"]').locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("14 items"); await expect(page.locator(".location-map-sheet-count")).toHaveText("14 items");