updates, i18n start

This commit is contained in:
Evert Prants 2023-11-07 22:08:56 +02:00
parent 8984ddd9dc
commit 8b2e2dd0aa
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
26 changed files with 1214 additions and 900 deletions

1651
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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] || []);

View File

@ -11,7 +11,6 @@
v-bind="props"
:class="[invalid ? 'dp__invalid' : '']"
:uid="fieldFQN"
:placeholder="placeholder"
:state="undefined"
:format="format"
:model-value="(value as string)"

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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
View 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
View 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"
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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';
}
};

View File

@ -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>

View File

@ -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

View File

@ -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 },

View File

@ -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(() =>
building.value?.floors.find(
(floor) => floor.number === Number(route.params.number)
)
const floor = computed(
() =>
building.value?.floors.find(
(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) {

View File

@ -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>

View File

@ -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;

View File

@ -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' },

View File

@ -8,5 +8,5 @@ module.exports = {
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')],
plugins: [require('@tailwindcss/forms')],
};

View File

@ -9,10 +9,27 @@
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"lib": [
"ESNext",
"DOM"
],
"skipLibCheck": true,
"noEmit": true
"noEmit": true,
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -4,4 +4,9 @@ import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': '/src',
},
},
});