floor manager progress
This commit is contained in:
parent
47f4df4971
commit
9424f831bf
@ -1,7 +1,3 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.min-h-half {
|
|
||||||
min-height: 50%;
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,28 @@
|
|||||||
</template>
|
</template>
|
||||||
</PageHead>
|
</PageHead>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="showMap = !showMap"
|
||||||
|
:class="[
|
||||||
|
showMap ? 'bg-gray-100' : 'rounded-md ring-1 ring-black ring-opacity-5',
|
||||||
|
'ml-auto flex items-center space-x-2 rounded-tl-lg rounded-tr-lg px-2 py-2 shadow-md',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{ showMap ? 'Hide' : 'Show' }}</span> Floor plan
|
||||||
|
<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
|
<HousePlanner
|
||||||
v-if="floorDocument"
|
v-if="floorDocument"
|
||||||
:key="`fdoc${floorDocument.id}`"
|
:key="`fdoc${floorDocument.id}`"
|
||||||
@ -31,45 +53,67 @@
|
|||||||
transparent
|
transparent
|
||||||
headless
|
headless
|
||||||
:floor-document="floorDocument"
|
:floor-document="floorDocument"
|
||||||
@click="clickOnRoom()"
|
@click="clickOnRoom(undefined, $event)"
|
||||||
>
|
>
|
||||||
<template v-for="room of rooms">
|
<RoomPolygon
|
||||||
<div
|
v-for="room of rooms"
|
||||||
v-if="!highlighted || highlighted.id === room.id"
|
:room="room"
|
||||||
:class="[
|
:highlighted="selectedRoom?.id"
|
||||||
highlighted
|
@clicked-in="(ev) => clickOnRoom(room, ev)"
|
||||||
? ''
|
|
||||||
: 'flex cursor-pointer items-center justify-center transition-transform hover:scale-105',
|
|
||||||
'pointer-events-auto absolute',
|
|
||||||
]"
|
|
||||||
@click.stop="clickOnRoom(room)"
|
|
||||||
:style="getRoomPositionCSS(room)"
|
|
||||||
>
|
>
|
||||||
<div class="absolute" :style="getRoomPolygonCSS(room)"></div>
|
<template v-if="selectedRoom?.id === room.id">
|
||||||
<span class="pointer-events-none" v-if="!highlighted">{{
|
<StorageBubble
|
||||||
room.displayName
|
:storage="storage"
|
||||||
}}</span>
|
v-for="storage of storages"
|
||||||
<template v-if="highlighted?.id === room.id">
|
:class="{ 'z-20': storage.id === hoveredBubble }"
|
||||||
<template v-for="storage of storages">
|
@mouseenter="() => (hoveredBubble = storage.id)"
|
||||||
<div
|
@mouseleave="() => (hoveredBubble = undefined)"
|
||||||
class="custom-storage absolute"
|
|
||||||
:style="getStoragePosition(storage)"
|
|
||||||
>
|
>
|
||||||
<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>
|
|
||||||
|
62
src/views/building/floors/RoomPolygon.vue
Normal file
62
src/views/building/floors/RoomPolygon.vue
Normal 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>
|
35
src/views/building/floors/StorageBubble.vue
Normal file
35
src/views/building/floors/StorageBubble.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user