updates, i18n start
This commit is contained in:
parent
8984ddd9dc
commit
8b2e2dd0aa
1651
package-lock.json
generated
1651
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -9,37 +9,39 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@headlessui/vue": "^1.7.16",
|
||||
"@heroicons/vue": "^2.0.18",
|
||||
"@vuepic/vue-datepicker": "^5.1.2",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vuepic/vue-datepicker": "^7.2.2",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"pinia": "^2.1.3",
|
||||
"sass": "^1.62.1",
|
||||
"vue": "^3.3.4",
|
||||
"pinia": "^2.1.7",
|
||||
"sass": "^1.69.5",
|
||||
"vue": "^3.3.8",
|
||||
"vue-i18n": "^9.6.5",
|
||||
"vue-material-design-icons": "^5.2.0",
|
||||
"vue-router": "^4.2.1"
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@types/lodash.get": "^4.4.7",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/lodash.pick": "^4.4.7",
|
||||
"@types/lodash.set": "^4.3.7",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.8",
|
||||
"vue-tsc": "^1.6.5"
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.omit": "^4.5.9",
|
||||
"@types/lodash.pick": "^4.4.9",
|
||||
"@types/lodash.set": "^4.3.9",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
:step="step"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:autocomplete="autocomplete"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@focus="onFocus"
|
||||
@ -73,13 +74,14 @@ const props = withDefaults(
|
||||
step?: string;
|
||||
min?: string;
|
||||
max?: string;
|
||||
autocomplete?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
required: false,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -96,11 +98,11 @@ const registerField = inject<(field: string) => void>('registerField');
|
||||
const unregisterField = inject<(field: string) => void>('unregisterField');
|
||||
|
||||
const fieldName = computed(() =>
|
||||
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
||||
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name,
|
||||
);
|
||||
|
||||
const forId = computed(
|
||||
() => `${props.forIdPrefix || 'form'}-${fieldName.value}`
|
||||
() => `${props.forIdPrefix || 'form'}-${fieldName.value}`,
|
||||
);
|
||||
const value = computed(() => get(formData?.value, fieldName.value));
|
||||
const errors = computed(() => formErrors?.value[fieldName.value] || []);
|
||||
|
@ -11,7 +11,6 @@
|
||||
v-bind="props"
|
||||
:class="[invalid ? 'dp__invalid' : '']"
|
||||
:uid="fieldFQN"
|
||||
:placeholder="placeholder"
|
||||
:state="undefined"
|
||||
:format="format"
|
||||
:model-value="(value as string)"
|
||||
|
@ -11,7 +11,7 @@
|
||||
<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-blue-500"
|
||||
>
|
||||
<span class="sr-only">Open menu</span>
|
||||
<span class="sr-only">{{ $t('common.openMenu') }}</span>
|
||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||
</PopoverButton>
|
||||
</div>
|
||||
@ -23,7 +23,7 @@
|
||||
'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>{{ $t('common.buildings') }}</span>
|
||||
<ChevronDownIcon
|
||||
:class="[
|
||||
open ? 'text-gray-600' : 'text-gray-400',
|
||||
@ -47,7 +47,7 @@
|
||||
<div
|
||||
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="relative bg-white pt-4 pb-4 sm:gap-5">
|
||||
<div class="relative bg-white pb-4 pt-4 sm:gap-5">
|
||||
<template v-for="building of buildings">
|
||||
<Building :building="building" />
|
||||
</template>
|
||||
@ -60,7 +60,7 @@
|
||||
<a
|
||||
href="#"
|
||||
class="text-base font-medium text-gray-500 hover:text-gray-900"
|
||||
>Groups</a
|
||||
>{{ $t('common.groups') }}</a
|
||||
>
|
||||
</PopoverGroup>
|
||||
<div class="hidden items-center justify-end md:flex md:flex-1 lg:w-0">
|
||||
@ -84,7 +84,7 @@
|
||||
<div
|
||||
class="divide-y-2 divide-gray-50 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="px-5 pt-5 pb-6">
|
||||
<div class="px-5 pb-6 pt-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<router-link to="/"><HomeIcon class="h-8 w-8" /></router-link>
|
||||
@ -93,13 +93,15 @@
|
||||
<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-blue-500"
|
||||
>
|
||||
<span class="sr-only">Close menu</span>
|
||||
<span class="sr-only">{{ $t('common.closeMenu') }}</span>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
</PopoverButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<span class="text-lg font-bold">Buildings</span>
|
||||
<span class="text-lg font-bold">{{
|
||||
$t('common.buildings')
|
||||
}}</span>
|
||||
<div class="-mx-5">
|
||||
<template v-for="building of buildings">
|
||||
<Building :building="building" />
|
||||
@ -107,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6 py-6 px-5">
|
||||
<div class="space-y-6 px-5 py-6">
|
||||
<div class="flex justify-center">
|
||||
<UserPill :user="user" />
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Menu title="User" :options="[{ title: 'Logout' }]">
|
||||
<Menu :title="$t('common.user')" :options="[{ title: $t('login.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"
|
||||
@ -22,7 +22,7 @@ import { ChevronDownIcon, UserIcon } from '@heroicons/vue/24/outline';
|
||||
import { User } from '../../interfaces/user.interfaces';
|
||||
import Menu from '../menu/Menu.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
user: User;
|
||||
}>();
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 left-0 z-10 w-screen sm:w-auto"
|
||||
class="pointer-events-none absolute left-0 top-0 z-10 w-screen sm:w-auto"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition-max-height ease-out duration-200 overflow-hidden"
|
||||
@ -16,12 +16,12 @@
|
||||
>
|
||||
<div class="h-14 px-2 py-2.5">
|
||||
<div class="flex flex-row items-center space-x-4 px-4">
|
||||
<button @click="router.go(-1)" title="Go back">
|
||||
<span class="sr-only">Go back</span
|
||||
<button @click="router.go(-1)" :title="$t('common.back')">
|
||||
<span class="sr-only">{{ $t('common.back') }}</span
|
||||
><ChevronLeftIcon class="-ml-4 h-6 w-6" />
|
||||
</button>
|
||||
<div class="flex flex-row items-center space-x-4">
|
||||
<label for="building">Building:</label>
|
||||
<label for="building">{{ $t('common.building') }}:</label>
|
||||
<select
|
||||
id="building"
|
||||
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
|
||||
@ -29,7 +29,7 @@
|
||||
@change="
|
||||
emit(
|
||||
'update:selectedBuildingId',
|
||||
Number(($event.target as HTMLSelectElement).value)
|
||||
Number(($event.target as HTMLSelectElement).value),
|
||||
)
|
||||
"
|
||||
>
|
||||
@ -42,7 +42,7 @@
|
||||
class="flex flex-row items-center space-x-4"
|
||||
v-if="selectedBuildingId"
|
||||
>
|
||||
<label for="floor">Floor:</label>
|
||||
<label for="floor">{{ $t('common.floor') }}:</label>
|
||||
<select
|
||||
id="floor"
|
||||
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
|
||||
@ -50,7 +50,7 @@
|
||||
@change="
|
||||
emit(
|
||||
'update:selectedFloorId',
|
||||
Number(($event.target as HTMLSelectElement).value)
|
||||
Number(($event.target as HTMLSelectElement).value),
|
||||
)
|
||||
"
|
||||
>
|
||||
@ -59,7 +59,9 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="selectedFloorId">{{ status }}</div>
|
||||
<div v-if="selectedFloorId">
|
||||
{{ $t('planner.status.' + status) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,7 +69,7 @@
|
||||
<button
|
||||
:class="[
|
||||
'bg-white',
|
||||
'pointer-events-auto -mt-0.5 ml-2 h-8 rounded-br-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||
'pointer-events-auto -mt-0.5 ml-2 h-8 rounded-bl-md rounded-br-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||
]"
|
||||
:title="`${open ? 'Hide' : 'Open'} panel`"
|
||||
:aria-expanded="open"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<PlannerSidebar title="Layers">
|
||||
<PlannerSidebar :title="$t('planner.panel.layers.title')">
|
||||
<div class="bg-white-50 flex h-full flex-col overflow-auto">
|
||||
<template v-for="layer of layers">
|
||||
<button
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<PlannerSidebar title="Properties">
|
||||
<PlannerSidebar :title="$t('planner.panel.properties.title')">
|
||||
<div
|
||||
class="bg-white-50 flex h-full flex-col overflow-auto"
|
||||
v-if="selectedObject && applicableProperties"
|
||||
@ -24,6 +24,9 @@ import {
|
||||
} from './interfaces/properties.interfaces';
|
||||
import { computed } from 'vue';
|
||||
import PropertyFormItem from './PropertyFormItem.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
layers: Layer[];
|
||||
@ -35,44 +38,64 @@ const emit = defineEmits<{
|
||||
layerId: number,
|
||||
objectId: number,
|
||||
key: string,
|
||||
value: unknown
|
||||
value: unknown,
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const commonProps: ObjectProperty[] = [
|
||||
{ key: 'name', title: 'Name', type: 'string' },
|
||||
{ key: 'visible', title: 'Visible', type: 'boolean', groupable: true },
|
||||
{ key: 'name', title: t('planner.properties.name'), type: 'string' },
|
||||
{
|
||||
key: 'visible',
|
||||
title: t('planner.properties.visible'),
|
||||
type: 'boolean',
|
||||
groupable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const lineProps: ObjectProperty[] = [
|
||||
...commonProps,
|
||||
{ key: 'width', title: 'Line Width', type: 'number', groupable: true },
|
||||
{ key: 'color', title: 'Color', type: 'color', groupable: true },
|
||||
{
|
||||
key: 'width',
|
||||
title: t('planner.properties.width'),
|
||||
type: 'number',
|
||||
groupable: true,
|
||||
},
|
||||
{
|
||||
key: 'color',
|
||||
title: t('planner.properties.color'),
|
||||
type: 'color',
|
||||
groupable: true,
|
||||
},
|
||||
{
|
||||
key: 'lineCap',
|
||||
title: 'Line Cap Style',
|
||||
title: t('planner.properties.lineCap'),
|
||||
type: 'select',
|
||||
groupable: true,
|
||||
options: [
|
||||
{ value: undefined, title: '' },
|
||||
{ value: 'butt', title: 'Butt' },
|
||||
{ value: 'round', title: 'Round' },
|
||||
{ value: 'square', title: 'Square' },
|
||||
{ value: 'butt', title: t('planner.properties.butt') },
|
||||
{ value: 'round', title: t('planner.properties.round') },
|
||||
{ value: 'square', title: t('planner.properties.square') },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'lineJoin',
|
||||
title: 'Line Join Style',
|
||||
title: t('planner.properties.lineJoin'),
|
||||
type: 'select',
|
||||
groupable: true,
|
||||
options: [
|
||||
{ value: undefined, title: '' },
|
||||
{ value: 'miter', title: 'Miter' },
|
||||
{ value: 'bevel', title: 'Bevel' },
|
||||
{ value: 'round', title: 'Round' },
|
||||
{ value: 'miter', title: t('planner.properties.miter') },
|
||||
{ value: 'bevel', title: t('planner.properties.bevel') },
|
||||
{ value: 'round', title: t('planner.properties.round') },
|
||||
],
|
||||
},
|
||||
{ key: 'closed', title: 'Closed', type: 'boolean', groupable: true },
|
||||
{
|
||||
key: 'closed',
|
||||
title: t('planner.properties.closed'),
|
||||
type: 'boolean',
|
||||
groupable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const objectTypeProperties: ObjectProperties[] = [
|
||||
@ -95,20 +118,20 @@ const objectTypeProperties: ObjectProperties[] = [
|
||||
];
|
||||
|
||||
const currentLayer = computed(() =>
|
||||
props.layers.find((layer) => layer.active && layer.visible)
|
||||
props.layers.find((layer) => layer.active && layer.visible),
|
||||
);
|
||||
|
||||
// TODO multi edit
|
||||
const selectedObject = computed(
|
||||
() => currentLayer.value?.contents?.filter((obj) => obj.selected)[0]
|
||||
() => currentLayer.value?.contents?.filter((obj) => obj.selected)[0],
|
||||
);
|
||||
|
||||
const applicableProperties = computed(
|
||||
() =>
|
||||
selectedObject.value &&
|
||||
objectTypeProperties.find(
|
||||
(prop) => prop.type === selectedObject.value?.type
|
||||
)
|
||||
(prop) => prop.type === selectedObject.value?.type,
|
||||
),
|
||||
);
|
||||
|
||||
const updateProp = (prop: ObjectProperty, value: unknown) => {
|
||||
@ -119,7 +142,7 @@ const updateProp = (prop: ObjectProperty, value: unknown) => {
|
||||
currentLayer.value!.id,
|
||||
selectedObject.value!.id,
|
||||
prop.key,
|
||||
value
|
||||
value,
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
@ -3,13 +3,18 @@
|
||||
<button
|
||||
:class="[
|
||||
open ? 'bg-gray-200' : 'bg-white',
|
||||
'h-8 rounded-tl-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||
'h-8 rounded-bl-md rounded-tl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||
]"
|
||||
:title="`${open ? 'Hide' : 'Open'} panel`"
|
||||
:title="`${$t(open ? 'common.hide' : 'common.open')} ${$t(
|
||||
'planner.panel.open',
|
||||
)}`"
|
||||
:aria-expanded="open"
|
||||
@click="() => (open = !open)"
|
||||
>
|
||||
<span class="sr-only">{{ open ? 'Hide' : 'Open' }} panel</span>
|
||||
<span class="sr-only"
|
||||
>{{ $t(open ? 'common.hide' : 'common.open') }}
|
||||
{{ $t('planner.panel.open') }}</span
|
||||
>
|
||||
<ChevronDoubleRightIcon class="h-4 w-4" v-if="open" />
|
||||
<ChevronDoubleLeftIcon class="h-4 w-4" v-else />
|
||||
</button>
|
||||
|
12
src/i18n.ts
Normal file
12
src/i18n.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import en from '@/locales/en.json';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
globalInjection: true,
|
||||
legacy: false,
|
||||
messages: { en },
|
||||
});
|
||||
|
||||
export default i18n;
|
86
src/locales/en.json
Normal file
86
src/locales/en.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in to your account",
|
||||
"submit": "Sign in",
|
||||
"email": "Email address",
|
||||
"password": "Password",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"common": {
|
||||
"user": "User",
|
||||
"users": "Users",
|
||||
"actions": "Actions",
|
||||
"back": "Go back",
|
||||
"building": "Building",
|
||||
"buildings": "Buildings",
|
||||
"group": "Group",
|
||||
"groups": "Groups",
|
||||
"floor": "Floor",
|
||||
"floors": "Floors",
|
||||
"hide": "Hide",
|
||||
"show": "Show",
|
||||
"open": "Open",
|
||||
"close": "Close",
|
||||
"storage": "Storage",
|
||||
"storages": "Storages",
|
||||
"set": "Set",
|
||||
"storageSet": "Storage set",
|
||||
"openMenu": "Open menu",
|
||||
"closeMenu": "Close menu"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"expiringSoon": "Expiring soon",
|
||||
"intro": "Hello, {name}"
|
||||
},
|
||||
"buildings": {
|
||||
"editPlans": "Edit floor plans",
|
||||
"editPlan": "Edit floor plan",
|
||||
"floorIn": "Floor {floor} in {building}",
|
||||
"floor": "Floor {floor}",
|
||||
"interactive": "interactive floor plan"
|
||||
},
|
||||
"planner": {
|
||||
"choose": "Choose a floor to edit from the top left",
|
||||
"status": {
|
||||
"modified": "Modified",
|
||||
"noChanges": "No changes",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved!",
|
||||
"failed": "Failed to save!"
|
||||
},
|
||||
"panel": {
|
||||
"open": "panel",
|
||||
"layers": {
|
||||
"title": "Layers"
|
||||
},
|
||||
"properties": {
|
||||
"title": "Properties"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"name": "Name",
|
||||
"visible": "Visible",
|
||||
"width": "Line Width",
|
||||
"color": "Color",
|
||||
"lineCap": "Line Cap Style",
|
||||
"butt": "Butt",
|
||||
"round": "Round",
|
||||
"square": "Square",
|
||||
"lineJoin": "Line Join Style",
|
||||
"miter": "Miter",
|
||||
"bevel": "Bevel",
|
||||
"closed": "Closed"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"selectRoom": "Select Room",
|
||||
"selectStorageSet": "Select Storage / Set",
|
||||
"selectStorage": "Select Storage",
|
||||
"move": "Move storage",
|
||||
"set": "set",
|
||||
"add": "Add Storage",
|
||||
"addSet": "Add Storage Set",
|
||||
"storedItems": "Stored items"
|
||||
}
|
||||
}
|
@ -3,11 +3,13 @@ import './style.scss';
|
||||
import App from './App.vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import router from './router';
|
||||
import i18n from './i18n';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(i18n);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { User } from '../interfaces/user.interfaces';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { BACKEND_URL } from '../constants';
|
||||
import jfetch from '../utils/jfetch';
|
||||
|
||||
@ -53,7 +53,7 @@ export const useUserStore = defineStore('user', {
|
||||
Authorization: `Basic ${header}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.accessToken = tokenResponse.access_token;
|
||||
|
@ -1,12 +1,16 @@
|
||||
<template>
|
||||
<StandardLayout>
|
||||
<PageHead bordered><h1 class="text-2xl font-bold">Dashboard</h1></PageHead>
|
||||
<PageHead bordered
|
||||
><h1 class="text-2xl font-bold">{{ $t('dashboard.title') }}</h1></PageHead
|
||||
>
|
||||
|
||||
<p>Hello, {{ user.name }}</p>
|
||||
<p>{{ $t('dashboard.intro', { name: user.name }) }}</p>
|
||||
|
||||
<template v-if="expiringItems?.length">
|
||||
<PageHead class="mt-4"
|
||||
><h2 class="text-xl font-bold">Expiring soon</h2></PageHead
|
||||
><h2 class="text-xl font-bold">
|
||||
{{ $t('dashboard.expiringSoon') }}
|
||||
</h2></PageHead
|
||||
>
|
||||
|
||||
<div
|
||||
@ -21,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<PageHead class="mt-4">
|
||||
<h2 class="text-xl font-bold">Buildings</h2>
|
||||
<h2 class="text-xl font-bold">{{ $t('common.buildings') }}</h2>
|
||||
</PageHead>
|
||||
|
||||
<div
|
||||
|
@ -20,15 +20,15 @@
|
||||
(layers, rooms, boundingBox) =>
|
||||
updateDocument(layers, rooms, boundingBox)
|
||||
"
|
||||
@edited="status = 'Modified'"
|
||||
@edited="status = 'modified'"
|
||||
/>
|
||||
<div
|
||||
class="flex h-full w-full select-none items-center justify-center text-lg"
|
||||
v-else
|
||||
>
|
||||
<span class="font-bold uppercase text-gray-300"
|
||||
>Choose a floor to edit from the top left</span
|
||||
>
|
||||
<span class="font-bold uppercase text-gray-300">{{
|
||||
$t('planner.choose')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -52,13 +52,13 @@ const route = useRoute();
|
||||
const { buildings, floors } = storeToRefs(building);
|
||||
const selectedBuildingId = ref<number>();
|
||||
const selectedFloorId = ref<number>();
|
||||
const status = ref('No changes');
|
||||
const status = ref('noChanges');
|
||||
const plannerRef = ref<InstanceType<typeof HousePlanner>>();
|
||||
|
||||
const currentFloor = computed(
|
||||
() =>
|
||||
selectedFloorId.value &&
|
||||
floors.value.find((floor) => floor.id === selectedFloorId.value)
|
||||
floors.value.find((floor) => floor.id === selectedFloorId.value),
|
||||
);
|
||||
const floorPlan = computed(
|
||||
() =>
|
||||
@ -67,7 +67,7 @@ const floorPlan = computed(
|
||||
...defaultRoomData,
|
||||
...JSON.parse(currentFloor.value.plan || '{}'),
|
||||
id: selectedFloorId.value,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const buildingSelected = async (id: number) => {
|
||||
@ -99,7 +99,7 @@ const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
|
||||
const createdRooms = await building.upsertFloorRooms(
|
||||
selectedBuildingId.value,
|
||||
currentFloor.value.number,
|
||||
extractedRooms
|
||||
extractedRooms,
|
||||
);
|
||||
|
||||
if (createdRooms?.length) {
|
||||
@ -122,25 +122,25 @@ const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
|
||||
const updateDocument = async (
|
||||
data: FloorDocument,
|
||||
rooms: Line[],
|
||||
boundingBox?: Vec2Box
|
||||
boundingBox?: Vec2Box,
|
||||
) => {
|
||||
if (
|
||||
!selectedBuildingId.value ||
|
||||
!selectedFloorId.value ||
|
||||
selectedFloorId.value !== data.id ||
|
||||
!currentFloor.value ||
|
||||
status.value === 'Saving...'
|
||||
status.value === 'saving'
|
||||
)
|
||||
return;
|
||||
|
||||
status.value = 'Saving...';
|
||||
status.value = 'saving';
|
||||
try {
|
||||
data = await updateRooms(data, rooms);
|
||||
|
||||
// Prevent useless requests
|
||||
const floorPlan = JSON.stringify({ ...data, boundingBox });
|
||||
if (currentFloor.value.plan === floorPlan) {
|
||||
status.value = 'Saved!';
|
||||
status.value = 'saved';
|
||||
return;
|
||||
}
|
||||
|
||||
@ -149,12 +149,12 @@ const updateDocument = async (
|
||||
currentFloor.value.number,
|
||||
{
|
||||
plan: floorPlan,
|
||||
}
|
||||
},
|
||||
);
|
||||
status.value = 'Saved!';
|
||||
status.value = 'saved';
|
||||
} catch (e) {
|
||||
console.error(`Failed to save floor document: ${(e as Error).stack}`);
|
||||
status.value = 'Failed to save!';
|
||||
status.value = 'failed';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex min-h-full items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"
|
||||
class="flex min-h-full items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<h2
|
||||
class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900"
|
||||
>
|
||||
Sign in to your account
|
||||
{{ $t('login.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<Transition
|
||||
@ -33,12 +33,22 @@
|
||||
<div>
|
||||
<div class="bg-white px-4 py-5 sm:p-6">
|
||||
<div class="space-y-5">
|
||||
<FormField name="email" label="Email address" type="email" />
|
||||
<FormField name="password" label="Password" type="password" />
|
||||
<FormField
|
||||
name="email"
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<FormField
|
||||
name="password"
|
||||
:label="$t('login.password')"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 text-right sm:px-6">
|
||||
<Button button-type="submit">Sign in</Button>
|
||||
<Button button-type="submit">{{ $t('login.submit') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -3,17 +3,17 @@
|
||||
<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">{{
|
||||
<span class="line-clamp-1 text-sm font-light text-gray-800">{{
|
||||
building?.address
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
title="Actions"
|
||||
:title="$t('common.actions')"
|
||||
class="self-end"
|
||||
:options="[
|
||||
{
|
||||
title: 'Edit floor plans',
|
||||
title: $t('buildings.editPlans'),
|
||||
link: { name: 'planner', query: { buildingId: building?.id } },
|
||||
},
|
||||
]"
|
||||
@ -21,7 +21,7 @@
|
||||
</PageHead>
|
||||
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold">Floors</h2>
|
||||
<h2 class="text-xl font-bold">{{ $t('common.floors') }}</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ImageBox
|
||||
:title="floor.displayName"
|
||||
:subtitle="`Floor ${floor.number}`"
|
||||
:subtitle="$t('buildings.floor', { floor: floor.number })"
|
||||
:route-link="{
|
||||
name: 'floor',
|
||||
params: { id: building, number: floor.number },
|
||||
|
@ -3,17 +3,20 @@
|
||||
<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
|
||||
>
|
||||
<span class="line-clamp-1 text-sm font-light text-gray-800">{{
|
||||
$t('buildings.floorIn', {
|
||||
floor: floor?.number,
|
||||
building: building?.displayName,
|
||||
})
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
title="Actions"
|
||||
:title="$t('common.actions')"
|
||||
class="self-end"
|
||||
:options="[
|
||||
{
|
||||
title: 'Edit floor plan',
|
||||
title: $t('buildings.editPlan'),
|
||||
link: {
|
||||
name: 'planner',
|
||||
query: { buildingId: building?.id, floorId: floor?.id },
|
||||
@ -31,7 +34,10 @@
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
]"
|
||||
>
|
||||
<span>{{ showMap ? 'Hide' : 'Show' }} interactive floor plan</span>
|
||||
<span
|
||||
>{{ $t(showMap ? 'common.hide' : 'common.show') }}
|
||||
{{ $t('buildings.interactive') }}</span
|
||||
>
|
||||
<ChevronUpIcon v-if="showMap" class="h-4 w-4" />
|
||||
<ChevronDownIcon v-else class="h-4 w-4" />
|
||||
</button>
|
||||
@ -95,7 +101,7 @@
|
||||
<span
|
||||
v-if="isSet(storage)"
|
||||
class="text-[0.75rem] font-bold uppercase text-gray-400"
|
||||
>(Storage Set)</span
|
||||
>({{ $t('common.storageSet') }})</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -124,11 +130,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm" v-if="!isSet(storage)"
|
||||
>Stored items:
|
||||
>{{ $t('storage.storedItems') }}:
|
||||
{{ (storage as StorageListItem).itemCount }}</span
|
||||
>
|
||||
<span class="text-sm" v-else
|
||||
>Storages:
|
||||
>{{ $t('common.storages') }}:
|
||||
{{ (storage as StorageSetListItem).storages.length }}</span
|
||||
>
|
||||
</StorageBubble>
|
||||
@ -218,14 +224,15 @@ const selectedStorage = ref<StorageListItem>();
|
||||
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
|
||||
const storageViewRef = ref<InstanceType<typeof StorageView>>();
|
||||
|
||||
const floor = computed(() =>
|
||||
const floor = computed(
|
||||
() =>
|
||||
building.value?.floors.find(
|
||||
(floor) => floor.number === Number(route.params.number)
|
||||
)
|
||||
(floor) => floor.number === Number(route.params.number),
|
||||
),
|
||||
);
|
||||
|
||||
const floorDocument = computed(
|
||||
() => floor.value?.plan && JSON.parse(floor.value.plan)
|
||||
() => floor.value?.plan && JSON.parse(floor.value.plan),
|
||||
);
|
||||
|
||||
const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => {
|
||||
@ -266,7 +273,7 @@ const setBubbleLocation = async (x: number, y: number) => {
|
||||
location: `${x},${y}`,
|
||||
},
|
||||
headers: authHeader.value,
|
||||
}
|
||||
},
|
||||
);
|
||||
movingBubble.value = undefined;
|
||||
};
|
||||
@ -276,7 +283,7 @@ const addNewStorage = (set: boolean | StorageSetListItem) => {
|
||||
newStorage.value?.openModal(
|
||||
building.value,
|
||||
selectedRoom.value as unknown as RoomListItem,
|
||||
set
|
||||
set,
|
||||
);
|
||||
};
|
||||
|
||||
@ -290,7 +297,7 @@ const selectRoomFromList = (room: RoomLayoutObject) => {
|
||||
|
||||
const selectStorage = async (
|
||||
storage: StorageSharedType,
|
||||
parentSet?: StorageSetListItem
|
||||
parentSet?: StorageSetListItem,
|
||||
) => {
|
||||
if (isSet(storage)) {
|
||||
selectedSet.value = storage as StorageSetListItem;
|
||||
@ -325,7 +332,7 @@ const rooms = computed<RoomLayoutObject[]>(
|
||||
storages: [],
|
||||
} as RoomLayoutObject;
|
||||
})) ||
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const getRoomStorages = async (room: RoomLayoutObject) => {
|
||||
@ -335,13 +342,13 @@ const getRoomStorages = async (room: RoomLayoutObject) => {
|
||||
`${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`,
|
||||
{
|
||||
headers: authHeader.value,
|
||||
}
|
||||
},
|
||||
);
|
||||
const { data: setList } = await jfetch(
|
||||
`${BACKEND_URL}/storage/set/room/${room.id}`,
|
||||
{
|
||||
headers: authHeader.value,
|
||||
}
|
||||
},
|
||||
);
|
||||
storages.value = [...storageList, ...setList];
|
||||
} catch (e) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
@click.stop
|
||||
class="custom-storage absolute z-10"
|
||||
:style="getStoragePosition(storage)"
|
||||
>
|
||||
@ -13,11 +14,11 @@
|
||||
class="absolute -bottom-2 left-1/2 h-0 w-0 -translate-x-1/2 border-x-8 border-t-[16px] border-x-transparent border-t-white"
|
||||
></div>
|
||||
<button
|
||||
class="absolute left-1/2 bottom-0.5 -translate-x-1/2 rounded-full bg-gray-50 px-1 py-1 ring-1 ring-black ring-opacity-5"
|
||||
class="absolute bottom-0.5 left-1/2 -translate-x-1/2 rounded-full bg-gray-50 px-1 py-1 ring-1 ring-black ring-opacity-5"
|
||||
@click.stop="emit('startMoving')"
|
||||
title="Move storage"
|
||||
:title="$t('storage.move')"
|
||||
>
|
||||
<span class="sr-only">Move storage</span>
|
||||
<span class="sr-only">{{ $t('storage.move') }}</span>
|
||||
<ArrowsPointingOutIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div
|
||||
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
|
||||
>
|
||||
Select Room
|
||||
{{ $t('storage.selectRoom') }}
|
||||
</div>
|
||||
<button
|
||||
v-for="room of rooms"
|
||||
@ -20,7 +20,7 @@
|
||||
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',
|
||||
'flex items-center justify-between border-b-1 border-gray-100 px-2 py-2',
|
||||
]"
|
||||
>
|
||||
<span>{{ room.displayName }}</span>
|
||||
@ -37,7 +37,7 @@
|
||||
<div
|
||||
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
|
||||
>
|
||||
Select Storage / Set
|
||||
{{ $t('storage.selectStorageSet') }}
|
||||
</div>
|
||||
<button
|
||||
v-for="storage of storages"
|
||||
@ -47,7 +47,7 @@
|
||||
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',
|
||||
'flex items-center justify-between border-b-1 border-gray-100 px-2 py-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
@ -57,7 +57,7 @@
|
||||
>({{ (storage as StorageListItem).itemCount }})</span
|
||||
>
|
||||
<span v-else>
|
||||
(set) ({{
|
||||
({{ $t('storage.set') }}) ({{
|
||||
(storage as StorageSetListItem).storages.length
|
||||
}})</span
|
||||
>
|
||||
@ -70,14 +70,16 @@
|
||||
@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>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
<span>{{ $t('storage.add') }}</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>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
<span>{{ $t('storage.addSet') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,7 +94,7 @@
|
||||
<div
|
||||
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
|
||||
>
|
||||
Select Storage
|
||||
{{ $t('storage.selectStorage') }}
|
||||
</div>
|
||||
<button
|
||||
v-for="storage of selectedSet.storages"
|
||||
@ -102,7 +104,7 @@
|
||||
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',
|
||||
'flex items-center justify-between border-b-1 border-gray-100 px-2 py-2',
|
||||
]"
|
||||
>
|
||||
<span>{{ storage.displayName }} ({{ storage.itemCount }})</span>
|
||||
@ -114,7 +116,7 @@
|
||||
@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>
|
||||
<PlusIcon class="h-4 w-4" /> <span>{{ $t('storage.add') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -153,7 +155,7 @@ const emit = defineEmits<{
|
||||
(
|
||||
e: 'selectStorage',
|
||||
storage: StorageSharedType,
|
||||
parentSet?: StorageSetListItem
|
||||
parentSet?: StorageSetListItem,
|
||||
): void;
|
||||
(e: 'selectRoom', room: RoomLayoutObject): void;
|
||||
(e: 'newStorage', set: boolean | StorageSetListItem): void;
|
||||
|
@ -4,7 +4,7 @@
|
||||
<h2 class="text-2xl font-bold">{{ storage.displayName }}</h2>
|
||||
|
||||
<Menu
|
||||
title="Actions"
|
||||
:title="$t('common.actions')"
|
||||
class="self-end"
|
||||
:options="[
|
||||
{
|
||||
@ -28,7 +28,7 @@
|
||||
<template #actions>
|
||||
<Menu
|
||||
title=""
|
||||
button-class="px-0.5 py-0.5"
|
||||
button-class="!px-0.5 !py-0.5"
|
||||
:options="[
|
||||
{ title: 'Change details' },
|
||||
{ title: 'Add transaction' },
|
||||
|
@ -8,5 +8,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')],
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
|
@ -9,10 +9,27 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -4,4 +4,9 @@ import vue from '@vitejs/plugin-vue';
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user