homemanager-fe/src/views/building/floors/FloorView.vue

373 lines
12 KiB
Vue

<template>
<PageHead bordered>
<div class="flex flex-col">
<h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1>
<span class="text-sm font-light text-gray-800 line-clamp-1"
>Floor {{ floor?.number }} in {{ building?.displayName }}</span
>
</div>
<Menu
title="Actions"
:options="[
{
title: 'Edit floor plan',
link: {
name: 'planner',
query: { buildingId: building?.id, floorId: floor?.id },
},
},
]"
/>
</PageHead>
<button
@click="showMap = !showMap"
:class="[
showMap ? 'bg-gray-100' : 'rounded-md ring-1 ring-black ring-opacity-5',
'ml-auto flex w-full items-center space-x-2 rounded-tl-lg rounded-tr-lg px-2 py-2 shadow-md md:w-auto',
'focus:outline-none focus:ring-2 focus:ring-blue-500',
]"
>
<span>{{ showMap ? 'Hide' : 'Show' }} interactive floor plan</span>
<ChevronUpIcon v-if="showMap" class="h-4 w-4" />
<ChevronDownIcon v-else class="h-4 w-4" />
</button>
<Transition
name="menu-transition"
enter-active-class="transition-height ease-out duration-200"
enter-from-class="h-0"
enter-to-class="h-[70vh]"
leave-active-class="transition-height ease-in duration-150"
leave-from-class="h-[70vh]"
leave-to-class="h-0"
>
<div v-if="showMap" class="z-10 overflow-hidden bg-gray-100 shadow-md">
<HousePlanner
v-if="floorDocument"
:key="`fdoc${floorDocument.id}`"
ref="canvas"
class="h-[70vh]"
:editable="false"
:grid="false"
transparent
headless
:floor-document="floorDocument"
@click="clickOnRoom()"
@zoom="(newZoom) => (canvasZoom = newZoom)"
>
<RoomPolygon
v-for="room of rooms"
:room="room"
:zoom="canvasZoom"
:highlighted="selectedRoom?.id"
@clicked-in="(x, y) => clickOnRoom(room, x, y)"
@mouse-moved-in="(x, y) => mouseMovedInRoom(room, x, y)"
>
<template v-if="selectedRoom?.id === room.id">
<template v-for="storage of storages">
<StorageBubble
v-if="
!movingBubble ||
(storage.id === movingBubble?.id &&
isSet(storage) === isSet(movingBubble))
"
:storage="storage"
:class="{
'z-20':
storage.id === hoveredBubble?.id ||
storage.id === selectedStorage?.id ||
(isSet(storage) && storage.id === selectedSet?.id),
'pointer-events-none': !!movingBubble,
}"
@mouseenter="() => (hoveredBubble = storage)"
@start-moving="moveBubble(storage)"
>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1">
<span class="text-md font-bold">{{
storage.displayName
}}</span>
<span
v-if="isSet(storage)"
class="text-[0.75rem] font-bold uppercase text-gray-400"
>(Storage Set)</span
>
</div>
<div class="flex flex-row items-center space-x-1">
<button
v-if="!isSet(storage)"
@click.stop="selectAndAdd(storage)"
class="rounded-full px-1 py-1 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
title="Insert into Storage"
>
<ArrowDownOnSquareIcon
aria-hidden="true"
class="h-4 w-4"
/>
</button>
<button
@click.stop="selectStorage(storage)"
class="rounded-full px-1 py-1 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
title="Open Storage"
>
<ArrowUpOnSquareIcon aria-hidden="true" class="h-4 w-4" />
</button>
</div>
</div>
<span class="text-sm" v-if="!isSet(storage)"
>Stored items:
{{ (storage as StorageListItem).itemCount }}</span
>
<span class="text-sm" v-else
>Storages:
{{ (storage as StorageSetListItem).storages.length }}</span
>
</StorageBubble>
</template>
</template>
</RoomPolygon>
</HousePlanner>
</div>
</Transition>
<ItemSelector
:class="{ 'mt-2': !showMap }"
:rooms="rooms"
:storages="storages"
:selected-room="selectedRoom"
:selected-storage="selectedStorage"
:selected-set="selectedSet"
@select-room="selectRoomFromList"
@select-storage="selectStorage"
@new-storage="addNewStorage"
/>
<StorageView
v-if="selectedStorage && building"
ref="storageViewRef"
@storage-added="newStoredItemAdded"
:storage="selectedStorage"
:building="building"
/>
<NewStorageModal ref="newStorage" @added="storageAdded" />
</template>
<script setup lang="ts">
import {
ArrowDownOnSquareIcon,
ArrowUpOnSquareIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@heroicons/vue/24/outline';
import { storeToRefs } from 'pinia';
import PageHead from '../../../components/PageHead.vue';
import { useRoute } from 'vue-router';
import { useBuildingStore } from '../../../store/building.store';
import { computed, ref } from '@vue/reactivity';
import HousePlanner from '../../../components/house-planner/HousePlanner.vue';
import { boundingBox } from '../../../modules/house-planner/utils';
import { Vec2 } from '../../../modules/house-planner/interfaces';
import {
RoomLayoutObject,
RoomListItem,
} from '../../../interfaces/room.interfaces';
import jfetch from '../../../utils/jfetch';
import { BACKEND_URL } from '../../../constants';
import { useAccessToken } from '../../../composables/useAccessToken';
import {
StorageListItem,
StorageSetListItem,
StorageSharedType,
StoredItem,
} from '../../../interfaces/storage.interfaces';
import StorageBubble from './StorageBubble.vue';
import RoomPolygon from './RoomPolygon.vue';
import { useLocalStorage } from '@vueuse/core';
import isSet from '../../../utils/is-storage-set';
import ItemSelector from './StorageSelector.vue';
import StoredItemCard from '../../../components/item/StoredItemCard.vue';
import NewStorageModal from '../../../components/item/NewStorageModal.vue';
import StorageView from './storage/StorageView.vue';
import Menu from '../../../components/menu/Menu.vue';
const route = useRoute();
const buildingStore = useBuildingStore();
const { building } = storeToRefs(buildingStore);
const { authHeader } = useAccessToken();
const canvas = ref<InstanceType<typeof HousePlanner>>();
const showMap = useLocalStorage('showRoomMap', true, { writeDefaults: false });
const canvasZoom = ref(1);
const storages = ref<StorageSharedType[]>([]);
const selectedRoom = ref<RoomLayoutObject>();
const hoveredBubble = ref<StorageSharedType>();
const movingBubble = ref<StorageSharedType>();
const selectedSet = ref<StorageSetListItem>();
const selectedStorage = ref<StorageListItem>();
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
const storageViewRef = ref<InstanceType<typeof StorageView>>();
const floor = computed(() =>
building.value?.floors.find(
(floor) => floor.number === Number(route.params.number)
)
);
const floorDocument = computed(
() => floor.value?.plan && JSON.parse(floor.value.plan)
);
const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => {
canvas.value?.setViewRectangle(room?.boundingBox);
selectedStorage.value = undefined;
selectedSet.value = undefined;
if (room && (!selectedRoom.value || room.id !== selectedRoom.value?.id)) {
movingBubble.value = undefined;
getRoomStorages(room);
} else if (room?.id == selectedRoom.value?.id) {
if (movingBubble.value && x != null && y != null) {
setBubbleLocation(x, y);
}
} else {
storages.value = [];
}
selectedRoom.value = room;
};
const mouseMovedInRoom = (room: RoomLayoutObject, x: number, y: number) => {
if (!movingBubble.value) return;
movingBubble.value.location = `${x},${y}`;
};
const moveBubble = (bubble: StorageSharedType) => {
movingBubble.value = bubble;
};
const setBubbleLocation = async (x: number, y: number) => {
if (!movingBubble.value) return;
await jfetch(
`${BACKEND_URL}/storage/${
(movingBubble.value as StorageSetListItem).storages ? 'set' : 'storages'
}/${movingBubble.value.id}`,
{
method: 'PATCH',
body: {
location: `${x},${y}`,
},
headers: authHeader.value,
}
);
movingBubble.value = undefined;
};
const addNewStorage = (set: boolean | StorageSetListItem) => {
if (!building.value || !selectedRoom.value) return;
newStorage.value?.openModal(
building.value,
selectedRoom.value as unknown as RoomListItem,
set
);
};
const selectRoomFromList = (room: RoomLayoutObject) => {
canvas.value?.setViewRectangle(room?.boundingBox);
selectedStorage.value = undefined;
selectedSet.value = undefined;
getRoomStorages(room);
selectedRoom.value = room;
};
const selectStorage = async (
storage: StorageSharedType,
parentSet?: StorageSetListItem
) => {
if (isSet(storage)) {
selectedSet.value = storage as StorageSetListItem;
selectedStorage.value = undefined;
return;
}
selectedSet.value = parentSet;
selectedStorage.value = storage as StorageListItem;
await jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
headers: authHeader.value,
}).then(({ data: storage }) => {
selectedStorage.value = storage;
});
};
const rooms = computed<RoomLayoutObject[]>(
() =>
(floor.value &&
floor.value.rooms
.filter((room) => room.plan?.includes('polygon'))
.map((room) => {
const { polygon } = JSON.parse(room.plan);
const aabb = boundingBox(polygon, [
[Infinity, Infinity],
[0, 0],
]);
return {
...room,
plan: polygon as Vec2[],
boundingBox: aabb,
storages: [],
} as RoomLayoutObject;
})) ||
[]
);
const getRoomStorages = async (room: RoomLayoutObject) => {
storages.value = [];
try {
const { data: storageList } = await jfetch(
`${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`,
{
headers: authHeader.value,
}
);
const { data: setList } = await jfetch(
`${BACKEND_URL}/storage/set/room/${room.id}`,
{
headers: authHeader.value,
}
);
storages.value = [...storageList, ...setList];
} catch (e) {
console.error(e);
}
};
const storageAdded = async (newStorage: StorageSharedType) => {
if (!selectedRoom.value) return;
await getRoomStorages(selectedRoom.value);
if (selectedSet.value) {
selectedSet.value.storages = [
...selectedSet.value.storages,
{
...(newStorage as StorageListItem),
itemCount: 0,
},
];
}
};
const newStoredItemAdded = async (newItem: StoredItem) => {
if (!selectedStorage.value) return;
selectedStorage.value.items = [
newItem,
...(selectedStorage.value.items || []),
];
selectedStorage.value.itemCount = selectedStorage.value.items.length;
};
const selectAndAdd = async (storage: StorageSharedType) => {
await selectStorage(storage);
storageViewRef.value?.addNewItem();
};
</script>