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 { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { AuthContext } from "../context/AuthContext";
|
import { AuthContext } from "../context/AuthContext";
|
||||||
import { HouseholdContext } from "../context/HouseholdContext";
|
import { HouseholdContext } from "../context/HouseholdContext";
|
||||||
@ -12,19 +12,17 @@ import ConfirmSlideModal from "../components/modals/ConfirmSlideModal";
|
|||||||
import useActionToast from "../hooks/useActionToast";
|
import useActionToast from "../hooks/useActionToast";
|
||||||
import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions";
|
import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions";
|
||||||
import useLocationMapDraftState from "../hooks/useLocationMapDraftState";
|
import useLocationMapDraftState from "../hooks/useLocationMapDraftState";
|
||||||
|
import useLocationMapObjectActions from "../hooks/useLocationMapObjectActions";
|
||||||
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
|
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAP_SIZE,
|
DEFAULT_MAP_SIZE,
|
||||||
clampMapZoom,
|
clampMapZoom,
|
||||||
clampObjectToMap,
|
|
||||||
createClientObject,
|
|
||||||
getMapObjectDisplayLabel,
|
getMapObjectDisplayLabel,
|
||||||
getMapStatus,
|
getMapStatus,
|
||||||
getObjectKey,
|
getObjectKey,
|
||||||
itemMatchesMapFilters,
|
itemMatchesMapFilters,
|
||||||
itemsForZone,
|
itemsForZone,
|
||||||
mapForMode,
|
mapForMode,
|
||||||
objectZoneId,
|
|
||||||
unmappedItems,
|
unmappedItems,
|
||||||
} from "../lib/locationMapUtils";
|
} from "../lib/locationMapUtils";
|
||||||
import "../styles/pages/LocationMapManager.css";
|
import "../styles/pages/LocationMapManager.css";
|
||||||
@ -84,7 +82,6 @@ export default function LocationMapManager() {
|
|||||||
locationId,
|
locationId,
|
||||||
toast,
|
toast,
|
||||||
});
|
});
|
||||||
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
|
|
||||||
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
||||||
@ -168,19 +165,6 @@ export default function LocationMapManager() {
|
|||||||
}
|
}
|
||||||
}, [filters.showZones, objects, selectedObjectKey]);
|
}, [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 {
|
const {
|
||||||
handleCreateBlank,
|
handleCreateBlank,
|
||||||
handleCreateFromZones,
|
handleCreateFromZones,
|
||||||
@ -202,95 +186,34 @@ export default function LocationMapManager() {
|
|||||||
toast,
|
toast,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddObject = () => {
|
const {
|
||||||
if (!canManage) return;
|
handleAddObject,
|
||||||
remember();
|
handleDeleteObject,
|
||||||
const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean));
|
handleDuplicateObject,
|
||||||
const nextZone = (mapState?.zones || []).find((zone) => !usedZoneIds.has(String(zone.id))) || mapState?.zones?.[0];
|
handleObjectField,
|
||||||
const nextObject = createClientObject(nextZone, objects.length);
|
handleShowHiddenAreas,
|
||||||
setObjects((currentObjects) => [...currentObjects, nextObject]);
|
handleZoneLinkChange,
|
||||||
setSelectedObjectKey(getObjectKey(nextObject));
|
pendingDeleteObject,
|
||||||
setEditorTool("edit");
|
remember,
|
||||||
setHasUnsavedChanges(true);
|
requestDeleteObject,
|
||||||
};
|
setPendingDeleteObject,
|
||||||
|
updateObjects,
|
||||||
const handleDuplicateObject = () => {
|
} = useLocationMapObjectActions({
|
||||||
if (!selectedObject) return;
|
canManage,
|
||||||
remember();
|
hiddenAreaCount,
|
||||||
const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area");
|
mapSize,
|
||||||
const nextObject = {
|
mapState,
|
||||||
...selectedObject,
|
objects,
|
||||||
id: undefined,
|
selectedObject,
|
||||||
client_id: `copy-${Date.now()}`,
|
selectedObjectKey,
|
||||||
label: `${displayLabel} Copy`,
|
setEditorTool,
|
||||||
x: selectedObject.x + 28,
|
setFuture,
|
||||||
y: selectedObject.y + 28,
|
setHasUnsavedChanges,
|
||||||
sort_order: objects.length + 1,
|
setHistory,
|
||||||
};
|
setObjects,
|
||||||
setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]);
|
setSelectedObjectKey,
|
||||||
setSelectedObjectKey(getObjectKey(nextObject));
|
toast,
|
||||||
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 handleUndo = () => {
|
const handleUndo = () => {
|
||||||
setHistory((previous) => {
|
setHistory((previous) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user