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",
|
||||
"sass": "^1.57.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-material-design-icons": "^5.1.2",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -2007,6 +2008,11 @@
|
||||
"@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": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
||||
@ -3300,6 +3306,11 @@
|
||||
"@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": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
|
||||
|
@ -21,6 +21,7 @@
|
||||
"pinia": "^2.0.28",
|
||||
"sass": "^1.57.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-material-design-icons": "^5.1.2",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"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
|
||||
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">
|
||||
<span class="text-lg font-bold">{{ title }}</span>
|
||||
<span class="text-sm" v-if="subtitle">{{ subtitle }}</span>
|
||||
|
@ -1,19 +1,16 @@
|
||||
<template>
|
||||
<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 />
|
||||
<DropdownButton title="Actions" :position="'right'" v-if="withActions">
|
||||
<div class="flex flex-col">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DropdownButton from './DropdownButton.vue';
|
||||
const props = defineProps<{
|
||||
withActions?: boolean;
|
||||
defineProps<{
|
||||
bordered?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<div class="-my-2 -mr-2 md:hidden">
|
||||
<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>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
@ -20,7 +20,7 @@
|
||||
<PopoverButton
|
||||
: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-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>
|
||||
@ -91,7 +91,7 @@
|
||||
</div>
|
||||
<div class="-mr-2">
|
||||
<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>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
|
@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button
|
||||
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
|
||||
class="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-gray-200"
|
||||
<Menu title="User" :options="[{ title: 'Logout' }]">
|
||||
<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"
|
||||
>
|
||||
<UserIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<span>{{ user.name }}</span>
|
||||
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-gray-200"
|
||||
>
|
||||
<UserIcon class="h-5 w-5" />
|
||||
</div>
|
||||
<span>{{ user.name }}</span>
|
||||
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
||||
</MenuButton>
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MenuButton } from '@headlessui/vue';
|
||||
import { ChevronDownIcon, UserIcon } from '@heroicons/vue/24/outline';
|
||||
import { User } from '../../interfaces/user.interfaces';
|
||||
import Menu from '../menu/Menu.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
user: User;
|
||||
|
@ -7,13 +7,32 @@
|
||||
<template #default="{ closeModal }">
|
||||
<FormAlert :message="error" />
|
||||
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
||||
<FormField name="displayName" label="Display Name" />
|
||||
<FormSelectField :options="selectOptions" name="type" label="Type" />
|
||||
<FormField name="displayName" label="Display Name" required />
|
||||
<FormSelectField
|
||||
:options="selectOptions"
|
||||
name="type"
|
||||
label="Type"
|
||||
required
|
||||
/>
|
||||
<FormField name="locationDescription" label="Location description">
|
||||
<span class="text-sm">Describe the location of this storage</span>
|
||||
</FormField>
|
||||
<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>
|
||||
</template>
|
||||
</Modal>
|
||||
@ -56,6 +75,7 @@ const building = ref<BuildingListItem>();
|
||||
const set = ref<StorageSetListItem | boolean>();
|
||||
const data = ref({ ...defaults });
|
||||
const error = ref('');
|
||||
const submitlock = ref(false);
|
||||
const selectOptions = computed(() => {
|
||||
const source = set.value === true ? StorageSetTypeName : StorageTypeName;
|
||||
return Object.keys(source).reduce<SelectOption[]>(
|
||||
@ -82,6 +102,10 @@ const emit = defineEmits<{
|
||||
(e: 'added', storage: StorageSharedType): void;
|
||||
}>();
|
||||
|
||||
const reset = () => {
|
||||
data.value = { ...defaults };
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openModal: (
|
||||
useBuilding: BuildingListItem,
|
||||
@ -91,7 +115,7 @@ defineExpose({
|
||||
room.value = useRoom;
|
||||
building.value = useBuilding;
|
||||
set.value = useSet;
|
||||
data.value = { ...defaults };
|
||||
reset();
|
||||
modalRef.value?.openModal();
|
||||
},
|
||||
});
|
||||
@ -133,7 +157,12 @@ const onSubmit = async (value: FormSubmit) => {
|
||||
);
|
||||
}
|
||||
|
||||
modalRef.value?.closeModal();
|
||||
emit('added', createdStorage);
|
||||
if (submitlock.value) {
|
||||
submitlock.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
modalRef.value?.closeModal();
|
||||
};
|
||||
</script>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div
|
||||
:class="[
|
||||
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
|
||||
@ -15,15 +15,15 @@
|
||||
/>
|
||||
<CubeIcon :class="iconClasses" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="-mt-1 flex flex-col">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:space-x-1">
|
||||
<span :class="`text-${size} font-bold`">{{
|
||||
storedItem.item.displayName
|
||||
}}</span>
|
||||
<span
|
||||
:class="`mt-0.5 ${fontSize} text-gray-500`"
|
||||
:class="`md:mt-0.5 ${fontSize} text-gray-500`"
|
||||
v-if="storedItem.addedBy"
|
||||
>· Added
|
||||
><span class="hidden md:inline">· </span>Added
|
||||
{{
|
||||
dateToLocaleString(storedItem.acquiredAt || storedItem.createdAt)
|
||||
}}
|
||||
@ -96,13 +96,13 @@ const props = defineProps<{
|
||||
const imageClasses = computed(() => {
|
||||
if (props.size === 'sm') return 'h-8 w-8';
|
||||
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(() => {
|
||||
if (props.size === 'sm') return 'h-4 w-4';
|
||||
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(() => {
|
||||
|
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 Login from '../views/Login.vue';
|
||||
import HousePlanner from '../views/HousePlanner.vue';
|
||||
import BuildingViewBase from '../views/building/BuildingViewBase.vue';
|
||||
import BuildingView from '../views/building/BuildingView.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 Demo from '../views/Demo.vue';
|
||||
|
||||
@ -73,7 +73,7 @@ const routes: RouteRecordRaw[] = [
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
@ -20,9 +20,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<PageHead class="mt-4"
|
||||
><h2 class="text-xl font-bold">Buildings</h2></PageHead
|
||||
>
|
||||
<PageHead class="mt-4">
|
||||
<h2 class="text-xl font-bold">Buildings</h2>
|
||||
</PageHead>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
||||
|
@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<PageHead with-actions>
|
||||
<template #actions>
|
||||
<RouterLink
|
||||
class="px-2 py-2"
|
||||
:to="{ name: 'planner', query: { buildingId: building?.id } }"
|
||||
>Edit floor plans</RouterLink
|
||||
>
|
||||
</template>
|
||||
<PageHead bordered>
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-2xl font-bold">{{ building?.displayName }}</h1>
|
||||
<span class="text-sm font-light text-gray-800 line-clamp-1">{{
|
||||
building?.address
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<template #default>
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-2xl font-bold">{{ building?.displayName }}</h1>
|
||||
<span class="text-sm font-light text-gray-800 line-clamp-1">{{
|
||||
building?.address
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<Menu
|
||||
title="Actions"
|
||||
:options="[
|
||||
{
|
||||
title: 'Edit floor plans',
|
||||
link: { name: 'planner', query: { buildingId: building?.id } },
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</PageHead>
|
||||
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
@ -38,6 +38,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import Menu from '../../components/menu/Menu.vue';
|
||||
import PageHead from '../../components/PageHead.vue';
|
||||
import { useBuildingStore } from '../../store/building.store';
|
||||
import FloorListItem from './floors/FloorListItem.vue';
|
||||
|
@ -1,34 +1,35 @@
|
||||
<template>
|
||||
<PageHead with-actions>
|
||||
<template #actions>
|
||||
<RouterLink
|
||||
class="px-2 py-2"
|
||||
:to="{
|
||||
name: 'planner',
|
||||
query: { buildingId: building?.id, floorId: floor?.id },
|
||||
}"
|
||||
>Edit floor plan</RouterLink
|
||||
<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
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #default>
|
||||
<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>
|
||||
</template>
|
||||
<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 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" />
|
||||
<ChevronDownIcon v-else class="h-4 w-4" />
|
||||
</button>
|
||||
@ -83,15 +84,38 @@
|
||||
@mouseenter="() => (hoveredBubble = storage)"
|
||||
@start-moving="moveBubble(storage)"
|
||||
>
|
||||
<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 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:
|
||||
@ -110,6 +134,7 @@
|
||||
</Transition>
|
||||
|
||||
<ItemSelector
|
||||
:class="{ 'mt-2': !showMap }"
|
||||
:rooms="rooms"
|
||||
:storages="storages"
|
||||
:selected-room="selectedRoom"
|
||||
@ -118,21 +143,26 @@
|
||||
@select-room="selectRoomFromList"
|
||||
@select-storage="selectStorage"
|
||||
@new-storage="addNewStorage"
|
||||
@add-item="newItem?.openModal(building!, selectedStorage!)"
|
||||
/>
|
||||
|
||||
<StoredItemCard
|
||||
v-for="stored of selectedStorage?.items || []"
|
||||
size="lg"
|
||||
:stored-item="stored"
|
||||
<StorageView
|
||||
v-if="selectedStorage && building"
|
||||
ref="storageViewRef"
|
||||
@storage-added="newStoredItemAdded"
|
||||
:storage="selectedStorage"
|
||||
:building="building"
|
||||
/>
|
||||
|
||||
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
||||
<NewItemModal ref="newItem" @added="newStoredItemAdded" />
|
||||
</template>
|
||||
|
||||
<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 PageHead from '../../../components/PageHead.vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
@ -158,10 +188,11 @@ 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 './ItemSelector.vue';
|
||||
import ItemSelector from './StorageSelector.vue';
|
||||
import StoredItemCard from '../../../components/item/StoredItemCard.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 buildingStore = useBuildingStore();
|
||||
@ -178,7 +209,7 @@ const selectedSet = ref<StorageSetListItem>();
|
||||
const selectedStorage = ref<StorageListItem>();
|
||||
|
||||
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
|
||||
const newItem = ref<InstanceType<typeof NewItemModal>>();
|
||||
const storageViewRef = ref<InstanceType<typeof StorageView>>();
|
||||
|
||||
const floor = computed(() =>
|
||||
building.value?.floors.find(
|
||||
@ -250,7 +281,7 @@ const selectRoomFromList = (room: RoomLayoutObject) => {
|
||||
selectedRoom.value = room;
|
||||
};
|
||||
|
||||
const selectStorage = (
|
||||
const selectStorage = async (
|
||||
storage: StorageSharedType,
|
||||
parentSet?: StorageSetListItem
|
||||
) => {
|
||||
@ -262,7 +293,7 @@ const selectStorage = (
|
||||
|
||||
selectedSet.value = parentSet;
|
||||
selectedStorage.value = storage as StorageListItem;
|
||||
jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
||||
await jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
||||
headers: authHeader.value,
|
||||
}).then(({ data: storage }) => {
|
||||
selectedStorage.value = storage;
|
||||
@ -333,4 +364,9 @@ const newStoredItemAdded = async (newItem: StoredItem) => {
|
||||
];
|
||||
selectedStorage.value.itemCount = selectedStorage.value.items.length;
|
||||
};
|
||||
|
||||
const selectAndAdd = async (storage: StorageSharedType) => {
|
||||
await selectStorage(storage);
|
||||
storageViewRef.value?.addNewItem();
|
||||
};
|
||||
</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 = {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
borderWidth: {
|
||||
1: '1px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user