additions
This commit is contained in:
parent
74ef2848e2
commit
b875c2465d
38
src/components/ImageBox.vue
Normal file
38
src/components/ImageBox.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<RouterLink :to="routeLink">
|
||||
<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" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
image?: string;
|
||||
title: string;
|
||||
routeLink: RouteLocationRaw;
|
||||
subtitle?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hoverbox {
|
||||
img {
|
||||
transition: all 3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="mb-8 flex items-center justify-between border-b-2 border-gray-100 pb-2"
|
||||
class="mb-2 flex items-center justify-between border-b-2 border-gray-100 pb-2"
|
||||
>
|
||||
<slot />
|
||||
<DropdownButton title="Actions" :position="'right'" v-if="withActions">
|
||||
|
@ -133,7 +133,8 @@ const inputClass = computed(() => {
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
|
||||
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
||||
'py-2 pl-3 pr-10 text-left cursor-default border-[1px] focus:ring-1',
|
||||
'sm:min-h-[38px] min-h-[42px]',
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -109,7 +109,8 @@ const inputClass = computed(() => {
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
|
||||
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
||||
'py-2 pl-3 pr-10 text-left cursor-default border-[1px] focus:ring-1',
|
||||
'sm:min-h-[38px] min-h-[42px]',
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -25,30 +25,25 @@
|
||||
label="Type"
|
||||
/>
|
||||
<FormField name="barcode" label="Barcode" />
|
||||
<FormField
|
||||
name="weight"
|
||||
type="number"
|
||||
step="0.001"
|
||||
label="Weight (g)"
|
||||
/>
|
||||
<FormField name="url" label="URL" />
|
||||
<FormField
|
||||
name="consumable"
|
||||
type="checkbox"
|
||||
label="Consumable / Food item"
|
||||
/>
|
||||
<FormField name="public" type="checkbox" label="Public item" />
|
||||
<div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2">
|
||||
<FormField
|
||||
name="weight"
|
||||
type="number"
|
||||
step="0.001"
|
||||
label="Weight (g)"
|
||||
/>
|
||||
<FormField
|
||||
name="consumable"
|
||||
type="checkbox"
|
||||
label="Consumable / Food item"
|
||||
/>
|
||||
<FormField name="public" type="checkbox" label="Public item" />
|
||||
</div>
|
||||
<FormTextareaField name="notes" label="Notes" />
|
||||
</template>
|
||||
|
||||
<FormGroup name="transactionInfo">
|
||||
<FormSelectField
|
||||
:options="itemTransactionTypesOptions"
|
||||
name="type"
|
||||
required
|
||||
label="Transaction type"
|
||||
:disabled="!item"
|
||||
/>
|
||||
<FormDateField
|
||||
name="actionAt"
|
||||
label="Action committed at"
|
||||
@ -56,13 +51,13 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-4 space-x-2">
|
||||
<div class="grid grid-cols-2 space-x-2 sm:grid-cols-4">
|
||||
<FormField
|
||||
name="price"
|
||||
label="Cost"
|
||||
type="number"
|
||||
step=".01"
|
||||
class="col-span-3"
|
||||
class="sm:col-span-3"
|
||||
:disabled="!item"
|
||||
/>
|
||||
<FormSelectField
|
||||
@ -81,7 +76,7 @@
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="additionalInfo">
|
||||
<div class="grid grid-cols-3 space-x-2">
|
||||
<div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2">
|
||||
<FormDateField
|
||||
name="expiresAt"
|
||||
label="Expires at"
|
||||
@ -147,6 +142,7 @@ import FormTextareaField from '../form/fields/FormTextareaField.vue';
|
||||
const defaults: any = {
|
||||
item: null,
|
||||
transactionInfo: {
|
||||
type: TransactionType.ACQUIRED,
|
||||
actionAt: new Date(),
|
||||
},
|
||||
additionalInfo: {
|
||||
@ -181,20 +177,6 @@ const itemTypesOptions = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const itemTransactionTypesOptions = computed(() => {
|
||||
return Object.keys(TransactionTypeName).reduce<SelectOption[]>(
|
||||
(list, key) => [
|
||||
...list,
|
||||
{
|
||||
value: key.toString(),
|
||||
name: TransactionTypeName[key as TransactionType],
|
||||
description: TransactionTypeDescription[key as TransactionType],
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
const validators = ref<FormValidator[]>([
|
||||
{
|
||||
field: 'displayName',
|
||||
@ -204,10 +186,6 @@ const validators = ref<FormValidator[]>([
|
||||
field: 'type',
|
||||
validators: [IsRequired()],
|
||||
},
|
||||
{
|
||||
field: 'transactionInfo.type',
|
||||
validators: [IsRequired()],
|
||||
},
|
||||
{
|
||||
field: 'transactionInfo.actionAt',
|
||||
validators: [IsRequired()],
|
||||
@ -227,8 +205,16 @@ const currencies = [
|
||||
|
||||
const searchForItems = async (search: string) => {
|
||||
if (!search || search.length < 3) return [];
|
||||
|
||||
const query = new URLSearchParams();
|
||||
if (search.startsWith('b:') && search.length > 3) {
|
||||
query.append('barcode', search.substring(2));
|
||||
} else {
|
||||
query.append('searchTerm', search);
|
||||
}
|
||||
|
||||
const { data: list } = await jfetch(
|
||||
`${BACKEND_URL}/storage/item?searchTerm=${search}`,
|
||||
`${BACKEND_URL}/storage/item?${query.toString()}`,
|
||||
{
|
||||
headers: authHeader.value,
|
||||
}
|
||||
@ -261,7 +247,11 @@ watch(
|
||||
|
||||
const startAddingNewItem = (insert: string) => {
|
||||
item.value = insert;
|
||||
data.value['displayName'] = insert;
|
||||
if (insert.startsWith('b:')) {
|
||||
data.value['barcode'] = insert.substring(2);
|
||||
} else {
|
||||
data.value['displayName'] = insert;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (form: FormSubmit) => {
|
||||
|
@ -20,7 +20,9 @@
|
||||
<span :class="`text-${size} font-bold`">{{
|
||||
storedItem.item.displayName
|
||||
}}</span>
|
||||
<span :class="`mt-0.5 ${fontSize} text-gray-500`"
|
||||
<span
|
||||
:class="`mt-0.5 ${fontSize} text-gray-500`"
|
||||
v-if="storedItem.addedBy"
|
||||
>· Added
|
||||
{{
|
||||
dateToLocaleString(storedItem.acquiredAt || storedItem.createdAt)
|
||||
|
@ -7,6 +7,7 @@ import BuildingView from '../views/building/BuildingView.vue';
|
||||
import FloorView from '../views/building/floors/FloorView.vue';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import { useUserStore } from '../store/user.store';
|
||||
import Demo from '../views/Demo.vue';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@ -24,6 +25,11 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/planner',
|
||||
component: HousePlanner,
|
||||
},
|
||||
{
|
||||
name: 'demo',
|
||||
path: '/demo',
|
||||
component: Demo,
|
||||
},
|
||||
{
|
||||
name: 'buildings',
|
||||
path: '/building/:id',
|
||||
|
@ -14,7 +14,9 @@ const { authHeader } = useAccessToken();
|
||||
export const useBuildingStore = defineStore('building', {
|
||||
state: () => {
|
||||
return {
|
||||
loadingBuildings: false,
|
||||
buildings: [] as BuildingListItem[],
|
||||
loadingFloors: false,
|
||||
floors: [] as FloorListItem[],
|
||||
building: null as null | BuildingResponse,
|
||||
};
|
||||
@ -30,19 +32,29 @@ export const useBuildingStore = defineStore('building', {
|
||||
this.building = building;
|
||||
},
|
||||
async getBuildings() {
|
||||
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
|
||||
headers: authHeader.value,
|
||||
});
|
||||
this.buildings = buildings;
|
||||
this.loadingBuildings = true;
|
||||
try {
|
||||
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
|
||||
headers: authHeader.value,
|
||||
});
|
||||
this.buildings = buildings;
|
||||
} finally {
|
||||
this.loadingBuildings = false;
|
||||
}
|
||||
},
|
||||
async getFloors(building: number) {
|
||||
const { data: floors } = await jfetch(
|
||||
`${BACKEND_URL}/buildings/${building}/floors`,
|
||||
{
|
||||
headers: authHeader.value,
|
||||
}
|
||||
);
|
||||
this.floors = floors;
|
||||
this.loadingFloors = true;
|
||||
try {
|
||||
const { data: floors } = await jfetch(
|
||||
`${BACKEND_URL}/buildings/${building}/floors`,
|
||||
{
|
||||
headers: authHeader.value,
|
||||
}
|
||||
);
|
||||
this.floors = floors;
|
||||
} finally {
|
||||
this.loadingFloors = false;
|
||||
}
|
||||
},
|
||||
async saveFloor(
|
||||
building: number,
|
||||
|
32
src/store/storage.store.ts
Normal file
32
src/store/storage.store.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAccessToken } from '../composables/useAccessToken';
|
||||
import { BACKEND_URL } from '../constants';
|
||||
import { StoredItem } from '../interfaces/storage.interfaces';
|
||||
import jfetch from '../utils/jfetch';
|
||||
|
||||
const { authHeader } = useAccessToken();
|
||||
export const useStorageStore = defineStore('storage', {
|
||||
state: () => {
|
||||
return {
|
||||
loadingExpiringItems: false,
|
||||
expiringItems: [] as StoredItem[],
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
async getExpiringItems() {
|
||||
this.loadingExpiringItems = true;
|
||||
this.expiringItems = [];
|
||||
try {
|
||||
const { data: items } = await jfetch(
|
||||
`${BACKEND_URL}/storage/expiring`,
|
||||
{
|
||||
headers: authHeader.value,
|
||||
}
|
||||
);
|
||||
this.expiringItems = items;
|
||||
} finally {
|
||||
this.loadingExpiringItems = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
@ -1,73 +1,57 @@
|
||||
<template>
|
||||
<StandardLayout>
|
||||
<h1>Dashboard</h1>
|
||||
<PageHead><h1 class="text-2xl font-bold">Dashboard</h1></PageHead>
|
||||
|
||||
<p>Hello, {{ user.name }}</p>
|
||||
|
||||
<Form @submit="test" :validators="validators">
|
||||
<FormDateField
|
||||
name="date"
|
||||
label="datepicker"
|
||||
locale="en-UK"
|
||||
clearable
|
||||
:enable-time-picker="false"
|
||||
/>
|
||||
<FormField name="test" label="test" />
|
||||
<FormAutocompleteField
|
||||
:search-fn="searchFn"
|
||||
name="autocomplete"
|
||||
label="autocomplete"
|
||||
/>
|
||||
<FormTextareaField name="textarea" label="textarea" rows="3" />
|
||||
<button type="submit">test</button>
|
||||
</Form>
|
||||
<template v-if="expiringItems?.length">
|
||||
<PageHead class="mt-4"
|
||||
><h2 class="text-xl font-bold">Expiring soon</h2></PageHead
|
||||
>
|
||||
|
||||
<div
|
||||
class="flex flex-nowrap gap-1 overflow-x-scroll px-1 py-1 md:overflow-hidden"
|
||||
>
|
||||
<StoredItemCard
|
||||
v-for="item of expiringItems"
|
||||
:stored-item="item"
|
||||
class="whitespace-nowrap rounded-md bg-white px-2 py-2 shadow-md ring-1 ring-black ring-opacity-5"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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"
|
||||
v-if="buildings?.length"
|
||||
>
|
||||
<BuildingListItem v-for="building of buildings" :building="building" />
|
||||
</div>
|
||||
</StandardLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useUserStore } from '../store/user.store';
|
||||
import StandardLayout from '../components/StandardLayout.vue';
|
||||
import FormDateField from '../components/form/fields/FormDateField.vue';
|
||||
import Form from '../components/form/Form.vue';
|
||||
import FormField from '../components/form/FormField.vue';
|
||||
import { IsRequired } from '../components/form/validators';
|
||||
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
|
||||
import FormTextareaField from '../components/form/fields/FormTextareaField.vue';
|
||||
import PageHead from '../components/PageHead.vue';
|
||||
import { useBuildingStore } from '../store/building.store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import BuildingListItem from './building/BuildingListItem.vue';
|
||||
import { useStorageStore } from '../store/storage.store';
|
||||
import StoredItemCard from '../components/item/StoredItemCard.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const storageStore = useStorageStore();
|
||||
const buildingStore = useBuildingStore();
|
||||
const { buildings, loadingBuildings } = storeToRefs(buildingStore);
|
||||
const { expiringItems, loadingExpiringItems } = storeToRefs(storageStore);
|
||||
const user = ref(userStore.user);
|
||||
|
||||
const test = console.log;
|
||||
const validators = [{ field: 'date', validators: [IsRequired()] }];
|
||||
|
||||
const autocompleteTest = [
|
||||
{
|
||||
id: 1,
|
||||
displayName: 'search things',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
displayName: 'potato',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
displayName: 'carrot',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
displayName: 'beet',
|
||||
},
|
||||
];
|
||||
|
||||
const searchFn = async (query: string) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
return query === ''
|
||||
? autocompleteTest
|
||||
: autocompleteTest.filter((person) =>
|
||||
person.displayName
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '')
|
||||
.includes(query.toLowerCase().replace(/\s+/g, ''))
|
||||
);
|
||||
};
|
||||
onMounted(() => {
|
||||
storageStore.getExpiringItems();
|
||||
});
|
||||
</script>
|
||||
|
65
src/views/Demo.vue
Normal file
65
src/views/Demo.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<StandardLayout>
|
||||
<Form @submit="test" :validators="validators">
|
||||
<FormDateField
|
||||
name="date"
|
||||
label="datepicker"
|
||||
locale="en-UK"
|
||||
clearable
|
||||
:enable-time-picker="false"
|
||||
/>
|
||||
<FormField name="test" label="test" />
|
||||
<FormAutocompleteField
|
||||
:search-fn="searchFn"
|
||||
name="autocomplete"
|
||||
label="autocomplete"
|
||||
/>
|
||||
<FormTextareaField name="textarea" label="textarea" rows="3" />
|
||||
<button type="submit">test</button>
|
||||
</Form>
|
||||
</StandardLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StandardLayout from '../components/StandardLayout.vue';
|
||||
import { IsRequired } from '../components/form/validators';
|
||||
import FormDateField from '../components/form/fields/FormDateField.vue';
|
||||
import Form from '../components/form/Form.vue';
|
||||
import FormField from '../components/form/FormField.vue';
|
||||
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
|
||||
import FormTextareaField from '../components/form/fields/FormTextareaField.vue';
|
||||
|
||||
const test = console.log;
|
||||
const validators = [{ field: 'date', validators: [IsRequired()] }];
|
||||
|
||||
const autocompleteTest = [
|
||||
{
|
||||
id: 1,
|
||||
displayName: 'search things',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
displayName: 'potato',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
displayName: 'carrot',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
displayName: 'beet',
|
||||
},
|
||||
];
|
||||
|
||||
const searchFn = async (query: string) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
return query === ''
|
||||
? autocompleteTest
|
||||
: autocompleteTest.filter((person) =>
|
||||
person.displayName
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '')
|
||||
.includes(query.toLowerCase().replace(/\s+/g, ''))
|
||||
);
|
||||
};
|
||||
</script>
|
16
src/views/building/BuildingListItem.vue
Normal file
16
src/views/building/BuildingListItem.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<ImageBox
|
||||
:title="building.displayName"
|
||||
:subtitle="building.address"
|
||||
:route-link="{ name: 'building', params: { id: building.id } }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ImageBox from '../../components/ImageBox.vue';
|
||||
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||
|
||||
const props = defineProps<{
|
||||
building: BuildingListItem;
|
||||
}>();
|
||||
</script>
|
@ -23,7 +23,10 @@
|
||||
<h2 class="text-xl font-bold">Floors</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3" v-if="building">
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
||||
v-if="building"
|
||||
>
|
||||
<FloorListItem
|
||||
:building="building!.id"
|
||||
v-for="floor of building?.floors || []"
|
||||
|
@ -1,24 +1,17 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
:to="{ name: 'floor', params: { id: building, number: floor.number } }"
|
||||
>
|
||||
<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
|
||||
:src="`${BACKEND_URL}/usercontent/${floor.planImage}`"
|
||||
v-if="floor.planImage"
|
||||
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">{{ floor.displayName }}</span>
|
||||
<span class="text-sm">Floor {{ floor.number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<ImageBox
|
||||
:title="floor.displayName"
|
||||
:subtitle="`Floor ${floor.number}`"
|
||||
:route-link="{
|
||||
name: 'floor',
|
||||
params: { id: building, number: floor.number },
|
||||
}"
|
||||
:image="floor.planImage && `${BACKEND_URL}/usercontent/${floor.planImage}`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ImageBox from '../../../components/ImageBox.vue';
|
||||
import { BACKEND_URL } from '../../../constants';
|
||||
import { FloorListItem } from '../../../interfaces/floor.interfaces';
|
||||
|
||||
@ -27,17 +20,3 @@ const props = defineProps<{
|
||||
floor: FloorListItem;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hoverbox {
|
||||
img {
|
||||
transition: all 3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user