This commit is contained in:
Evert Prants 2023-05-22 21:01:41 +03:00
parent 5a63f00f51
commit 8984ddd9dc
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
8 changed files with 1630 additions and 1120 deletions

2363
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,36 +9,37 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.7", "@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.0.13", "@heroicons/vue": "^2.0.18",
"@vuepic/vue-datepicker": "^3.6.5", "@vuepic/vue-datepicker": "^5.1.2",
"@vueuse/core": "^9.10.0", "@vueuse/core": "^10.1.2",
"date-fns": "^2.30.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0", "lodash.pick": "^4.4.0",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"pinia": "^2.0.28", "pinia": "^2.1.3",
"sass": "^1.57.1", "sass": "^1.62.1",
"vue": "^3.2.45", "vue": "^3.3.4",
"vue-material-design-icons": "^5.1.2", "vue-material-design-icons": "^5.2.0",
"vue-router": "^4.1.6" "vue-router": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/line-clamp": "^0.4.4",
"@types/lodash.get": "^4.4.7", "@types/lodash.get": "^4.4.7",
"@types/lodash.omit": "^4.5.7", "@types/lodash.omit": "^4.5.7",
"@types/lodash.pick": "^4.4.7", "@types/lodash.pick": "^4.4.7",
"@types/lodash.set": "^4.3.7", "@types/lodash.set": "^4.3.7",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.14",
"postcss": "^8.4.21", "postcss": "^8.4.23",
"prettier": "^2.8.3", "prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.2.1", "prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.3.2",
"typescript": "^4.9.3", "typescript": "^5.0.4",
"vite": "^4.0.0", "vite": "^4.3.8",
"vue-tsc": "^1.0.11" "vue-tsc": "^1.6.5"
} }
} }

View File

