382 lines
13 KiB
JavaScript
382 lines
13 KiB
JavaScript
import { useState } from "react";
|
|
import {
|
|
addLocationToStore,
|
|
removeLocation,
|
|
setDefaultLocation,
|
|
updateLocation,
|
|
} from "../../api/stores";
|
|
import useActionToast from "../../hooks/useActionToast";
|
|
import getApiErrorMessage from "../../lib/getApiErrorMessage";
|
|
import ConfirmSlideModal from "../modals/ConfirmSlideModal";
|
|
|
|
function locationLabel(location) {
|
|
return location.display_name || location.name;
|
|
}
|
|
|
|
function locationEditName(location) {
|
|
return location.location_name || location.name || "";
|
|
}
|
|
|
|
function LocationSettingsModal({
|
|
location,
|
|
draft,
|
|
setDraft,
|
|
canManage,
|
|
onCancel,
|
|
onSave,
|
|
onSetDefault,
|
|
}) {
|
|
if (!location) return null;
|
|
|
|
return (
|
|
<div className="store-items-modal-overlay" onClick={onCancel}>
|
|
<div className="store-items-modal store-settings-modal" onClick={(event) => event.stopPropagation()}>
|
|
<div className="store-items-modal-header">
|
|
<div>
|
|
<h3>{locationLabel(location)} Settings</h3>
|
|
<p>Update this location name, notes, or default status.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="store-items-modal-close"
|
|
onClick={onCancel}
|
|
aria-label="Close location settings"
|
|
>
|
|
x
|
|
</button>
|
|
</div>
|
|
|
|
<div className="store-settings-form">
|
|
<label>
|
|
<span>Location name</span>
|
|
<input
|
|
value={draft.name}
|
|
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
|
|
disabled={!canManage}
|
|
/>
|
|
</label>
|
|
<label>
|
|
<span>Address or notes</span>
|
|
<input
|
|
value={draft.address}
|
|
onChange={(event) => setDraft((current) => ({ ...current, address: event.target.value }))}
|
|
disabled={!canManage}
|
|
/>
|
|
</label>
|
|
|
|
{canManage ? (
|
|
<div className="store-settings-actions">
|
|
{!location.is_default ? (
|
|
<button type="button" className="btn-secondary btn-small" onClick={onSetDefault}>
|
|
Set Default
|
|
</button>
|
|
) : null}
|
|
<button type="button" className="btn-primary btn-small" onClick={onSave}>
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function StoreLocationManager({
|
|
householdId,
|
|
storeGroup,
|
|
allLocationCount,
|
|
canManage,
|
|
refreshAfterStoreChange,
|
|
}) {
|
|
const toast = useActionToast();
|
|
const locationCount = storeGroup.locations.length;
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [locationDraft, setLocationDraft] = useState({ name: "", address: "" });
|
|
const [deleteMode, setDeleteMode] = useState(false);
|
|
const [selectedDeleteIds, setSelectedDeleteIds] = useState(() => new Set());
|
|
const [pendingDeleteLocations, setPendingDeleteLocations] = useState([]);
|
|
const [editingLocation, setEditingLocation] = useState(null);
|
|
const [editingLocationDraft, setEditingLocationDraft] = useState({ name: "", address: "" });
|
|
|
|
const selectedDeleteLocations = storeGroup.locations.filter((location) =>
|
|
selectedDeleteIds.has(location.id)
|
|
);
|
|
const selectedDeleteCount = selectedDeleteLocations.length;
|
|
const canConfirmDelete = selectedDeleteCount > 0 && allLocationCount - selectedDeleteCount >= 1;
|
|
|
|
const closeManager = () => {
|
|
setIsOpen(false);
|
|
setDeleteMode(false);
|
|
setSelectedDeleteIds(new Set());
|
|
setPendingDeleteLocations([]);
|
|
setEditingLocation(null);
|
|
};
|
|
|
|
const handleAddLocation = async () => {
|
|
const name = locationDraft.name.trim();
|
|
if (!name) return;
|
|
|
|
try {
|
|
await addLocationToStore(householdId, storeGroup.household_store_id, {
|
|
name,
|
|
address: locationDraft.address.trim() || null,
|
|
});
|
|
setLocationDraft({ name: "", address: "" });
|
|
await refreshAfterStoreChange();
|
|
toast.success("Added location", `Added ${name} to ${storeGroup.name}`);
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(error, "Failed to add location");
|
|
toast.error("Add location failed", `Add location failed: ${message}`);
|
|
}
|
|
};
|
|
|
|
const openLocationSettings = (location) => {
|
|
setEditingLocation(location);
|
|
setEditingLocationDraft({
|
|
name: locationEditName(location),
|
|
address: location.address || "",
|
|
});
|
|
};
|
|
|
|
const handleSaveLocation = async () => {
|
|
if (!editingLocation) return;
|
|
|
|
const name = editingLocationDraft.name.trim();
|
|
if (!name) return;
|
|
|
|
try {
|
|
await updateLocation(householdId, editingLocation.id, {
|
|
name,
|
|
address: editingLocationDraft.address.trim() || null,
|
|
});
|
|
await refreshAfterStoreChange();
|
|
setEditingLocation(null);
|
|
toast.success("Updated location", `Updated ${name}`);
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(error, "Failed to update location");
|
|
toast.error("Update location failed", `Update location failed: ${message}`);
|
|
}
|
|
};
|
|
|
|
const handleSetDefault = async () => {
|
|
if (!editingLocation) return;
|
|
|
|
try {
|
|
await setDefaultLocation(householdId, editingLocation.id);
|
|
await refreshAfterStoreChange();
|
|
setEditingLocation(null);
|
|
toast.success("Updated default location", `Default location set to ${locationLabel(editingLocation)}`);
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(error, "Failed to set default location");
|
|
toast.error("Set default failed", `Set default failed: ${message}`);
|
|
}
|
|
};
|
|
|
|
const toggleLocationSelection = (locationId) => {
|
|
setSelectedDeleteIds((currentIds) => {
|
|
const nextIds = new Set(currentIds);
|
|
if (nextIds.has(locationId)) {
|
|
nextIds.delete(locationId);
|
|
} else {
|
|
nextIds.add(locationId);
|
|
}
|
|
return nextIds;
|
|
});
|
|
};
|
|
|
|
const startDeleteMode = () => {
|
|
setDeleteMode(true);
|
|
setSelectedDeleteIds(new Set());
|
|
};
|
|
|
|
const cancelDeleteMode = () => {
|
|
setDeleteMode(false);
|
|
setSelectedDeleteIds(new Set());
|
|
};
|
|
|
|
const confirmSelectedDelete = () => {
|
|
if (!canConfirmDelete) return;
|
|
setPendingDeleteLocations(selectedDeleteLocations);
|
|
};
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
if (pendingDeleteLocations.length === 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await Promise.all(
|
|
pendingDeleteLocations.map((location) => removeLocation(householdId, location.id))
|
|
);
|
|
const count = pendingDeleteLocations.length;
|
|
await refreshAfterStoreChange();
|
|
setPendingDeleteLocations([]);
|
|
setDeleteMode(false);
|
|
setSelectedDeleteIds(new Set());
|
|
toast.success(
|
|
count === 1 ? "Removed location" : "Removed locations",
|
|
`Removed ${count} ${count === 1 ? "location" : "locations"} from ${storeGroup.name}`
|
|
);
|
|
} catch (error) {
|
|
const message = getApiErrorMessage(error, "Failed to remove locations");
|
|
toast.error("Remove locations failed", `Remove locations failed: ${message}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="btn-secondary btn-small store-location-manager-trigger"
|
|
onClick={() => setIsOpen(true)}
|
|
>
|
|
Manage Locations ({locationCount})
|
|
</button>
|
|
|
|
{isOpen ? (
|
|
<div className="store-items-modal-overlay" onClick={closeManager}>
|
|
<div className="store-items-modal" onClick={(event) => event.stopPropagation()}>
|
|
<div className="store-items-modal-header">
|
|
<div>
|
|
<h3>{storeGroup.name} Locations</h3>
|
|
<p>Manage locations, defaults, and location notes for this store.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="store-items-modal-close"
|
|
onClick={closeManager}
|
|
aria-label="Close manage locations modal"
|
|
>
|
|
x
|
|
</button>
|
|
</div>
|
|
|
|
{canManage ? (
|
|
<div className="store-items-modal-toolbar store-management-create-row store-location-create-row">
|
|
<input
|
|
value={locationDraft.name}
|
|
onChange={(event) =>
|
|
setLocationDraft((current) => ({ ...current, name: event.target.value }))
|
|
}
|
|
placeholder="Location name"
|
|
/>
|
|
<input
|
|
value={locationDraft.address}
|
|
onChange={(event) =>
|
|
setLocationDraft((current) => ({ ...current, address: event.target.value }))
|
|
}
|
|
placeholder="Address or notes"
|
|
/>
|
|
<button type="button" className="btn-primary btn-small" onClick={handleAddLocation}>
|
|
Add Location
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
{canManage && storeGroup.locations.length > 0 ? (
|
|
<div className="store-items-bulk-toolbar">
|
|
<button
|
|
type="button"
|
|
className="btn-danger btn-small store-items-delete-toggle"
|
|
disabled={deleteMode ? !canConfirmDelete : allLocationCount <= 1}
|
|
onClick={deleteMode ? confirmSelectedDelete : startDeleteMode}
|
|
title={
|
|
deleteMode && selectedDeleteCount > 0 && !canConfirmDelete
|
|
? "At least one household location must remain"
|
|
: ""
|
|
}
|
|
>
|
|
{deleteMode ? `Confirm Delete (${selectedDeleteCount})` : "Delete Locations"}
|
|
</button>
|
|
{deleteMode ? (
|
|
<button
|
|
type="button"
|
|
className="btn-secondary btn-small store-items-delete-cancel"
|
|
onClick={cancelDeleteMode}
|
|
>
|
|
Cancel
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="store-items-modal-body">
|
|
<div className="store-items-table">
|
|
<div className="store-items-table-body">
|
|
{storeGroup.locations.map((location) => {
|
|
const isSelectedForDelete = selectedDeleteIds.has(location.id);
|
|
|
|
return (
|
|
<button
|
|
key={location.id}
|
|
type="button"
|
|
className={`store-items-table-row store-items-table-row-button store-management-row ${deleteMode ? "is-delete-selectable" : ""} ${isSelectedForDelete ? "is-selected" : ""}`}
|
|
aria-label={
|
|
deleteMode
|
|
? `${isSelectedForDelete ? "Deselect" : "Select"} ${locationLabel(location)} for deletion`
|
|
: `Edit location ${locationLabel(location)}`
|
|
}
|
|
aria-pressed={deleteMode ? isSelectedForDelete : undefined}
|
|
onClick={() => {
|
|
if (deleteMode) {
|
|
toggleLocationSelection(location.id);
|
|
} else {
|
|
openLocationSettings(location);
|
|
}
|
|
}}
|
|
>
|
|
<span className="store-management-name">
|
|
{locationLabel(location)}
|
|
{location.is_default ? (
|
|
<span className="store-management-badge">Default</span>
|
|
) : null}
|
|
</span>
|
|
{location.address ? (
|
|
<span className="store-management-meta">{location.address}</span>
|
|
) : null}
|
|
{deleteMode ? (
|
|
<span className="store-items-delete-indicator" aria-hidden="true">
|
|
{isSelectedForDelete ? "\u2713" : ""}
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<LocationSettingsModal
|
|
location={editingLocation}
|
|
draft={editingLocationDraft}
|
|
setDraft={setEditingLocationDraft}
|
|
canManage={canManage}
|
|
onCancel={() => setEditingLocation(null)}
|
|
onSave={handleSaveLocation}
|
|
onSetDefault={handleSetDefault}
|
|
/>
|
|
|
|
<ConfirmSlideModal
|
|
isOpen={pendingDeleteLocations.length > 0}
|
|
title={
|
|
pendingDeleteLocations.length === 1
|
|
? `Delete ${locationLabel(pendingDeleteLocations[0])}?`
|
|
: `Delete ${pendingDeleteLocations.length} locations?`
|
|
}
|
|
description={
|
|
pendingDeleteLocations.length > 0
|
|
? `Slide to confirm. This removes ${pendingDeleteLocations.length === 1 ? locationLabel(pendingDeleteLocations[0]) : `${pendingDeleteLocations.length} locations`} from this household.`
|
|
: ""
|
|
}
|
|
confirmLabel={pendingDeleteLocations.length === 1 ? "Delete Location" : "Delete Locations"}
|
|
onClose={() => setPendingDeleteLocations([])}
|
|
onConfirm={handleDeleteConfirm}
|
|
/>
|
|
</>
|
|
);
|
|
}
|