floor manager progress

This commit is contained in:
Evert Prants 2023-01-24 22:40:01 +02:00
parent 47f4df4971
commit 9424f831bf
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
4 changed files with 212 additions and 88 deletions

View File

@ -1,7 +1,3 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
.min-h-half {
min-height: 50%;
}

View File

@ -21,55 +21,99 @@
</template> </template>
</PageHead> </PageHead>
<HousePlanner <button
v-if="floorDocument" @click="showMap = !showMap"
:key="`fdoc${floorDocument.id}`" :class="[
ref="canvas" showMap ? 'bg-gray-100' : 'rounded-md ring-1 ring-black ring-opacity-5',
class="h-[70vh]" 'ml-auto flex items-center space-x-2 rounded-tl-lg rounded-tr-lg px-2 py-2 shadow-md',
:editable="false" ]"
:grid="false"
transparent
headless
:floor-document="floorDocument"
@click="clickOnRoom()"
> >
<template v-for="room of rooms"> <span class="sr-only">{ showMap ? 'Hide' : 'Show' }}</span> Floor plan
<div <ChevronUpIcon v-if="showMap" class="h-4 w-4" />
v-if="!highlighted || highlighted.id === room.id" <ChevronDownIcon v-else class="h-4 w-4" />
:class="[ </button>
highlighted
? '' <Transition
: 'flex cursor-pointer items-center justify-center transition-transform hover:scale-105', name="menu-transition"
'pointer-events-auto absolute', enter-active-class="transition-height ease-out duration-200"
]" enter-from-class="h-0"
@click.stop="clickOnRoom(room)" enter-to-class="h-[70vh]"
:style="getRoomPositionCSS(room)" 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(undefined, $event)"
> >
<div class="absolute" :style="getRoomPolygonCSS(room)"></div> <RoomPolygon
<span class="pointer-events-none" v-if="!highlighted">{{ v-for="room of rooms"
room.displayName :room="room"
}}</span> :highlighted="selectedRoom?.id"
<template v-if="highlighted?.id === room.id"> @clicked-in="(ev) => clickOnRoom(room, ev)"
<template v-for="storage of storages"> >
<div <template v-if="selectedRoom?.id === room.id">
class="custom-storage absolute" <StorageBubble
:style="getStoragePosition(storage)" :storage="storage"
v-for="storage of storages"
:class="{ 'z-20': storage.id === hoveredBubble }"
@mouseenter="() => (hoveredBubble = storage.id)"
@mouseleave="() => (hoveredBubble = undefined)"
> >
<div <span class="text-md font-bold">{{ storage.displayName }}</span>
class="absolute bottom-2 left-1/2 h-20 w-20 -translate-x-1/2 rounded-lg bg-blue-400 px-2 py-2" <span class="text-sm">Stored items: {{ storage.itemCount }}</span>
> </StorageBubble>
<span>{{ storage.displayName }}</span>
<div
class="absolute -bottom-2 left-1/2 h-0 w-0 -translate-x-1/2 border-x-8 border-t-[16px] border-x-transparent border-t-blue-400"
></div>
</div>
</div>
</template> </template>
</template></div </RoomPolygon>
></template> </HousePlanner>
</HousePlanner> </div>
</Transition>
<div class="grid grid-cols-3">
<div class="flex flex-col">
<button
v-for="room of rooms"
@click="selectRoomFromList(room)"
:class="[
selectedRoom?.id === room.id
? 'bg-blue-100 hover:bg-blue-200'
: 'hover:bg-blue-100',
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
]"
>
{{ room.displayName }} <ChevronRightIcon class="h-4 w-4" />
</button>
</div>
<div class="flex flex-col" v-if="selectedRoom?.id">
<button
v-for="storage of storages"
:class="[
'hover:bg-blue-100',
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
]"
>
{{ storage.displayName }} ({{ storage.itemCount }})
<ChevronRightIcon class="h-4 w-4" />
</button>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {
ChevronDownIcon,
ChevronRightIcon,
ChevronUpIcon,
} from '@heroicons/vue/24/outline';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import PageHead from '../../../components/PageHead.vue'; import PageHead from '../../../components/PageHead.vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@ -84,14 +128,20 @@ import jfetch from '../../../utils/jfetch';
import { BACKEND_URL } from '../../../constants'; import { BACKEND_URL } from '../../../constants';
import { useAccessToken } from '../../../composables/useAccessToken'; import { useAccessToken } from '../../../composables/useAccessToken';
import { StorageListItem } from '../../../interfaces/storage.interfaces'; import { StorageListItem } from '../../../interfaces/storage.interfaces';
import StorageBubble from './StorageBubble.vue';
import RoomPolygon from './RoomPolygon.vue';
import { useLocalStorage } from '@vueuse/core';
const route = useRoute(); const route = useRoute();
const buildingStore = useBuildingStore(); const buildingStore = useBuildingStore();
const { building } = storeToRefs(buildingStore); const { building } = storeToRefs(buildingStore);
const { authHeader } = useAccessToken(); const { authHeader } = useAccessToken();
const canvas = ref<InstanceType<typeof HousePlanner>>(); const canvas = ref<InstanceType<typeof HousePlanner>>();
const highlighted = ref<RoomLayoutObject>();
const storages = ref<StorageListItem[]>([]); const storages = ref<StorageListItem[]>([]);
const selectedRoom = ref<RoomLayoutObject>();
const showMap = useLocalStorage('showRoomMap', true, { writeDefaults: false });
const hoveredBubble = ref<number>();
const selectedStorage = ref<number>();
const floor = computed(() => const floor = computed(() =>
building.value?.floors.find( building.value?.floors.find(
@ -103,14 +153,30 @@ const floorDocument = computed(
() => floor.value?.plan && JSON.parse(floor.value.plan) () => floor.value?.plan && JSON.parse(floor.value.plan)
); );
const clickOnRoom = (room?: RoomLayoutObject) => { const roomCoordinate = (ev: MouseEvent) => {
const rect = (ev.target as HTMLElement).getBoundingClientRect();
const [x, y] = [ev.clientX - rect.left, ev.clientY - rect.top];
console.log(x, y);
};
const clickOnRoom = (room: RoomLayoutObject | undefined, ev: MouseEvent) => {
canvas.value?.setViewRectangle(room?.boundingBox); canvas.value?.setViewRectangle(room?.boundingBox);
if (room && !highlighted.value) { selectedStorage.value = undefined;
if (room && (!selectedRoom.value || room.id !== selectedRoom.value?.id)) {
getRoomStorages(room); getRoomStorages(room);
} else if (!room && highlighted.value) { } else if (room?.id == selectedRoom.value?.id) {
roomCoordinate(ev);
} else {
storages.value = []; storages.value = [];
} }
highlighted.value = room; selectedRoom.value = room;
};
const selectRoomFromList = (room: RoomLayoutObject) => {
canvas.value?.setViewRectangle(room?.boundingBox);
selectedStorage.value = undefined;
getRoomStorages(room);
selectedRoom.value = room;
}; };
const rooms = computed<RoomLayoutObject[]>( const rooms = computed<RoomLayoutObject[]>(
@ -134,38 +200,8 @@ const rooms = computed<RoomLayoutObject[]>(
[] []
); );
const getRoomPolygonCSS = (room: RoomLayoutObject) => {
const [min, max] = room.boundingBox;
return {
clipPath: `polygon(${room.plan
.map((point) => `${point[0] - min[0]}px ${point[1] - min[1]}px`)
.join(', ')})`,
width: `${max[0] - min[0]}px`,
height: `${max[1] - min[1]}px`,
backgroundColor: 'rgb(40 180 255 / 40%)',
};
};
const getRoomPositionCSS = (room: RoomLayoutObject) => {
const [min, max] = room.boundingBox;
return {
top: `${min[1]}px`,
left: `${min[0]}px`,
width: `${max[0] - min[0]}px`,
height: `${max[1] - min[1]}px`,
};
};
const getStoragePosition = (storage: any) => {
const locationSplit = storage.location
.split(',')
.map((int: string) => `${int}px`);
return {
left: locationSplit[0],
top: locationSplit[1],
};
};
const getRoomStorages = async (room: RoomLayoutObject) => { const getRoomStorages = async (room: RoomLayoutObject) => {
storages.value = [];
try { try {
const { data: storageList } = await jfetch( const { data: storageList } = await jfetch(
`${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`, `${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`,
@ -185,8 +221,3 @@ const getRoomStorages = async (room: RoomLayoutObject) => {
} }
}; };
</script> </script>
<style scoped lang="scss">
.custom-storage {
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div
v-if="!highlighted || highlighted === room.id"
:class="[
highlighted
? ''
: 'flex cursor-pointer items-center justify-center transition-transform hover:scale-105',
'pointer-events-auto absolute',
]"
:style="roomPositionCSS"
>
<div
class="absolute"
:style="roomPolygonCSS"
@click.stop="clickOnRoom($event)"
></div>
<span class="pointer-events-none z-10" v-if="!highlighted">{{
room.displayName
}}</span>
<slot />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { RoomLayoutObject } from '../../../interfaces/room.interfaces';
const props = defineProps<{
room: RoomLayoutObject;
highlighted?: number;
}>();
const emit = defineEmits<{
(e: 'clickedIn', ev: MouseEvent): void;
}>();
const clickOnRoom = (ev: MouseEvent) => emit('clickedIn', ev);
const roomPolygonCSS = computed(() => {
if (!props.room) return {};
const [min, max] = props.room.boundingBox;
return {
clipPath: `polygon(${props.room.plan
.map((point) => `${point[0] - min[0]}px ${point[1] - min[1]}px`)
.join(', ')})`,
width: `${max[0] - min[0]}px`,
height: `${max[1] - min[1]}px`,
backgroundColor: 'rgb(40 180 255 / 40%)',
};
});
const roomPositionCSS = computed(() => {
if (!props.room) return {};
const [min, max] = props.room.boundingBox;
return {
top: `${min[1]}px`,
left: `${min[0]}px`,
width: `${max[0] - min[0]}px`,
height: `${max[1] - min[1]}px`,
};
});
</script>

View File

@ -0,0 +1,35 @@
<template>
<div
class="custom-storage absolute z-10"
:style="getStoragePosition(storage)"
>
<div
class="absolute bottom-2 left-1/2 h-20 w-60 -translate-x-1/2 rounded-lg bg-white px-2 py-2 shadow-lg ring-1 ring-black ring-opacity-5"
>
<div class="flex flex-col">
<slot />
</div>
<div
class="absolute -bottom-2 left-1/2 h-0 w-0 -translate-x-1/2 border-x-8 border-t-[16px] border-x-transparent border-t-white"
></div>
</div>
</div>
</template>
<script setup lang="ts">
import { StorageListItem } from '../../../interfaces/storage.interfaces';
const props = defineProps<{
storage: StorageListItem;
}>();
const getStoragePosition = (storage: any) => {
const locationSplit = storage.location
.split(',')
.map((int: string) => `${int}px`);
return {
left: locationSplit[0],
top: locationSplit[1],
};
};
</script>