Merge pull request 'Flatten store location cards' (#14) from feature/flatten-store-location-cards into feature-custom-store-locations

This commit is contained in:
nalalangan 2026-05-31 18:58:19 -09:00
commit 8387e22b4f
3 changed files with 96 additions and 52 deletions

View File

@ -87,8 +87,8 @@ export default function ManageStores() {
<section className="manage-section">
<h2>Store Locations ({householdStores.length})</h2>
<p className="manage-stores-help">
Stores and locations are private to this household. Each location has its own zones,
item defaults, and shopping order.
Stores are private to this household. Locations define map-specific zones, item
placement, and shopping order.
</p>
{householdStores.length === 0 ? (
<p className="empty-message">No store locations added yet.</p>
@ -96,45 +96,55 @@ export default function ManageStores() {
<div className="stores-list">
{groupedStores.map((storeGroup) => (
<div key={storeGroup.household_store_id} className="store-card">
<div className="store-info">
<h3>{storeGroup.name}</h3>
<div className="store-card-header">
<div className="store-info">
<h3>{storeGroup.name}</h3>
</div>
<StoreLocationManager
householdId={activeHousehold.id}
storeGroup={storeGroup}
allLocationCount={householdStores.length}
canManage={isAdmin}
refreshAfterStoreChange={refreshAfterStoreChange}
/>
</div>
<StoreLocationManager
householdId={activeHousehold.id}
storeGroup={storeGroup}
allLocationCount={householdStores.length}
canManage={isAdmin}
refreshAfterStoreChange={refreshAfterStoreChange}
/>
<div className="store-location-list">
{storeGroup.locations.map((location) => (
<div key={location.id} className="store-location-row">
<div className="store-info">
<strong>{locationLabel(location)}</strong>
{location.address ? (
<p className="store-location">{location.address}</p>
) : null}
{location.is_default ? (
<p className="store-location">Default shopping location</p>
) : null}
{storeGroup.locations.map((location) => {
const label = locationLabel(location);
const showLocationName =
storeGroup.locations.length > 1 || label !== storeGroup.name;
return (
<div key={location.id} className="store-location-row">
<div className="store-info store-location-copy">
{showLocationName ? <strong>{label}</strong> : null}
{location.address ? (
<p className="store-location">{location.address}</p>
) : null}
{location.is_default ? (
<p className="store-location">Default location</p>
) : null}
</div>
<div className="store-location-controls">
<StoreZoneManager
householdId={activeHousehold.id}
location={location}
canManage={isAdmin}
refreshActiveZones={refreshZones}
/>
<StoreAvailableItemsManager
householdId={activeHousehold.id}
store={location}
isAdmin={isAdmin}
/>
</div>
</div>
<StoreZoneManager
householdId={activeHousehold.id}
location={location}
canManage={isAdmin}
refreshActiveZones={refreshZones}
/>
<StoreAvailableItemsManager
householdId={activeHousehold.id}
store={location}
isAdmin={isAdmin}
/>
</div>
))}
);
})}
</div>
</div>
))}

View File

@ -50,11 +50,11 @@
background: var(--background);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.25rem;
padding: 1rem;
transition: all 0.2s;
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0.75rem;
}
.store-card:hover {
@ -62,16 +62,23 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.store-card-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
}
.store-info h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.5rem 0;
margin: 0;
}
.store-location {
color: var(--text-secondary);
font-size: 0.9rem;
font-size: 0.85rem;
margin: 0;
}
@ -90,17 +97,37 @@
.store-location-list {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0.65rem;
}
.store-location-row {
display: flex;
flex-direction: column;
gap: 0.55rem;
padding-top: 0.65rem;
border-top: 1px solid var(--border);
}
.store-location-row:first-child {
padding-top: 0;
border-top: 0;
}
.store-location-copy {
display: flex;
flex-direction: column;
gap: 0.12rem;
}
.store-location-copy strong {
color: var(--text-primary);
font-size: 0.94rem;
}
.store-location-controls {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--card-bg);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.store-location-manager-trigger,
@ -108,9 +135,8 @@
width: 100%;
}
.store-location-row > .store-zone-manager-trigger,
.store-location-row > .store-available-items-trigger {
grid-column: 1 / -1;
.store-card-header > .store-location-manager-trigger {
width: auto;
}
.store-items-modal-toolbar.store-management-create-row {
@ -283,13 +309,18 @@
width: 100%;
}
.store-location-row,
.store-card-header,
.store-location-controls,
.store-items-modal-toolbar.store-management-create-row,
.store-items-modal-toolbar.store-location-create-row,
.store-management-row {
grid-template-columns: 1fr;
}
.store-card-header > .store-location-manager-trigger {
width: 100%;
}
.store-management-order {
text-align: left;
}

View File

@ -107,6 +107,9 @@ test("manage stores opens a modal to edit and delete household store items", asy
const storeCard = page.locator(".store-card").filter({ hasText: "Costco" });
await expect(storeCard).toBeVisible();
await expect(storeCard.getByText("Costco", { exact: true })).toHaveCount(1);
await expect(storeCard.getByText("Default location")).toBeVisible();
await expect(storeCard.getByText("Default shopping location")).toHaveCount(0);
await expect(storeCard.getByRole("button", { name: "Manage Items" })).toBeVisible();
await storeCard.getByRole("button", { name: "Manage Items" }).click();