191 lines
6.8 KiB
JavaScript
191 lines
6.8 KiB
JavaScript
import { useContext, useMemo, useState } from "react";
|
|
import { createHouseholdStore } from "../../api/stores";
|
|
import { HouseholdContext } from "../../context/HouseholdContext";
|
|
import { StoreContext } from "../../context/StoreContext";
|
|
import useActionToast from "../../hooks/useActionToast";
|
|
import getApiErrorMessage from "../../lib/getApiErrorMessage";
|
|
import StoreAvailableItemsManager from "./StoreAvailableItemsManager";
|
|
import StoreLocationManager from "./StoreLocationManager";
|
|
import StoreZoneManager from "./StoreZoneManager";
|
|
import "../../styles/components/manage/StoreAvailableItemsManager.css";
|
|
import "../../styles/components/manage/ManageStores.css";
|
|
|
|
function groupLocationsByStore(locations) {
|
|
const grouped = new Map();
|
|
|
|
for (const location of locations) {
|
|
const key = location.household_store_id;
|
|
if (!grouped.has(key)) {
|
|
grouped.set(key, {
|
|
household_store_id: location.household_store_id,
|
|
name: location.name,
|
|
locations: [],
|
|
});
|
|
}
|
|
|
|
grouped.get(key).locations.push(location);
|
|
}
|
|
|
|
return Array.from(grouped.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
function locationLabel(location) {
|
|
return location.display_name || location.name;
|
|
}
|
|
|
|
export default function ManageStores() {
|
|
const { activeHousehold } = useContext(HouseholdContext);
|
|
const {
|
|
activeStore,
|
|
stores: householdStores,
|
|
refreshStores,
|
|
refreshZones,
|
|
} = useContext(StoreContext);
|
|
const toast = useActionToast();
|
|
const [createForm, setCreateForm] = useState({
|
|
name: "",
|
|
location_name: "",
|
|
address: "",
|
|
});
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const isAdmin = ["owner", "admin"].includes(activeHousehold?.role);
|
|
const groupedStores = useMemo(
|
|
() => groupLocationsByStore(householdStores),
|
|
[householdStores]
|
|
);
|
|
|
|
const refreshAfterStoreChange = async () => {
|
|
await refreshStores();
|
|
await refreshZones();
|
|
};
|
|
|
|
const handleCreateStore = async (event) => {
|
|
event.preventDefault();
|
|
if (!createForm.name.trim()) return;
|
|
|
|
setSaving(true);
|
|
try {
|
|
await createHouseholdStore(activeHousehold.id, {
|
|
name: createForm.name.trim(),
|
|
location_name: createForm.location_name.trim() || "Default Location",
|
|
address: createForm.address.trim() || null,
|
|
});
|
|
setCreateForm({ name: "", location_name: "", address: "" });
|
|
await refreshAfterStoreChange();
|
|
toast.success("Created store", `Created store ${createForm.name.trim()}`);
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(error, "Failed to create store");
|
|
toast.error("Create store failed", `Create store failed: ${message}`);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="manage-stores">
|
|
<section className="manage-section">
|
|
<h2>Store Locations ({householdStores.length})</h2>
|
|
<p className="manage-stores-help">
|
|
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>
|
|
) : (
|
|
<div className="stores-list">
|
|
{groupedStores.map((storeGroup) => (
|
|
<div key={storeGroup.household_store_id} className="store-card">
|
|
<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>
|
|
|
|
<div className="store-location-list">
|
|
{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>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{isAdmin ? (
|
|
<section className="manage-section">
|
|
<h2>Add Store</h2>
|
|
<form className="add-store-panel" onSubmit={handleCreateStore}>
|
|
<input
|
|
value={createForm.name}
|
|
onChange={(event) => setCreateForm((current) => ({ ...current, name: event.target.value }))}
|
|
placeholder="Store name, e.g. Costco"
|
|
required
|
|
/>
|
|
<input
|
|
value={createForm.location_name}
|
|
onChange={(event) =>
|
|
setCreateForm((current) => ({ ...current, location_name: event.target.value }))
|
|
}
|
|
placeholder="Location name, e.g. Fontana"
|
|
/>
|
|
<input
|
|
value={createForm.address}
|
|
onChange={(event) => setCreateForm((current) => ({ ...current, address: event.target.value }))}
|
|
placeholder="Address or notes"
|
|
/>
|
|
<button type="submit" className="btn-primary" disabled={saving}>
|
|
{saving ? "Adding..." : "+ Add Store"}
|
|
</button>
|
|
</form>
|
|
</section>
|
|
) : activeStore ? (
|
|
<p className="manage-stores-note">
|
|
Household members can manage item defaults. Only owners and admins can manage stores,
|
|
locations, zones, and item deletion.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|