|
|
|
|
@ -58,6 +58,7 @@ export default function LocationMapManager() {
|
|
|
|
|
const [selectedObjectKey, setSelectedObjectKey] = useState(null);
|
|
|
|
|
const [filters, setFilters] = useState(DEFAULT_MAP_FILTERS);
|
|
|
|
|
const [layersOpen, setLayersOpen] = useState(false);
|
|
|
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
|
|
|
const [zoom, setZoom] = useState(0.75);
|
|
|
|
|
const [history, setHistory] = useState([]);
|
|
|
|
|
const [future, setFuture] = useState([]);
|
|
|
|
|
@ -80,6 +81,23 @@ export default function LocationMapManager() {
|
|
|
|
|
height: mapDraft.height || activeMap?.height || DEFAULT_MAP_SIZE.height,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => {
|
|
|
|
|
const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft);
|
|
|
|
|
const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft);
|
|
|
|
|
if (!nextMap) return;
|
|
|
|
|
|
|
|
|
|
setMapDraft({
|
|
|
|
|
name: nextMap.name || "Store Map",
|
|
|
|
|
width: nextMap.width || DEFAULT_MAP_SIZE.width,
|
|
|
|
|
height: nextMap.height || DEFAULT_MAP_SIZE.height,
|
|
|
|
|
});
|
|
|
|
|
setObjects(nextObjects.map(normalizeMapObject));
|
|
|
|
|
setSelectedObjectKey(null);
|
|
|
|
|
setHistory([]);
|
|
|
|
|
setFuture([]);
|
|
|
|
|
setHasUnsavedChanges(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const loadMap = useCallback(async () => {
|
|
|
|
|
if (!activeHousehold?.id || !locationId) return;
|
|
|
|
|
|
|
|
|
|
@ -94,6 +112,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(false);
|
|
|
|
|
setObjects([]);
|
|
|
|
|
setHasUnsavedChanges(false);
|
|
|
|
|
setMapDraft({
|
|
|
|
|
name: "Store Map",
|
|
|
|
|
width: DEFAULT_MAP_SIZE.width,
|
|
|
|
|
@ -112,18 +131,10 @@ export default function LocationMapManager() {
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextMap = nextState.published_map || nextState.draft_map;
|
|
|
|
|
setMapDraft({
|
|
|
|
|
name: nextMap?.name || "Store Map",
|
|
|
|
|
width: nextMap?.width || DEFAULT_MAP_SIZE.width,
|
|
|
|
|
height: nextMap?.height || DEFAULT_MAP_SIZE.height,
|
|
|
|
|
});
|
|
|
|
|
setObjects(
|
|
|
|
|
objectsForMode(
|
|
|
|
|
nextState,
|
|
|
|
|
nextState.published_map ? "view" : "edit",
|
|
|
|
|
!nextState.published_map
|
|
|
|
|
).map(normalizeMapObject)
|
|
|
|
|
syncMapDraftFromState(
|
|
|
|
|
nextState,
|
|
|
|
|
nextState.published_map ? "view" : "edit",
|
|
|
|
|
!nextState.published_map
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = getApiErrorMessage(error, "Failed to load map");
|
|
|
|
|
@ -132,7 +143,7 @@ export default function LocationMapManager() {
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [activeHousehold?.id, locationId]);
|
|
|
|
|
}, [activeHousehold?.id, locationId, syncMapDraftFromState]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
toastRef.current = toast;
|
|
|
|
|
@ -151,23 +162,6 @@ export default function LocationMapManager() {
|
|
|
|
|
}
|
|
|
|
|
}, [activeStore?.id, locationId, setActiveStore, stores]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!mapState) return;
|
|
|
|
|
const nextMap = mapForMode(mapState, mode, previewDraft);
|
|
|
|
|
const nextObjects = objectsForMode(mapState, mode, previewDraft);
|
|
|
|
|
if (!nextMap) return;
|
|
|
|
|
|
|
|
|
|
setMapDraft({
|
|
|
|
|
name: nextMap.name || "Store Map",
|
|
|
|
|
width: nextMap.width || DEFAULT_MAP_SIZE.width,
|
|
|
|
|
height: nextMap.height || DEFAULT_MAP_SIZE.height,
|
|
|
|
|
});
|
|
|
|
|
setObjects(nextObjects.map(normalizeMapObject));
|
|
|
|
|
setSelectedObjectKey(null);
|
|
|
|
|
setHistory([]);
|
|
|
|
|
setFuture([]);
|
|
|
|
|
}, [mapState, mode, previewDraft]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode !== "edit") {
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
@ -185,6 +179,7 @@ export default function LocationMapManager() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateObjects = (updater) => {
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
setObjects((currentObjects) => {
|
|
|
|
|
const nextObjects = updater(currentObjects).map((object) => clampObjectToMap(object, mapSize));
|
|
|
|
|
return nextObjects;
|
|
|
|
|
@ -200,6 +195,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setMode("edit");
|
|
|
|
|
setEditorTool("edit");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
syncMapDraftFromState(response.data, "edit", true);
|
|
|
|
|
toast.success("Created map", "Blank draft map created");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map"));
|
|
|
|
|
@ -217,6 +213,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setMode("edit");
|
|
|
|
|
setEditorTool("edit");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
syncMapDraftFromState(response.data, "edit", true);
|
|
|
|
|
toast.success("Created starter map", "Zones were added as editable rectangles");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map from zones"));
|
|
|
|
|
@ -236,6 +233,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setMapState(response.data);
|
|
|
|
|
setMode("edit");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
syncMapDraftFromState(response.data, "edit", true);
|
|
|
|
|
toast.success("Saved draft", "Map draft saved");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft"));
|
|
|
|
|
@ -253,6 +251,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setMode("view");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(false);
|
|
|
|
|
syncMapDraftFromState(response.data, "view", false);
|
|
|
|
|
toast.success("Published map", "Map is now visible to household members");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map"));
|
|
|
|
|
@ -270,6 +269,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setObjects((currentObjects) => [...currentObjects, nextObject]);
|
|
|
|
|
setSelectedObjectKey(getObjectKey(nextObject));
|
|
|
|
|
setEditorTool("edit");
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDuplicateObject = () => {
|
|
|
|
|
@ -287,6 +287,7 @@ export default function LocationMapManager() {
|
|
|
|
|
setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]);
|
|
|
|
|
setSelectedObjectKey(getObjectKey(nextObject));
|
|
|
|
|
setEditorTool("edit");
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const requestDeleteObject = () => {
|
|
|
|
|
@ -301,6 +302,7 @@ export default function LocationMapManager() {
|
|
|
|
|
currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject))
|
|
|
|
|
);
|
|
|
|
|
setSelectedObjectKey(null);
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
toast.success(
|
|
|
|
|
"Deleted map area",
|
|
|
|
|
"Save the draft to keep this change."
|
|
|
|
|
@ -342,6 +344,7 @@ export default function LocationMapManager() {
|
|
|
|
|
const priorObjects = previous[previous.length - 1];
|
|
|
|
|
setFuture((nextFuture) => [objects.map((object) => ({ ...object })), ...nextFuture.slice(0, 19)]);
|
|
|
|
|
setObjects(priorObjects);
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
return previous.slice(0, -1);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
@ -352,10 +355,41 @@ export default function LocationMapManager() {
|
|
|
|
|
const nextObjects = previous[0];
|
|
|
|
|
setHistory((nextHistory) => [...nextHistory.slice(-19), objects.map((object) => ({ ...object }))]);
|
|
|
|
|
setObjects(nextObjects);
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
return previous.slice(1);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePreviewDraft = () => {
|
|
|
|
|
setMode("view");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
setSelectedObjectKey(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleViewMode = () => {
|
|
|
|
|
setMode("view");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
if (hasUnsavedChanges) {
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
setSelectedObjectKey(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setPreviewDraft(false);
|
|
|
|
|
if (mapState) {
|
|
|
|
|
syncMapDraftFromState(mapState, "view", false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditMode = () => {
|
|
|
|
|
setMode("edit");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
if (!previewDraft && mapState && !hasUnsavedChanges) {
|
|
|
|
|
syncMapDraftFromState(mapState, "edit", true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFitMap = () => {
|
|
|
|
|
const availableWidth = typeof window === "undefined"
|
|
|
|
|
? DEFAULT_MAP_SIZE.width
|
|
|
|
|
@ -383,7 +417,7 @@ export default function LocationMapManager() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasAnyMap = Boolean(mapState?.draft_map || mapState?.published_map);
|
|
|
|
|
const status = getMapStatus(mapState);
|
|
|
|
|
const status = hasUnsavedChanges ? "Unsaved Draft" : getMapStatus(mapState);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="location-map-page">
|
|
|
|
|
@ -398,16 +432,8 @@ export default function LocationMapManager() {
|
|
|
|
|
zoom={zoom}
|
|
|
|
|
setZoom={setZoom}
|
|
|
|
|
onFit={handleFitMap}
|
|
|
|
|
onView={() => {
|
|
|
|
|
setMode("view");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(false);
|
|
|
|
|
}}
|
|
|
|
|
onEdit={() => {
|
|
|
|
|
setMode("edit");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
}}
|
|
|
|
|
onView={handleViewMode}
|
|
|
|
|
onEdit={handleEditMode}
|
|
|
|
|
onPanMode={() => setEditorTool("pan")}
|
|
|
|
|
onEditObjects={() => setEditorTool("edit")}
|
|
|
|
|
/>
|
|
|
|
|
@ -418,10 +444,7 @@ export default function LocationMapManager() {
|
|
|
|
|
canManage={canManage}
|
|
|
|
|
saving={saving}
|
|
|
|
|
onContinue={() => setMode("edit")}
|
|
|
|
|
onPreview={() => {
|
|
|
|
|
setMode("view");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
}}
|
|
|
|
|
onPreview={handlePreviewDraft}
|
|
|
|
|
onPublish={handlePublish}
|
|
|
|
|
onCreateFromZones={handleCreateFromZones}
|
|
|
|
|
onCreateBlank={handleCreateBlank}
|
|
|
|
|
@ -459,19 +482,17 @@ export default function LocationMapManager() {
|
|
|
|
|
history={history}
|
|
|
|
|
future={future}
|
|
|
|
|
saving={saving}
|
|
|
|
|
hasUnsavedChanges={hasUnsavedChanges}
|
|
|
|
|
mapState={mapState}
|
|
|
|
|
onAddObject={handleAddObject}
|
|
|
|
|
onUndo={handleUndo}
|
|
|
|
|
onRedo={handleRedo}
|
|
|
|
|
onSaveDraft={handleSaveDraft}
|
|
|
|
|
onPublish={handlePublish}
|
|
|
|
|
onPreviewDraft={handlePreviewDraft}
|
|
|
|
|
onPanMode={() => setEditorTool("pan")}
|
|
|
|
|
onEditObjects={() => setEditorTool("edit")}
|
|
|
|
|
onEditMode={() => {
|
|
|
|
|
setMode("edit");
|
|
|
|
|
setEditorTool("pan");
|
|
|
|
|
setPreviewDraft(true);
|
|
|
|
|
}}
|
|
|
|
|
onEditMode={handleEditMode}
|
|
|
|
|
onObjectField={handleObjectField}
|
|
|
|
|
onZoneLinkChange={handleZoneLinkChange}
|
|
|
|
|
onDuplicateObject={handleDuplicateObject}
|
|
|
|
|
|