Add store location map manager #20
136
frontend/src/hooks/useLocationMapObjectActions.js
Normal file
136
frontend/src/hooks/useLocationMapObjectActions.js
Normal file
@ -0,0 +1,136 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
clampObjectToMap,
|
||||
createClientObject,
|
||||
getMapObjectDisplayLabel,
|
||||
getObjectKey,
|
||||
objectZoneId,
|
||||
} from "../lib/locationMapUtils";
|
||||
|
||||
const EMPTY_ZONES = [];
|
||||
|
||||
export default function useLocationMapObjectActions({
|
||||
canManage,
|
||||
hiddenAreaCount,
|
||||
mapSize,
|
||||
mapState,
|
||||
objects,
|
||||
selectedObject,
|
||||
selectedObjectKey,
|
||||
setEditorTool,
|
||||
setFuture,
|
||||
setHasUnsavedChanges,
|
||||
setHistory,
|
||||
setObjects,
|
||||
setSelectedObjectKey,
|
||||
toast,
|
||||
}) {
|
||||
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
||||
const zones = mapState?.zones || EMPTY_ZONES;
|
||||
|
||||
const remember = useCallback((currentObjects = objects) => {
|
||||
setHistory((previous) => [...previous.slice(-19), currentObjects.map((object) => ({ ...object }))]);
|
||||
setFuture([]);
|
||||
}, [objects, setFuture, setHistory]);
|
||||
|
||||
const updateObjects = useCallback((updater) => {
|
||||
setHasUnsavedChanges(true);
|
||||
setObjects((currentObjects) =>
|
||||
updater(currentObjects).map((object) => clampObjectToMap(object, mapSize))
|
||||
);
|
||||
}, [mapSize, setHasUnsavedChanges, setObjects]);
|
||||
|
||||
const handleAddObject = useCallback(() => {
|
||||
if (!canManage) return;
|
||||
remember();
|
||||
const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean));
|
||||
const nextZone = zones.find((zone) => !usedZoneIds.has(String(zone.id))) || zones[0];
|
||||
const nextObject = createClientObject(nextZone, objects.length);
|
||||
setObjects((currentObjects) => [...currentObjects, nextObject]);
|
||||
setSelectedObjectKey(getObjectKey(nextObject));
|
||||
setEditorTool("edit");
|
||||
setHasUnsavedChanges(true);
|
||||
}, [canManage, objects, remember, setEditorTool, setHasUnsavedChanges, setObjects, setSelectedObjectKey, zones]);
|
||||
|
||||
const handleDuplicateObject = useCallback(() => {
|
||||
if (!selectedObject) return;
|
||||
remember();
|
||||
const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area");
|
||||
const nextObject = {
|
||||
...selectedObject,
|
||||
id: undefined,
|
||||
client_id: `copy-${Date.now()}`,
|
||||
label: `${displayLabel} Copy`,
|
||||
x: selectedObject.x + 28,
|
||||
y: selectedObject.y + 28,
|
||||
sort_order: objects.length + 1,
|
||||
};
|
||||
setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]);
|
||||
setSelectedObjectKey(getObjectKey(nextObject));
|
||||
setEditorTool("edit");
|
||||
setHasUnsavedChanges(true);
|
||||
}, [mapSize, objects.length, remember, selectedObject, setEditorTool, setHasUnsavedChanges, setObjects, setSelectedObjectKey]);
|
||||
|
||||
const requestDeleteObject = useCallback(() => {
|
||||
if (selectedObject) setPendingDeleteObject(selectedObject);
|
||||
}, [selectedObject]);
|
||||
|
||||
const handleDeleteObject = useCallback(() => {
|
||||
if (!pendingDeleteObject) return;
|
||||
remember();
|
||||
setObjects((currentObjects) =>
|
||||
currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject))
|
||||
);
|
||||
setSelectedObjectKey(null);
|
||||
setHasUnsavedChanges(true);
|
||||
toast.success("Deleted map area", "Save the draft to keep this change.");
|
||||
setPendingDeleteObject(null);
|
||||
}, [pendingDeleteObject, remember, setHasUnsavedChanges, setObjects, setSelectedObjectKey, toast]);
|
||||
|
||||
const handleShowHiddenAreas = useCallback(() => {
|
||||
if (!canManage || hiddenAreaCount === 0) return;
|
||||
remember();
|
||||
updateObjects((currentObjects) =>
|
||||
currentObjects.map((object) => (
|
||||
object.visible === false ? { ...object, visible: true } : object
|
||||
))
|
||||
);
|
||||
toast.success("Showed hidden areas", "Save the draft to keep this change.");
|
||||
}, [canManage, hiddenAreaCount, remember, toast, updateObjects]);
|
||||
|
||||
const handleObjectField = useCallback((field, value) => {
|
||||
if (!selectedObject) return;
|
||||
remember();
|
||||
updateObjects((currentObjects) =>
|
||||
currentObjects.map((object) => (
|
||||
getObjectKey(object) === getObjectKey(selectedObject) ? { ...object, [field]: value } : object
|
||||
))
|
||||
);
|
||||
}, [remember, selectedObject, updateObjects]);
|
||||
|
||||
const handleZoneLinkChange = useCallback((zoneId) => {
|
||||
const zone = zones.find((candidate) => String(candidate.id) === String(zoneId));
|
||||
handleObjectField("zone_id", zone?.id || null);
|
||||
setObjects((currentObjects) =>
|
||||
currentObjects.map((object) => (
|
||||
getObjectKey(object) === selectedObjectKey
|
||||
? { ...object, zone_name: zone?.name || null, label: object.label ?? zone?.name ?? "" }
|
||||
: object
|
||||
))
|
||||
);
|
||||
}, [handleObjectField, selectedObjectKey, setObjects, zones]);
|
||||
|
||||
return {
|
||||
handleAddObject,
|
||||
handleDeleteObject,
|
||||
handleDuplicateObject,
|
||||
handleObjectField,
|
||||
handleShowHiddenAreas,
|
||||
handleZoneLinkChange,
|
||||
pendingDeleteObject,
|
||||
remember,
|
||||
requestDeleteObject,
|
||||
setPendingDeleteObject,
|
||||
updateObjects,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext } from "../context/AuthContext";
|
||||
import { HouseholdContext } from "../context/HouseholdContext";
|
||||
@ -12,19 +12,17 @@ import ConfirmSlideModal from "../components/modals/ConfirmSlideModal";
|
||||
import useActionToast from "../hooks/useActionToast";
|
||||
import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions";
|
||||
import useLocationMapDraftState from "../hooks/useLocationMapDraftState";
|
||||
import useLocationMapObjectActions from "../hooks/useLocationMapObjectActions";
|
||||
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
|
||||
import {
|
||||
DEFAULT_MAP_SIZE,
|
||||
clampMapZoom,
|
||||
clampObjectToMap,
|
||||
createClientObject,
|
||||
getMapObjectDisplayLabel,
|
||||
getMapStatus,
|
||||
getObjectKey,
|
||||
itemMatchesMapFilters,
|
||||
itemsForZone,
|
||||
mapForMode,
|
||||
objectZoneId,
|
||||
unmappedItems,
|
||||
} from "../lib/locationMapUtils";
|
||||
import "../styles/pages/LocationMapManager.css";
|
||||
@ -84,7 +82,6 @@ export default function LocationMapManager() {
|
||||
locationId,
|
||||
toast,
|
||||
});
|
||||
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
||||
const svgRef = useRef(null);
|
||||
|
||||
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
||||
@ -168,19 +165,6 @@ export default function LocationMapManager() {
|
||||
}
|
||||
}, [filters.showZones, objects, selectedObjectKey]);
|
||||
|
||||
const remember = (currentObjects = objects) => {
|
||||
setHistory((previous) => [...previous.slice(-19), currentObjects.map((object) => ({ ...object }))]);
|
||||
setFuture([]);
|
||||
};
|
||||
|
||||
const updateObjects = (updater) => {
|
||||
setHasUnsavedChanges(true);
|
||||
setObjects((currentObjects) => {
|
||||
const nextObjects = updater(currentObjects).map((object) => clampObjectToMap(object, mapSize));
|
||||
return nextObjects;
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
handleCreateBlank,
|
||||
handleCreateFromZones,
|
||||
@ -202,95 +186,34 @@ export default function LocationMapManager() {
|
||||
toast,
|
||||
});
|
||||
|
||||
const handleAddObject = () => {
|
||||
if (!canManage) return;
|
||||
remember();
|
||||
const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean));
|
||||
const nextZone = (mapState?.zones || []).find((zone) => !usedZoneIds.has(String(zone.id))) || mapState?.zones?.[0];
|
||||
const nextObject = createClientObject(nextZone, objects.length);
|
||||
setObjects((currentObjects) => [...currentObjects, nextObject]);
|
||||
setSelectedObjectKey(getObjectKey(nextObject));
|
||||
setEditorTool("edit");
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleDuplicateObject = () => {
|
||||
if (!selectedObject) return;
|
||||
remember();
|
||||
const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area");
|
||||
const nextObject = {
|
||||
...selectedObject,
|
||||
id: undefined,
|
||||
client_id: `copy-${Date.now()}`,
|
||||
label: `${displayLabel} Copy`,
|
||||
x: selectedObject.x + 28,
|
||||
y: selectedObject.y + 28,
|
||||
sort_order: objects.length + 1,
|
||||
};
|
||||
setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]);
|
||||
setSelectedObjectKey(getObjectKey(nextObject));
|
||||
setEditorTool("edit");
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const requestDeleteObject = () => {
|
||||
if (!selectedObject) return;
|
||||
setPendingDeleteObject(selectedObject);
|
||||
};
|
||||
|
||||
const handleDeleteObject = () => {
|
||||
if (!pendingDeleteObject) return;
|
||||
remember();
|
||||
setObjects((currentObjects) =>
|
||||
currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject))
|
||||
);
|
||||
setSelectedObjectKey(null);
|
||||
setHasUnsavedChanges(true);
|
||||
toast.success(
|
||||
"Deleted map area",
|
||||
"Save the draft to keep this change."
|
||||
);
|
||||
setPendingDeleteObject(null);
|
||||
};
|
||||
|
||||
const handleShowHiddenAreas = () => {
|
||||
if (!canManage || hiddenAreaCount === 0) return;
|
||||
remember();
|
||||
updateObjects((currentObjects) =>
|
||||
currentObjects.map((object) =>
|
||||
object.visible === false ? { ...object, visible: true } : object
|
||||
)
|
||||
);
|
||||
toast.success("Showed hidden areas", "Save the draft to keep this change.");
|
||||
};
|
||||
|
||||
const handleObjectField = (field, value) => {
|
||||
if (!selectedObject) return;
|
||||
remember();
|
||||
updateObjects((currentObjects) =>
|
||||
currentObjects.map((object) =>
|
||||
getObjectKey(object) === getObjectKey(selectedObject)
|
||||
? { ...object, [field]: value }
|
||||
: object
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleZoneLinkChange = (zoneId) => {
|
||||
const zone = (mapState?.zones || []).find((candidate) => String(candidate.id) === String(zoneId));
|
||||
handleObjectField("zone_id", zone?.id || null);
|
||||
setObjects((currentObjects) =>
|
||||
currentObjects.map((object) =>
|
||||
getObjectKey(object) === selectedObjectKey
|
||||
? {
|
||||
...object,
|
||||
zone_name: zone?.name || null,
|
||||
label: object.label ?? zone?.name ?? "",
|
||||
}
|
||||
: object
|
||||
)
|
||||
);
|
||||
};
|
||||
const {
|
||||
handleAddObject,
|
||||
handleDeleteObject,
|
||||
handleDuplicateObject,
|
||||
handleObjectField,
|
||||
handleShowHiddenAreas,
|
||||
handleZoneLinkChange,
|
||||
pendingDeleteObject,
|
||||
remember,
|
||||
requestDeleteObject,
|
||||
setPendingDeleteObject,
|
||||
updateObjects,
|
||||
} = useLocationMapObjectActions({
|
||||
canManage,
|
||||
hiddenAreaCount,
|
||||
mapSize,
|
||||
mapState,
|
||||
objects,
|
||||
selectedObject,
|
||||
selectedObjectKey,
|
||||
setEditorTool,
|
||||
setFuture,
|
||||
setHasUnsavedChanges,
|
||||
setHistory,
|
||||
setObjects,
|
||||
setSelectedObjectKey,
|
||||
toast,
|
||||
});
|
||||
|
||||
const handleUndo = () => {
|
||||
setHistory((previous) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user