@ -8,14 +8,12 @@
> >
<template #input="{ invalid, fieldFQN, value, setValue }"> <template #input="{ invalid, fieldFQN, value, setValue }">
<Datepicker <Datepicker
text-input v-bind="props"
arrow-navigation
:class="[invalid ? 'dp__invalid' : '']" :class="[invalid ? 'dp__invalid' : '']"
:uid="fieldFQN" :uid="fieldFQN"
:placeholder="placeholder" :placeholder="placeholder"
:disabled="disabled" :state="undefined"
:clearable="clearable" :format="format"
v-bind="$attrs"
:model-value="(value as string)" :model-value="(value as string)"
@update:model-value="setValue" @update:model-value="setValue"
/> />
@ -26,10 +24,12 @@
<script setup lang="ts"> <script setup lang="ts">
import FormField from '../FormField.vue'; import FormField from '../FormField.vue';
import Datepicker from '@vuepic/vue-datepicker'; import Datepicker, { VueDatePickerProps } from '@vuepic/vue-datepicker';
import { ExtractComponentProps } from '../../../utils/extract-component-props'; import '@vuepic/vue-datepicker/dist/main.css';
import { Omit } from 'lodash';
import { computed } from 'vue';
interface DatePickerProps extends ExtractComponentProps<typeof Datepicker> { interface DatePickerProps extends VueDatePickerProps {
label: string; label: string;
name: string; name: string;
disabled?: boolean; disabled?: boolean;
@ -41,5 +41,14 @@ interface DatePickerProps extends ExtractComponentProps<typeof Datepicker> {
// https://vue3datepicker.com/ // https://vue3datepicker.com/
const props = withDefaults(defineProps<DatePickerProps>(), { const props = withDefaults(defineProps<DatePickerProps>(), {
disabled: false, disabled: false,
textInput: true,
arrowNavigation: true,
enableTimePicker: true,
autoPosition: true,
is24: true,
}); });
const format = computed(() =>
props.format ?? props.enableTimePicker ? `dd/MM/yyyy, HH:mm` : 'dd/MM/yyyy'
);
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="relative h-full w-full" :class="{ 'bg-white': !transparent }"> <div class="relative w-full" :class="{ 'bg-white': !transparent }">
<div <div
class="relative h-full w-full overflow-hidden" class="relative h-full w-full overflow-hidden"
:class="{ 'bg-gray-100': !transparent }" :class="{ 'bg-gray-100': !transparent }"
@ -21,7 +21,7 @@
}" }"
/> />
<div <div
class="pointer-events-none absolute top-0 left-0 h-full w-full" class="pointer-events-none absolute left-0 top-0 h-full w-full"
v-if="!editable" v-if="!editable"
:style="{ :style="{
transformOrigin: 'top left', transformOrigin: 'top left',

View File

@ -24,9 +24,15 @@
name="type" name="type"
label="Type" label="Type"
/> />
<FormField name="barcode" label="Barcode" />
<div class="grid space-y-2 sm:grid-cols-2 sm:space-x-2 sm:space-y-0">
<FormField name="barcode" label="Barcode" />
<FormField name="sku" label="SKU / Product code" />
</div>
<FormField name="url" label="URL" /> <FormField name="url" label="URL" />
<div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2">
<div class="grid space-y-2 sm:grid-cols-3 sm:space-x-2 sm:space-y-0">
<FormField <FormField
name="weight" name="weight"
type="number" type="number"
@ -76,7 +82,7 @@
</FormGroup> </FormGroup>
<FormGroup name="additionalInfo"> <FormGroup name="additionalInfo">
<div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2"> <div class="grid space-y-2 sm:grid-cols-3 sm:space-x-2 sm:space-y-0">
<FormDateField <FormDateField
name="expiresAt" name="expiresAt"
label="Expires at" label="Expires at"

View File

@ -1,7 +1,6 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import '@vuepic/vue-datepicker/src/VueDatePicker/style/main.scss';
.dp__input { .dp__input {
@apply block w-full rounded-md border-gray-300 shadow-sm transition-colors duration-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm; @apply block w-full rounded-md border-gray-300 shadow-sm transition-colors duration-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm;

View File

@ -12,6 +12,7 @@
<HousePlanner <HousePlanner
v-if="selectedFloorId" v-if="selectedFloorId"
editable editable
class="h-full"
ref="plannerRef" ref="plannerRef"
:key="`planner-${selectedFloorId}`" :key="`planner-${selectedFloorId}`"
:floor-document="floorPlan" :floor-document="floorPlan"

View File

@ -1,161 +1,166 @@
<template> <template>
<PageHead bordered> <div class="min-h-[160vh]">
<div class="flex flex-col"> <PageHead bordered>
<h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1> <div class="flex flex-col">
<span class="text-sm font-light text-gray-800 line-clamp-1" <h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1>
>Floor {{ floor?.number }} in {{ building?.displayName }}</span <span class="text-sm font-light text-gray-800 line-clamp-1"
> >Floor {{ floor?.number }} in {{ building?.displayName }}</span
</div>
<Menu
title="Actions"
class="self-end"
: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"> </div>
<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,
'pointer-events-auto': !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"> <Menu
<button title="Actions"
v-if="!isSet(storage)" class="self-end"
@click.stop="selectAndAdd(storage)" :options="[
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" title: 'Edit floor plan',
> link: {
<ArrowDownOnSquareIcon name: 'planner',
aria-hidden="true" query: { buildingId: building?.id, floorId: floor?.id },
class="h-4 w-4" },
/> },
</button> ]"
<button />
@click.stop="selectStorage(storage)" </PageHead>
class="rounded-full px-1 py-1 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
title="Open Storage" <button
> @click="showMap = !showMap"
<ArrowUpOnSquareIcon aria-hidden="true" class="h-4 w-4" /> :class="[
</button> 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,
'pointer-events-auto': !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> </div>
</div> <span class="text-sm" v-if="!isSet(storage)"
<span class="text-sm" v-if="!isSet(storage)" >Stored items:
>Stored items: {{ (storage as StorageListItem).itemCount }}</span
{{ (storage as StorageListItem).itemCount }}</span >
> <span class="text-sm" v-else
<span class="text-sm" v-else >Storages:
>Storages: {{ (storage as StorageSetListItem).storages.length }}</span
{{ (storage as StorageSetListItem).storages.length }}</span >
> </StorageBubble>
</StorageBubble> </template>
</template> </template>
</template> </RoomPolygon>
</RoomPolygon> </HousePlanner>
</HousePlanner> </div>
</div> </Transition>
</Transition>
<ItemSelector <ItemSelector
:class="{ 'mt-2': !showMap }" :class="{ 'mt-2': !showMap }"
:rooms="rooms" :rooms="rooms"
:storages="storages" :storages="storages"
:selected-room="selectedRoom" :selected-room="selectedRoom"
:selected-storage="selectedStorage" :selected-storage="selectedStorage"
:selected-set="selectedSet" :selected-set="selectedSet"
@select-room="selectRoomFromList" @select-room="selectRoomFromList"
@select-storage="selectStorage" @select-storage="selectStorage"
@new-storage="addNewStorage" @new-storage="addNewStorage"
/> />
<StorageView <StorageView
v-if="selectedStorage && building" v-if="selectedStorage && building"
ref="storageViewRef" ref="storageViewRef"
@storage-added="newStoredItemAdded" @storage-added="newStoredItemAdded"
:storage="selectedStorage" :storage="selectedStorage"
:building="building" :building="building"
/> />
<NewStorageModal ref="newStorage" @added="storageAdded" /> <NewStorageModal ref="newStorage" @added="storageAdded" />
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">