stored item list, menu component wrapper
This commit is contained in:
parent
b875c2465d
commit
710a904323
11
package-lock.json
generated
11
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
|
"vue-material-design-icons": "^5.1.2",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -2007,6 +2008,11 @@
|
|||||||
"@vue/shared": "3.2.45"
|
"@vue/shared": "3.2.45"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-material-design-icons": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-nD1qFM2qAkMlVoe23EfNKIeMfYl6YjHZjSty9q0mwc2gXmPmvEhixywJQhM+VF5KVBI1zAkVTNLoUEERPY10pA=="
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
||||||
@ -3300,6 +3306,11 @@
|
|||||||
"@vue/shared": "3.2.45"
|
"@vue/shared": "3.2.45"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vue-material-design-icons": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-nD1qFM2qAkMlVoe23EfNKIeMfYl6YjHZjSty9q0mwc2gXmPmvEhixywJQhM+VF5KVBI1zAkVTNLoUEERPY10pA=="
|
||||||
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
|
"vue-material-design-icons": "^5.1.2",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative" ref="wrapper">
|
|
||||||
<slot name="trigger" :title="title" :open="open" :toggle="toggle">
|
|
||||||
<button type="button" @click="() => toggle()" :aria-expanded="open">
|
|
||||||
<span>{{ title }}</span>
|
|
||||||
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
name="menu-transition"
|
|
||||||
enter-active-class="transition ease-out duration-200"
|
|
||||||
enter-from-class="opacity-0 translate-y-1"
|
|
||||||
enter-to-class="opacity-100 translate-y-0"
|
|
||||||
leave-active-class="transition ease-in duration-150"
|
|
||||||
leave-from-class="opacity-100 translate-y-0"
|
|
||||||
leave-to-class="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<slot :title="title" :open="open" :toggle="toggle" v-if="open"></slot>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
|
||||||
|
|
||||||
const open = ref(false);
|
|
||||||
const wrapper = ref();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
title: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const toggle = (to?: boolean) => {
|
|
||||||
open.value = to ?? !open.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const event = (e: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
wrapper.value.contains(e.target as HTMLElement) &&
|
|
||||||
!(e.target as HTMLElement).closest('a')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
open.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('click', event);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('click', event);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
toggle(false);
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -1,56 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative">
|
|
||||||
<Dropdown :title="title">
|
|
||||||
<template #trigger="{ title, open, toggle }">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="() => toggle()"
|
|
||||||
:aria-expanded="open"
|
|
||||||
:class="[
|
|
||||||
open ? 'text-gray-900' : 'text-gray-500',
|
|
||||||
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span>{{ title }}</span>
|
|
||||||
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default="{ title, open, toggle }">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
positionClass,
|
|
||||||
'absolute z-10 -ml-4 mt-1 w-screen max-w-xs transform px-2 sm:px-0 lg:ml-0',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
|
||||||
import { computed } from '@vue/reactivity';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
|
||||||
import Dropdown from './Dropdown.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
title: string;
|
|
||||||
position?: 'left' | 'center' | 'right';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const positionClass = computed(() => {
|
|
||||||
if (!props.position || props.position === 'center')
|
|
||||||
return 'left-1/2 -translate-x-1/2';
|
|
||||||
if (props.position === 'left') return 'left-0';
|
|
||||||
if (props.position === 'right') return 'right-0';
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -3,7 +3,7 @@
|
|||||||
<div
|
<div
|
||||||
class="hoverbox relative flex h-60 flex-col overflow-hidden rounded-lg bg-gray-50 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 hover:scale-105 md:h-80"
|
class="hoverbox relative flex h-60 flex-col overflow-hidden rounded-lg bg-gray-50 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 hover:scale-105 md:h-80"
|
||||||
>
|
>
|
||||||
<img v-if="image" :src="image" class="absolute object-cover" />
|
<img v-if="image" :src="image" alt="" class="absolute object-cover" />
|
||||||
<div class="z-10 mt-auto flex flex-col bg-white px-4 py-4">
|
<div class="z-10 mt-auto flex flex-col bg-white px-4 py-4">
|
||||||
<span class="text-lg font-bold">{{ title }}</span>
|
<span class="text-lg font-bold">{{ title }}</span>
|
||||||
<span class="text-sm" v-if="subtitle">{{ subtitle }}</span>
|
<span class="text-sm" v-if="subtitle">{{ subtitle }}</span>
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="mb-2 flex items-center justify-between border-b-2 border-gray-100 pb-2"
|
:class="[
|
||||||
|
'mb-2 flex items-center justify-between pb-2',
|
||||||
|
bordered ? 'border-b-2 border-gray-100' : '',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<DropdownButton title="Actions" :position="'right'" v-if="withActions">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<slot name="actions" />
|
|
||||||
</div>
|
|
||||||
</DropdownButton>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DropdownButton from './DropdownButton.vue';
|
defineProps<{
|
||||||
const props = defineProps<{
|
bordered?: boolean;
|
||||||
withActions?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="-my-2 -mr-2 md:hidden">
|
<div class="-my-2 -mr-2 md:hidden">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Open menu</span>
|
<span class="sr-only">Open menu</span>
|
||||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<PopoverButton
|
<PopoverButton
|
||||||
:class="[
|
:class="[
|
||||||
open ? 'text-gray-900' : 'text-gray-500',
|
open ? 'text-gray-900' : 'text-gray-500',
|
||||||
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2',
|
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span>Buildings</span>
|
<span>Buildings</span>
|
||||||
@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="-mr-2">
|
<div class="-mr-2">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<span class="sr-only">Close menu</span>
|
<span class="sr-only">Close menu</span>
|
||||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<Menu title="User" :options="[{ title: 'Logout' }]">
|
||||||
<button
|
<template #trigger>
|
||||||
|
<MenuButton
|
||||||
class="flex flex-row items-center space-x-2 rounded-full px-1 py-1 pr-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
class="flex flex-row items-center space-x-2 rounded-full px-1 py-1 pr-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -10,13 +11,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<span>{{ user.name }}</span>
|
<span>{{ user.name }}</span>
|
||||||
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
||||||
</button>
|
</MenuButton>
|
||||||
</div>
|
</template>
|
||||||
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { MenuButton } from '@headlessui/vue';
|
||||||
import { ChevronDownIcon, UserIcon } from '@heroicons/vue/24/outline';
|
import { ChevronDownIcon, UserIcon } from '@heroicons/vue/24/outline';
|
||||||
import { User } from '../../interfaces/user.interfaces';
|
import { User } from '../../interfaces/user.interfaces';
|
||||||
|
import Menu from '../menu/Menu.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: User;
|
user: User;
|
||||||
|
@ -7,13 +7,32 @@
|
|||||||
<template #default="{ closeModal }">
|
<template #default="{ closeModal }">
|
||||||
<FormAlert :message="error" />
|
<FormAlert :message="error" />
|
||||||
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
||||||
<FormField name="displayName" label="Display Name" />
|
<FormField name="displayName" label="Display Name" required />
|
||||||
<FormSelectField :options="selectOptions" name="type" label="Type" />
|
<FormSelectField
|
||||||
|
:options="selectOptions"
|
||||||
|
name="type"
|
||||||
|
label="Type"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<FormField name="locationDescription" label="Location description">
|
<FormField name="locationDescription" label="Location description">
|
||||||
<span class="text-sm">Describe the location of this storage</span>
|
<span class="text-sm">Describe the location of this storage</span>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormColorField name="color" label="Color" />
|
<FormColorField name="color" label="Color" />
|
||||||
<button type="submit">Submit</button>
|
<div class="flex justify-end space-x-1">
|
||||||
|
<button
|
||||||
|
class="inline-flex justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
type="submit"
|
||||||
|
@click="submitlock = true"
|
||||||
|
>
|
||||||
|
Submit and add another
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -56,6 +75,7 @@ const building = ref<BuildingListItem>();
|
|||||||
const set = ref<StorageSetListItem | boolean>();
|
const set = ref<StorageSetListItem | boolean>();
|
||||||
const data = ref({ ...defaults });
|
const data = ref({ ...defaults });
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
const submitlock = ref(false);
|
||||||
const selectOptions = computed(() => {
|
const selectOptions = computed(() => {
|
||||||
const source = set.value === true ? StorageSetTypeName : StorageTypeName;
|
const source = set.value === true ? StorageSetTypeName : StorageTypeName;
|
||||||
return Object.keys(source).reduce<SelectOption[]>(
|
return Object.keys(source).reduce<SelectOption[]>(
|
||||||
@ -82,6 +102,10 @@ const emit = defineEmits<{
|
|||||||
(e: 'added', storage: StorageSharedType): void;
|
(e: 'added', storage: StorageSharedType): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
data.value = { ...defaults };
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openModal: (
|
openModal: (
|
||||||
useBuilding: BuildingListItem,
|
useBuilding: BuildingListItem,
|
||||||
@ -91,7 +115,7 @@ defineExpose({
|
|||||||
room.value = useRoom;
|
room.value = useRoom;
|
||||||
building.value = useBuilding;
|
building.value = useBuilding;
|
||||||
set.value = useSet;
|
set.value = useSet;
|
||||||
data.value = { ...defaults };
|
reset();
|
||||||
modalRef.value?.openModal();
|
modalRef.value?.openModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -133,7 +157,12 @@ const onSubmit = async (value: FormSubmit) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
modalRef.value?.closeModal();
|
|
||||||
emit('added', createdStorage);
|
emit('added', createdStorage);
|
||||||
|
if (submitlock.value) {
|
||||||
|
submitlock.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalRef.value?.closeModal();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
imageClasses,
|
imageClasses,
|
||||||
'relative mt-2 flex items-center justify-center overflow-hidden rounded-md bg-gray-100 ring-1 ring-black ring-opacity-5',
|
'relative flex flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-gray-100 ring-1 ring-black ring-opacity-5',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@ -15,15 +15,15 @@
|
|||||||
/>
|
/>
|
||||||
<CubeIcon :class="iconClasses" />
|
<CubeIcon :class="iconClasses" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="-mt-1 flex flex-col">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex flex-col md:flex-row md:items-center md:space-x-1">
|
||||||
<span :class="`text-${size} font-bold`">{{
|
<span :class="`text-${size} font-bold`">{{
|
||||||
storedItem.item.displayName
|
storedItem.item.displayName
|
||||||
}}</span>
|
}}</span>
|
||||||
<span
|
<span
|
||||||
:class="`mt-0.5 ${fontSize} text-gray-500`"
|
:class="`md:mt-0.5 ${fontSize} text-gray-500`"
|
||||||
v-if="storedItem.addedBy"
|
v-if="storedItem.addedBy"
|
||||||
>· Added
|
><span class="hidden md:inline">· </span>Added
|
||||||
{{
|
{{
|
||||||
dateToLocaleString(storedItem.acquiredAt || storedItem.createdAt)
|
dateToLocaleString(storedItem.acquiredAt || storedItem.createdAt)
|
||||||
}}
|
}}
|
||||||
@ -96,13 +96,13 @@ const props = defineProps<{
|
|||||||
const imageClasses = computed(() => {
|
const imageClasses = computed(() => {
|
||||||
if (props.size === 'sm') return 'h-8 w-8';
|
if (props.size === 'sm') return 'h-8 w-8';
|
||||||
if (props.size === 'md') return 'h-12 w-12';
|
if (props.size === 'md') return 'h-12 w-12';
|
||||||
return 'h-16 w-16';
|
return 'sm:h-16 sm:w-16 h-8 w-8';
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconClasses = computed(() => {
|
const iconClasses = computed(() => {
|
||||||
if (props.size === 'sm') return 'h-4 w-4';
|
if (props.size === 'sm') return 'h-4 w-4';
|
||||||
if (props.size === 'md') return 'h-8 w-8';
|
if (props.size === 'md') return 'h-8 w-8';
|
||||||
return 'h-12 w-12';
|
return 'sm:h-12 sm:w-12 h-4 w-4';
|
||||||
});
|
});
|
||||||
|
|
||||||
const fontSize = computed(() => {
|
const fontSize = computed(() => {
|
||||||
|
95
src/components/menu/Menu.vue
Normal file
95
src/components/menu/Menu.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<Menu as="div" class="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<slot name="trigger" :title="title">
|
||||||
|
<MenuButton
|
||||||
|
:class="[
|
||||||
|
'inline-flex w-full justify-center rounded-md bg-opacity-20 px-4 py-2',
|
||||||
|
'text-sm font-medium text-black hover:bg-opacity-30',
|
||||||
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-opacity-75',
|
||||||
|
buttonClass,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot name="default">
|
||||||
|
{{ title }}
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="ml-2 -mr-1 h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-if="!hideChevron"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</MenuButton>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition duration-100 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
|
enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-75 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
<MenuItems
|
||||||
|
:class="[
|
||||||
|
'absolute z-10 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg',
|
||||||
|
'ring-1 ring-black ring-opacity-5 focus:outline-none',
|
||||||
|
positionClass,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="px-1 py-1">
|
||||||
|
<MenuItem
|
||||||
|
as="template"
|
||||||
|
v-for="(option, index) of options"
|
||||||
|
:key="option.key || index"
|
||||||
|
v-slot="{ active }"
|
||||||
|
>
|
||||||
|
<slot name="option" :option="option" :active="active">
|
||||||
|
<RouterLink v-if="option.link" :to="option.link">
|
||||||
|
<MenuOption :active="active" :option="option" />
|
||||||
|
</RouterLink>
|
||||||
|
<MenuOption
|
||||||
|
v-else
|
||||||
|
:active="active"
|
||||||
|
:option="option"
|
||||||
|
@clicked="() => option.onClick?.()"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
|
||||||
|
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { Component, computed } from 'vue';
|
||||||
|
import { RouteLocationRaw, RouterLink } from 'vue-router';
|
||||||
|
import MenuOption from './MenuOption.vue';
|
||||||
|
|
||||||
|
export interface MenuOption {
|
||||||
|
title: string;
|
||||||
|
key?: string;
|
||||||
|
link?: RouteLocationRaw;
|
||||||
|
icon?: Component;
|
||||||
|
onClick?: () => void;
|
||||||
|
component?: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
options: MenuOption[];
|
||||||
|
buttonClass?: string;
|
||||||
|
hideChevron?: boolean;
|
||||||
|
position?: 'left' | 'center' | 'right';
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const positionClass = computed(() => {
|
||||||
|
if (!props.position || props.position === 'right') return 'right-0';
|
||||||
|
if (props.position === 'center') return 'left-1/2 -translate-x-1/2';
|
||||||
|
if (props.position === 'left') return 'left-0';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
</script>
|
30
src/components/menu/MenuOption.vue
Normal file
30
src/components/menu/MenuOption.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-blue-500 text-white' : 'text-gray-900',
|
||||||
|
'group flex w-full items-center rounded-md px-2 py-2 text-sm',
|
||||||
|
]"
|
||||||
|
@click="emit('clicked')"
|
||||||
|
>
|
||||||
|
<component v-if="option.icon" :is="option.icon" class="mr-2 h-5 w-5" />
|
||||||
|
<component
|
||||||
|
v-if="option.component"
|
||||||
|
:is="option.component"
|
||||||
|
:option="option"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ option.title }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MenuOption } from './Menu.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
active: boolean;
|
||||||
|
option: MenuOption;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'clicked'): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
@ -1,11 +1,11 @@
|
|||||||
import { NavigationGuardWithThis, RouteRecordRaw } from 'vue-router';
|
import { createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||||
import Dashboard from '../views/Dashboard.vue';
|
import Dashboard from '../views/Dashboard.vue';
|
||||||
import Login from '../views/Login.vue';
|
import Login from '../views/Login.vue';
|
||||||
import HousePlanner from '../views/HousePlanner.vue';
|
import HousePlanner from '../views/HousePlanner.vue';
|
||||||
import BuildingViewBase from '../views/building/BuildingViewBase.vue';
|
import BuildingViewBase from '../views/building/BuildingViewBase.vue';
|
||||||
import BuildingView from '../views/building/BuildingView.vue';
|
import BuildingView from '../views/building/BuildingView.vue';
|
||||||
import FloorView from '../views/building/floors/FloorView.vue';
|
import FloorView from '../views/building/floors/FloorView.vue';
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter } from 'vue-router';
|
||||||
import { useUserStore } from '../store/user.store';
|
import { useUserStore } from '../store/user.store';
|
||||||
import Demo from '../views/Demo.vue';
|
import Demo from '../views/Demo.vue';
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<StandardLayout>
|
<StandardLayout>
|
||||||
<PageHead><h1 class="text-2xl font-bold">Dashboard</h1></PageHead>
|
<PageHead bordered><h1 class="text-2xl font-bold">Dashboard</h1></PageHead>
|
||||||
|
|
||||||
<p>Hello, {{ user.name }}</p>
|
<p>Hello, {{ user.name }}</p>
|
||||||
|
|
||||||
@ -20,9 +20,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<PageHead class="mt-4"
|
<PageHead class="mt-4">
|
||||||
><h2 class="text-xl font-bold">Buildings</h2></PageHead
|
<h2 class="text-xl font-bold">Buildings</h2>
|
||||||
>
|
</PageHead>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<PageHead with-actions>
|
<PageHead bordered>
|
||||||
<template #actions>
|
|
||||||
<RouterLink
|
|
||||||
class="px-2 py-2"
|
|
||||||
:to="{ name: 'planner', query: { buildingId: building?.id } }"
|
|
||||||
>Edit floor plans</RouterLink
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default>
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1 class="text-2xl font-bold">{{ building?.displayName }}</h1>
|
<h1 class="text-2xl font-bold">{{ building?.displayName }}</h1>
|
||||||
<span class="text-sm font-light text-gray-800 line-clamp-1">{{
|
<span class="text-sm font-light text-gray-800 line-clamp-1">{{
|
||||||
building?.address
|
building?.address
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<Menu
|
||||||
|
title="Actions"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
title: 'Edit floor plans',
|
||||||
|
link: { name: 'planner', query: { buildingId: building?.id } },
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</PageHead>
|
</PageHead>
|
||||||
|
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import Menu from '../../components/menu/Menu.vue';
|
||||||
import PageHead from '../../components/PageHead.vue';
|
import PageHead from '../../components/PageHead.vue';
|
||||||
import { useBuildingStore } from '../../store/building.store';
|
import { useBuildingStore } from '../../store/building.store';
|
||||||
import FloorListItem from './floors/FloorListItem.vue';
|
import FloorListItem from './floors/FloorListItem.vue';
|
||||||
|
@ -1,34 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<PageHead with-actions>
|
<PageHead bordered>
|
||||||
<template #actions>
|
|
||||||
<RouterLink
|
|
||||||
class="px-2 py-2"
|
|
||||||
:to="{
|
|
||||||
name: 'planner',
|
|
||||||
query: { buildingId: building?.id, floorId: floor?.id },
|
|
||||||
}"
|
|
||||||
>Edit floor plan</RouterLink
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default>
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1>
|
<h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1>
|
||||||
<span class="text-sm font-light text-gray-800 line-clamp-1"
|
<span class="text-sm font-light text-gray-800 line-clamp-1"
|
||||||
>Floor {{ floor?.number }} in {{ building?.displayName }}</span
|
>Floor {{ floor?.number }} in {{ building?.displayName }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<Menu
|
||||||
|
title="Actions"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
title: 'Edit floor plan',
|
||||||
|
link: {
|
||||||
|
name: 'planner',
|
||||||
|
query: { buildingId: building?.id, floorId: floor?.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</PageHead>
|
</PageHead>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="showMap = !showMap"
|
@click="showMap = !showMap"
|
||||||
:class="[
|
:class="[
|
||||||
showMap ? 'bg-gray-100' : 'rounded-md ring-1 ring-black ring-opacity-5',
|
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',
|
'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 class="sr-only">{ showMap ? 'Hide' : 'Show' }}</span> Floor plan
|
<span>{{ showMap ? 'Hide' : 'Show' }} interactive floor plan</span>
|
||||||
<ChevronUpIcon v-if="showMap" class="h-4 w-4" />
|
<ChevronUpIcon v-if="showMap" class="h-4 w-4" />
|
||||||
<ChevronDownIcon v-else class="h-4 w-4" />
|
<ChevronDownIcon v-else class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -83,6 +84,7 @@
|
|||||||
@mouseenter="() => (hoveredBubble = storage)"
|
@mouseenter="() => (hoveredBubble = storage)"
|
||||||
@start-moving="moveBubble(storage)"
|
@start-moving="moveBubble(storage)"
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<span class="text-md font-bold">{{
|
<span class="text-md font-bold">{{
|
||||||
storage.displayName
|
storage.displayName
|
||||||
@ -93,6 +95,28 @@
|
|||||||
>(Storage Set)</span
|
>(Storage Set)</span
|
||||||
>
|
>
|
||||||
</div>
|
</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)"
|
<span class="text-sm" v-if="!isSet(storage)"
|
||||||
>Stored items:
|
>Stored items:
|
||||||
{{ (storage as StorageListItem).itemCount }}</span
|
{{ (storage as StorageListItem).itemCount }}</span
|
||||||
@ -110,6 +134,7 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<ItemSelector
|
<ItemSelector
|
||||||
|
:class="{ 'mt-2': !showMap }"
|
||||||
:rooms="rooms"
|
:rooms="rooms"
|
||||||
:storages="storages"
|
:storages="storages"
|
||||||
:selected-room="selectedRoom"
|
:selected-room="selectedRoom"
|
||||||
@ -118,21 +143,26 @@
|
|||||||
@select-room="selectRoomFromList"
|
@select-room="selectRoomFromList"
|
||||||
@select-storage="selectStorage"
|
@select-storage="selectStorage"
|
||||||
@new-storage="addNewStorage"
|
@new-storage="addNewStorage"
|
||||||
@add-item="newItem?.openModal(building!, selectedStorage!)"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StoredItemCard
|
<StorageView
|
||||||
v-for="stored of selectedStorage?.items || []"
|
v-if="selectedStorage && building"
|
||||||
size="lg"
|
ref="storageViewRef"
|
||||||
:stored-item="stored"
|
@storage-added="newStoredItemAdded"
|
||||||
|
:storage="selectedStorage"
|
||||||
|
:building="building"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
||||||
<NewItemModal ref="newItem" @added="newStoredItemAdded" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/outline';
|
import {
|
||||||
|
ArrowDownOnSquareIcon,
|
||||||
|
ArrowUpOnSquareIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
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';
|
||||||
@ -158,10 +188,11 @@ import StorageBubble from './StorageBubble.vue';
|
|||||||
import RoomPolygon from './RoomPolygon.vue';
|
import RoomPolygon from './RoomPolygon.vue';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
import isSet from '../../../utils/is-storage-set';
|
import isSet from '../../../utils/is-storage-set';
|
||||||
import ItemSelector from './ItemSelector.vue';
|
import ItemSelector from './StorageSelector.vue';
|
||||||
import StoredItemCard from '../../../components/item/StoredItemCard.vue';
|
import StoredItemCard from '../../../components/item/StoredItemCard.vue';
|
||||||
import NewStorageModal from '../../../components/item/NewStorageModal.vue';
|
import NewStorageModal from '../../../components/item/NewStorageModal.vue';
|
||||||
import NewItemModal from '../../../components/item/NewItemModal.vue';
|
import StorageView from './storage/StorageView.vue';
|
||||||
|
import Menu from '../../../components/menu/Menu.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const buildingStore = useBuildingStore();
|
const buildingStore = useBuildingStore();
|
||||||
@ -178,7 +209,7 @@ const selectedSet = ref<StorageSetListItem>();
|
|||||||
const selectedStorage = ref<StorageListItem>();
|
const selectedStorage = ref<StorageListItem>();
|
||||||
|
|
||||||
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
|
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
|
||||||
const newItem = ref<InstanceType<typeof NewItemModal>>();
|
const storageViewRef = ref<InstanceType<typeof StorageView>>();
|
||||||
|
|
||||||
const floor = computed(() =>
|
const floor = computed(() =>
|
||||||
building.value?.floors.find(
|
building.value?.floors.find(
|
||||||
@ -250,7 +281,7 @@ const selectRoomFromList = (room: RoomLayoutObject) => {
|
|||||||
selectedRoom.value = room;
|
selectedRoom.value = room;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectStorage = (
|
const selectStorage = async (
|
||||||
storage: StorageSharedType,
|
storage: StorageSharedType,
|
||||||
parentSet?: StorageSetListItem
|
parentSet?: StorageSetListItem
|
||||||
) => {
|
) => {
|
||||||
@ -262,7 +293,7 @@ const selectStorage = (
|
|||||||
|
|
||||||
selectedSet.value = parentSet;
|
selectedSet.value = parentSet;
|
||||||
selectedStorage.value = storage as StorageListItem;
|
selectedStorage.value = storage as StorageListItem;
|
||||||
jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
await jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
||||||
headers: authHeader.value,
|
headers: authHeader.value,
|
||||||
}).then(({ data: storage }) => {
|
}).then(({ data: storage }) => {
|
||||||
selectedStorage.value = storage;
|
selectedStorage.value = storage;
|
||||||
@ -333,4 +364,9 @@ const newStoredItemAdded = async (newItem: StoredItem) => {
|
|||||||
];
|
];
|
||||||
selectedStorage.value.itemCount = selectedStorage.value.items.length;
|
selectedStorage.value.itemCount = selectedStorage.value.items.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectAndAdd = async (storage: StorageSharedType) => {
|
||||||
|
await selectStorage(storage);
|
||||||
|
storageViewRef.value?.addNewItem();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,173 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'flex flex-col md:grid',
|
|
||||||
selectedSet ? 'md:grid-cols-4' : 'md:grid-cols-3',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<button
|
|
||||||
v-for="room of rooms"
|
|
||||||
@click="emit('selectRoom', 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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span>{{ room.displayName }}</span> <ChevronRightIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="selectedRoom?.id">
|
|
||||||
<div class="my-2 flex justify-center md:hidden">
|
|
||||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<button
|
|
||||||
v-for="storage of storages"
|
|
||||||
@click="emit('selectStorage', storage)"
|
|
||||||
:class="[
|
|
||||||
(isSet(storage) && selectedSet?.id === storage.id) ||
|
|
||||||
(selectedStorage?.id === storage.id && !isSet(storage))
|
|
||||||
? '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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
>{{ storage.displayName }}
|
|
||||||
|
|
||||||
<span v-if="!isSet(storage)"
|
|
||||||
>({{ (storage as StorageListItem).itemCount }})</span
|
|
||||||
>
|
|
||||||
<span v-else>
|
|
||||||
(set) ({{
|
|
||||||
(storage as StorageSetListItem).storages.length
|
|
||||||
}})</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<ChevronRightIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mx-auto flex space-x-2 py-2">
|
|
||||||
<button
|
|
||||||
@click="emit('newStorage', false)"
|
|
||||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
|
||||||
</button>
|
|
||||||
<span>·</span>
|
|
||||||
<button
|
|
||||||
@click="emit('newStorage', true)"
|
|
||||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage Set</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="selectedSet">
|
|
||||||
<div class="my-2 flex justify-center md:hidden">
|
|
||||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<button
|
|
||||||
v-for="storage of selectedSet.storages"
|
|
||||||
@click="emit('selectStorage', storage, selectedSet)"
|
|
||||||
:class="[
|
|
||||||
selectedStorage?.id === storage.id && !isSet(storage)
|
|
||||||
? '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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span>{{ storage.displayName }} ({{ storage.itemCount }})</span>
|
|
||||||
<ChevronRightIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mx-auto flex space-x-2 py-2">
|
|
||||||
<button
|
|
||||||
@click="emit('newStorage', selectedSet!)"
|
|
||||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="selectedStorage?.items">
|
|
||||||
<div class="my-2 flex justify-center md:hidden">
|
|
||||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<button
|
|
||||||
v-for="item of selectedStorage.items"
|
|
||||||
:class="[
|
|
||||||
'hover:bg-blue-100',
|
|
||||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span>{{ item.item.displayName }}</span>
|
|
||||||
<ChevronRightIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mx-auto flex space-x-2 py-2">
|
|
||||||
<button
|
|
||||||
@click="emit('addItem', selectedStorage!)"
|
|
||||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<PlusIcon class="h-4 w-4" /> <span>Add Items</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="my-2 mb-4 flex justify-center md:hidden"
|
|
||||||
v-if="selectedStorage?.items?.length"
|
|
||||||
>
|
|
||||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ChevronDoubleDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
PlusIcon,
|
|
||||||
} from '@heroicons/vue/24/outline';
|
|
||||||
import isSet from '../../../utils/is-storage-set';
|
|
||||||
import {
|
|
||||||
StorageListItem,
|
|
||||||
StorageSetListItem,
|
|
||||||
StorageSharedType,
|
|
||||||
} from '../../../interfaces/storage.interfaces';
|
|
||||||
import { RoomLayoutObject } from '../../../interfaces/room.interfaces';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
rooms: RoomLayoutObject[];
|
|
||||||
storages: StorageSharedType[];
|
|
||||||
selectedRoom?: RoomLayoutObject;
|
|
||||||
selectedSet?: StorageSetListItem;
|
|
||||||
selectedStorage?: StorageListItem;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(
|
|
||||||
e: 'selectStorage',
|
|
||||||
storage: StorageSharedType,
|
|
||||||
parentSet?: StorageSetListItem
|
|
||||||
): void;
|
|
||||||
(e: 'selectRoom', room: RoomLayoutObject): void;
|
|
||||||
(e: 'newStorage', set: boolean | StorageSetListItem): void;
|
|
||||||
(e: 'addItem', storage: StorageListItem): void;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
169
src/views/building/floors/StorageSelector.vue
Normal file
169
src/views/building/floors/StorageSelector.vue
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'flex flex-col md:grid',
|
||||||
|
selectedSet ? 'md:grid-cols-3' : 'md:grid-cols-2',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div
|
||||||
|
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
|
||||||
|
>
|
||||||
|
Select Room
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-for="room of rooms"
|
||||||
|
@click="emit('selectRoom', room)"
|
||||||
|
:aria-expanded="selectedRoom?.id === room.id"
|
||||||
|
:class="[
|
||||||
|
selectedRoom?.id === room.id
|
||||||
|
? 'bg-blue-100 hover:bg-blue-200'
|
||||||
|
: 'hover:bg-blue-100',
|
||||||
|
'flex items-center justify-between border-b-1 border-gray-100 py-2 px-2',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span>{{ room.displayName }}</span>
|
||||||
|
<ChevronRightIcon class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="selectedRoom?.id">
|
||||||
|
<div class="my-2 flex justify-center md:hidden">
|
||||||
|
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div
|
||||||
|
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
|
||||||
|
>
|
||||||
|
Select Storage / Set
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-for="storage of storages"
|
||||||
|
@click="emit('selectStorage', storage)"
|
||||||
|
:aria-expanded="isStorageOrSetSelected(storage)"
|
||||||
|
:class="[
|
||||||
|
isStorageOrSetSelected(storage)
|
||||||
|
? 'bg-blue-100 hover:bg-blue-200'
|
||||||
|
: 'hover:bg-blue-100',
|
||||||
|
'flex items-center justify-between border-b-1 border-gray-100 py-2 px-2',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
>{{ storage.displayName }}
|
||||||
|
|
||||||
|
<span v-if="!isSet(storage)"
|
||||||
|
>({{ (storage as StorageListItem).itemCount }})</span
|
||||||
|
>
|
||||||
|
<span v-else>
|
||||||
|
(set) ({{
|
||||||
|
(storage as StorageSetListItem).storages.length
|
||||||
|
}})</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<ChevronRightIcon class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mx-auto flex space-x-2 py-2">
|
||||||
|
<button
|
||||||
|
@click="emit('newStorage', false)"
|
||||||
|
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
||||||
|
</button>
|
||||||
|
<span>·</span>
|
||||||
|
<button
|
||||||
|
@click="emit('newStorage', true)"
|
||||||
|
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
<PlusIcon class="h-4 w-4" /> <span>Add Storage Set</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="selectedSet">
|
||||||
|
<div class="my-2 flex justify-center md:hidden">
|
||||||
|
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div
|
||||||
|
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
|
||||||
|
>
|
||||||
|
Select Storage
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-for="storage of selectedSet.storages"
|
||||||
|
@click="emit('selectStorage', storage, selectedSet)"
|
||||||
|
:aria-expanded="isStorageSelected(storage)"
|
||||||
|
:class="[
|
||||||
|
isStorageSelected(storage)
|
||||||
|
? 'bg-blue-100 hover:bg-blue-200'
|
||||||
|
: 'hover:bg-blue-100',
|
||||||
|
'flex items-center justify-between border-b-1 border-gray-100 py-2 px-2',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span>{{ storage.displayName }} ({{ storage.itemCount }})</span>
|
||||||
|
<ChevronRightIcon class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mx-auto flex space-x-2 py-2">
|
||||||
|
<button
|
||||||
|
@click="emit('newStorage', selectedSet!)"
|
||||||
|
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 mb-4 flex justify-center" v-if="selectedStorage">
|
||||||
|
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChevronDoubleDownIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
import isSet from '../../../utils/is-storage-set';
|
||||||
|
import {
|
||||||
|
StorageListItem,
|
||||||
|
StorageSetListItem,
|
||||||
|
StorageSharedType,
|
||||||
|
} from '../../../interfaces/storage.interfaces';
|
||||||
|
import { RoomLayoutObject } from '../../../interfaces/room.interfaces';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rooms: RoomLayoutObject[];
|
||||||
|
storages: StorageSharedType[];
|
||||||
|
selectedRoom?: RoomLayoutObject;
|
||||||
|
selectedSet?: StorageSetListItem;
|
||||||
|
selectedStorage?: StorageListItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'selectStorage',
|
||||||
|
storage: StorageSharedType,
|
||||||
|
parentSet?: StorageSetListItem
|
||||||
|
): void;
|
||||||
|
(e: 'selectRoom', room: RoomLayoutObject): void;
|
||||||
|
(e: 'newStorage', set: boolean | StorageSetListItem): void;
|
||||||
|
(e: 'addItem', storage: StorageListItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isStorageOrSetSelected = (storage: StorageSharedType) =>
|
||||||
|
(isSet(storage) && props.selectedSet?.id === storage.id) ||
|
||||||
|
(props.selectedStorage?.id === storage.id && !isSet(storage));
|
||||||
|
|
||||||
|
const isStorageSelected = (storage: StorageSharedType) =>
|
||||||
|
props.selectedStorage?.id === storage.id && !isSet(storage);
|
||||||
|
</script>
|
80
src/views/building/floors/storage/StorageView.vue
Normal file
80
src/views/building/floors/storage/StorageView.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageHead bordered>
|
||||||
|
<h2 class="text-2xl font-bold">{{ storage.displayName }}</h2>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
title="Actions"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
title: 'Add new item',
|
||||||
|
onClick: addNewItem,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</PageHead>
|
||||||
|
|
||||||
|
<span class="text-xl font-bold text-gray-400" v-if="!storage.items?.length"
|
||||||
|
>No items in storage.</span
|
||||||
|
>
|
||||||
|
<div class="grid gap-2 md:grid-cols-2" v-else>
|
||||||
|
<StoredItemCard
|
||||||
|
v-for="stored of storage.items || []"
|
||||||
|
class="rounded-md bg-white px-2 py-2 shadow-md ring-1 ring-black ring-opacity-5"
|
||||||
|
size="lg"
|
||||||
|
:stored-item="stored"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<Menu
|
||||||
|
title=""
|
||||||
|
button-class="px-0.5 py-0.5"
|
||||||
|
:options="[
|
||||||
|
{ title: 'Change details' },
|
||||||
|
{ title: 'Add transaction' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||||
|
</Menu>
|
||||||
|
</template>
|
||||||
|
</StoredItemCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NewItemModal ref="newItem" @added="newStoredItemAdded" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { EllipsisVerticalIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import NewItemModal from '../../../../components/item/NewItemModal.vue';
|
||||||
|
import StoredItemCard from '../../../../components/item/StoredItemCard.vue';
|
||||||
|
import Menu from '../../../../components/menu/Menu.vue';
|
||||||
|
import PageHead from '../../../../components/PageHead.vue';
|
||||||
|
import { BuildingListItem } from '../../../../interfaces/building.interfaces';
|
||||||
|
import {
|
||||||
|
StorageListItem,
|
||||||
|
StoredItem,
|
||||||
|
} from '../../../../interfaces/storage.interfaces';
|
||||||
|
|
||||||
|
const newItem = ref<InstanceType<typeof NewItemModal>>();
|
||||||
|
const props = defineProps<{
|
||||||
|
storage: StorageListItem;
|
||||||
|
building: BuildingListItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'storageAdded', item: StoredItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const newStoredItemAdded = (item: StoredItem) => {
|
||||||
|
emit('storageAdded', item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNewItem = () => {
|
||||||
|
newItem.value?.openModal(props.building, props.storage);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
addNewItem,
|
||||||
|
});
|
||||||
|
</script>
|
@ -2,7 +2,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
borderWidth: {
|
||||||
|
1: '1px',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')],
|
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')],
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user