forked from mirrors/homebox
feat: QR Codes (#226)
* code gen updates * qrcode support * remove opacity on toast * update item view to use tab-like pages * adjust view for cards * fix old API calls for ioutils * move embed * extract QR code * add docs for QR codes * add QR code
This commit is contained in:
parent
f532b39c46
commit
c19fe94c08
88 changed files with 3151 additions and 6454 deletions
|
@ -4,7 +4,7 @@
|
|||
<div
|
||||
v-for="(notify, index) in notifications.slice(0, 4)"
|
||||
:key="notify.id"
|
||||
class="my-2 w-[300px] rounded-md p-3 text-sm text-white opacity-75"
|
||||
class="my-2 w-[300px] rounded-md p-3 text-sm text-white"
|
||||
:class="{
|
||||
'bg-primary': notify.type === 'info',
|
||||
'bg-red-600': notify.type === 'error',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<NuxtLink class="group card rounded-md" :to="`/item/${item.id}`">
|
||||
<div class="rounded-t flex flex-col bg-neutral text-neutral-content p-5">
|
||||
<h2 class="text-base mb-4 last:mb-0 font-bold two-line min-h-[48px]">{{ item.name }}</h2>
|
||||
<div class="rounded-t flex flex-col justify-center bg-neutral text-neutral-content p-5">
|
||||
<h2 class="text-base mb-2 last:mb-0 font-bold two-line">{{ item.name }}</h2>
|
||||
<NuxtLink
|
||||
v-if="item.location"
|
||||
class="inline-flex text-sm items-center hover:link"
|
||||
|
|
27
frontend/components/global/PageQRCode.vue
Normal file
27
frontend/components/global/PageQRCode.vue
Normal file
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="dropdown dropdown-left">
|
||||
<slot>
|
||||
<label tabindex="0" class="btn btn-circle btn-sm">
|
||||
<Icon name="mdi-qrcode" />
|
||||
</label>
|
||||
</slot>
|
||||
<div tabindex="0" class="card compact dropdown-content shadow-lg bg-base-100 rounded-box w-64">
|
||||
<div class="card-body">
|
||||
<h2 class="text-center">Page URL</h2>
|
||||
<img :src="getQRCodeUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const api = useUserApi();
|
||||
|
||||
function getQRCodeUrl(): string {
|
||||
const currentURL = window.location.href;
|
||||
|
||||
return `/api/v1/qrcode?data=${encodeURIComponent(currentURL)}&access_token=${api.items.attachmentToken}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -96,7 +96,7 @@ export interface ItemOut {
|
|||
/** @example "0" */
|
||||
purchasePrice: string;
|
||||
/** Purchase */
|
||||
purchaseTime: Date;
|
||||
purchaseTime: string;
|
||||
quantity: number;
|
||||
serialNumber: string;
|
||||
soldNotes: string;
|
||||
|
@ -148,7 +148,7 @@ export interface ItemUpdate {
|
|||
/** @example "0" */
|
||||
purchasePrice: string;
|
||||
/** Purchase */
|
||||
purchaseTime: Date;
|
||||
purchaseTime: string;
|
||||
quantity: number;
|
||||
/** Identifications */
|
||||
serialNumber: string;
|
||||
|
@ -228,7 +228,7 @@ export interface LocationUpdate {
|
|||
export interface MaintenanceEntry {
|
||||
/** @example "0" */
|
||||
cost: string;
|
||||
date: Date;
|
||||
date: string;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -237,7 +237,7 @@ export interface MaintenanceEntry {
|
|||
export interface MaintenanceEntryCreate {
|
||||
/** @example "0" */
|
||||
cost: string;
|
||||
date: Date;
|
||||
date: string;
|
||||
description: string;
|
||||
name: string;
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ export interface MaintenanceEntryCreate {
|
|||
export interface MaintenanceEntryUpdate {
|
||||
/** @example "0" */
|
||||
cost: string;
|
||||
date: Date;
|
||||
date: string;
|
||||
description: string;
|
||||
name: string;
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ export interface MaintenanceLog {
|
|||
itemId: string;
|
||||
}
|
||||
|
||||
export interface PaginationResultRepoItemSummary {
|
||||
export interface PaginationResultItemSummary {
|
||||
items: ItemSummary[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
|
@ -294,7 +294,7 @@ export interface ValueOverTime {
|
|||
}
|
||||
|
||||
export interface ValueOverTimeEntry {
|
||||
date: Date;
|
||||
date: string;
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
@ -347,13 +347,13 @@ export interface EnsureAssetIDResult {
|
|||
}
|
||||
|
||||
export interface GroupInvitation {
|
||||
expiresAt: Date;
|
||||
expiresAt: string;
|
||||
token: string;
|
||||
uses: number;
|
||||
}
|
||||
|
||||
export interface GroupInvitationCreate {
|
||||
expiresAt: Date;
|
||||
expiresAt: string;
|
||||
uses: number;
|
||||
}
|
||||
|
||||
|
@ -363,6 +363,6 @@ export interface ItemAttachmentToken {
|
|||
|
||||
export interface TokenResponse {
|
||||
attachmentToken: string;
|
||||
expiresAt: Date;
|
||||
expiresAt: string;
|
||||
token: string;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,15 @@
|
|||
refresh();
|
||||
});
|
||||
|
||||
const lastRoute = ref(route.fullPath);
|
||||
watchEffect(() => {
|
||||
if (lastRoute.value.endsWith("edit")) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
lastRoute.value = route.fullPath;
|
||||
});
|
||||
|
||||
type FilteredAttachments = {
|
||||
attachments: ItemAttachment[];
|
||||
warranty: ItemAttachment[];
|
||||
|
@ -325,6 +334,30 @@
|
|||
onClickOutside(refDialogBody, () => {
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
const currentPath = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: "details",
|
||||
name: "Details",
|
||||
to: `/item/${itemId.value}`,
|
||||
},
|
||||
{
|
||||
id: "log",
|
||||
name: "Log",
|
||||
to: `/item/${itemId.value}/log`,
|
||||
},
|
||||
{
|
||||
id: "edit",
|
||||
name: "Edit",
|
||||
to: `/item/${itemId.value}/edit`,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -343,66 +376,66 @@
|
|||
<img class="max-w-[80vw] max-h-[80vh]" :src="dialoged.src" />
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-package-variant" class="mr-2 -mt-1 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ item ? item.name : "" }}
|
||||
</span>
|
||||
<div v-if="item.parent" class="text-sm breadcrumbs pb-0">
|
||||
<ul class="text-base-content/70">
|
||||
<li>
|
||||
<NuxtLink :to="`/item/${item.parent.id}`"> {{ item.parent.name }}</NuxtLink>
|
||||
</li>
|
||||
<li>{{ item.name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template #description>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<NuxtLink ref="badge" class="badge p-3" :to="`/location/${item.location.id}`">
|
||||
<Icon name="heroicons-map-pin" class="mr-2 swap-on"></Icon>
|
||||
{{ item.location.name }}
|
||||
</NuxtLink>
|
||||
<template v-if="item.labels && item.labels.length > 0">
|
||||
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
<div class="flex flex-wrap items-center justify-between mb-6">
|
||||
<div class="tabs">
|
||||
<NuxtLink
|
||||
v-for="t in tabs"
|
||||
:key="t.id"
|
||||
:to="t.to"
|
||||
class="tab tab-bordered lg:tab-lg"
|
||||
:class="`${t.to === currentPath ? 'tab-active' : ''}`"
|
||||
>
|
||||
{{ t.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<BaseButton class="btn btn-sm" @click="deleteItem()">
|
||||
<Icon name="mdi-delete" class="mr-2" />
|
||||
Delete
|
||||
</BaseButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="space-y-6">
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-package-variant" class="mr-2 -mt-1 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ item ? item.name : "" }}
|
||||
</span>
|
||||
<div v-if="item.parent" class="text-sm breadcrumbs pb-0">
|
||||
<ul class="text-base-content/70">
|
||||
<li>
|
||||
<NuxtLink :to="`/item/${item.parent.id}`"> {{ item.parent.name }}</NuxtLink>
|
||||
</li>
|
||||
<li>{{ item.name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template #description>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<NuxtLink ref="badge" class="badge p-3" :to="`/location/${item.location.id}`">
|
||||
<Icon name="heroicons-map-pin" class="mr-2 swap-on"></Icon>
|
||||
{{ item.location.name }}
|
||||
</NuxtLink>
|
||||
<template v-if="item.labels && item.labels.length > 0">
|
||||
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
<BaseCard v-if="!hasNested">
|
||||
<template #title> Details </template>
|
||||
<template #title-actions>
|
||||
<div class="flex flex-wrap justify-between items-center mt-2 gap-4">
|
||||
<label v-if="!hasNested" class="label cursor-pointer">
|
||||
<label class="label cursor-pointer">
|
||||
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Show Empty </span>
|
||||
</label>
|
||||
<div class="flex flex-wrap justify-end gap-2 ml-auto">
|
||||
<BaseButton size="sm" :to="`/item/${itemId}/edit`">
|
||||
<template #icon>
|
||||
<Icon name="mdi-pencil" />
|
||||
</template>
|
||||
Edit
|
||||
</BaseButton>
|
||||
<BaseButton size="sm" @click="deleteItem">
|
||||
<template #icon>
|
||||
<Icon name="mdi-delete" />
|
||||
</template>
|
||||
Delete
|
||||
</BaseButton>
|
||||
<BaseButton size="sm" :to="`/item/${itemId}/log`">
|
||||
<template #icon>
|
||||
<Icon name="mdi-post" />
|
||||
</template>
|
||||
Log
|
||||
</BaseButton>
|
||||
</div>
|
||||
<PageQRCode />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DetailsSection v-if="!hasNested" :details="itemDetails" />
|
||||
<DetailsSection :details="itemDetails" />
|
||||
</BaseCard>
|
||||
|
||||
<NuxtPage :item="item" :page-key="itemId" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ItemAttachment, ItemUpdate } from "~~/lib/api/types/data-contracts";
|
||||
import { ItemAttachment, ItemField, ItemUpdate } from "~~/lib/api/types/data-contracts";
|
||||
import { AttachmentTypes } from "~~/lib/api/types/non-generated";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
@ -196,11 +196,16 @@
|
|||
|
||||
function uploadImage(e: InputEvent) {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (!files) {
|
||||
if (!files || !files.item(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadAttachment([files.item(0)], AttachmentTypes.Attachment);
|
||||
const first = files.item(0);
|
||||
if (!first) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadAttachment([first], AttachmentTypes.Attachment);
|
||||
}
|
||||
|
||||
const dropPhoto = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Photo);
|
||||
|
@ -210,7 +215,7 @@
|
|||
const dropReceipt = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Receipt);
|
||||
|
||||
async function uploadAttachment(files: File[] | null, type: AttachmentTypes) {
|
||||
if (!files && files.length === 0) {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -295,22 +300,6 @@
|
|||
toast.success("Attachment updated");
|
||||
}
|
||||
|
||||
// Custom Fields
|
||||
// const fieldTypes = [
|
||||
// {
|
||||
// name: "Text",
|
||||
// value: "text",
|
||||
// },
|
||||
// {
|
||||
// name: "Number",
|
||||
// value: "number",
|
||||
// },
|
||||
// {
|
||||
// name: "Boolean",
|
||||
// value: "boolean",
|
||||
// },
|
||||
// ];
|
||||
|
||||
function addField() {
|
||||
item.value.fields.push({
|
||||
id: null,
|
||||
|
@ -320,7 +309,7 @@
|
|||
numberValue: 0,
|
||||
booleanValue: false,
|
||||
timeValue: null,
|
||||
});
|
||||
} as unknown as ItemField);
|
||||
}
|
||||
|
||||
const { query, results } = useItemSearch(api, { immediate: false });
|
||||
|
@ -328,7 +317,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<BaseContainer v-if="item" class="pb-8">
|
||||
<div v-if="item" class="pb-8">
|
||||
<BaseModal v-model="editState.modal">
|
||||
<template #title> Attachment Edit </template>
|
||||
|
||||
|
@ -346,15 +335,11 @@
|
|||
</div>
|
||||
</BaseModal>
|
||||
|
||||
<section class="px-3">
|
||||
<div class="space-y-4">
|
||||
<section>
|
||||
<div class="space-y-6">
|
||||
<div class="card bg-base-100 shadow-xl sm:rounded-lg overflow-visible">
|
||||
<BaseSectionHeader v-if="item" class="p-5">
|
||||
<Icon name="mdi-package-variant" class="-mt-1 mr-2 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<p class="text-sm text-base-content font-bold pb-0 mb-0">Quantity {{ item.quantity }}</p>
|
||||
<span class="text-base-content"> Edit </span>
|
||||
<template #after>
|
||||
<div class="modal-action mt-3">
|
||||
<div class="mr-auto tooltip" data-tip="Hide the cruft! ">
|
||||
|
@ -632,5 +617,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</div>
|
||||
</template>
|
|
@ -113,20 +113,6 @@
|
|||
</BaseModal>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="flex">
|
||||
<BaseButton size="sm" @click="$router.go(-1)">
|
||||
<template #icon>
|
||||
<Icon name="mdi-arrow-left" class="h-5 w-5" />
|
||||
</template>
|
||||
Back
|
||||
</BaseButton>
|
||||
<BaseButton class="ml-auto" size="sm" @click="newEntry()">
|
||||
<template #icon>
|
||||
<Icon name="mdi-post" />
|
||||
</template>
|
||||
Log Maintenance
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<StatCard
|
||||
v-for="stat in stats"
|
||||
|
@ -137,6 +123,14 @@
|
|||
:type="stat.type"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<BaseButton class="ml-auto" size="sm" @click="newEntry()">
|
||||
<template #icon>
|
||||
<Icon name="mdi-post" />
|
||||
</template>
|
||||
Log Maintenance
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="container space-y-6">
|
||||
<BaseCard v-for="e in log.entries" :key="e.id">
|
||||
<BaseSectionHeader class="p-6 border-b border-b-gray-300">
|
||||
|
|
|
@ -149,6 +149,7 @@
|
|||
<Icon class="mr-1" name="mdi-delete" />
|
||||
Delete
|
||||
</BaseButton>
|
||||
<PageQRCode />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
<Icon class="mr-1" name="mdi-delete" />
|
||||
Delete
|
||||
</BaseButton>
|
||||
<PageQRCode />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue