feat: maintenance log (#170)

* remove repo for document tokens

* remove schema for doc tokens

* fix id template and generate cmd

* schema updates

* code gen

* bump dependencies

* fix broken migrations + add maintenance entry type

* spelling

* remove debug logger

* implement repository layer

* routes

* API client

* wip: maintenance log

* remove depreciated call
This commit is contained in:
Hayden 2022-12-09 20:57:57 -09:00 committed by GitHub
parent d6da63187b
commit 5bbb969763
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 6320 additions and 4957 deletions

View file

@ -214,7 +214,7 @@
return;
}
const { data, error } = await api.items.addAttachment(itemId.value, files[0], files[0].name, type);
const { data, error } = await api.items.attachments.add(itemId.value, files[0], files[0].name, type);
if (error) {
toast.error("Failed to upload attachment");
@ -235,7 +235,7 @@
return;
}
const { error } = await api.items.deleteAttachment(itemId.value, attachmentId);
const { error } = await api.items.attachments.delete(itemId.value, attachmentId);
if (error) {
toast.error("Failed to delete attachment");
@ -273,7 +273,7 @@
async function updateAttachment() {
editState.loading = true;
const { error, data } = await api.items.updateAttachment(itemId.value, editState.id, {
const { error, data } = await api.items.attachments.update(itemId.value, editState.id, {
title: editState.title,
type: editState.type,
});

View file

@ -13,6 +13,10 @@
const itemId = computed<string>(() => route.params.id as string);
const preferences = useViewPreferences();
const hasNested = computed<boolean>(() => {
return route.fullPath.split("/").at(-1) !== itemId.value;
});
const { data: item, refresh } = useAsyncData(itemId.value, async () => {
const { data, error } = await api.items.get(itemId.value);
if (error) {
@ -219,7 +223,7 @@
} else {
details.push({
name: "Warranty Expires",
text: item.value?.warrantyExpires,
text: item.value?.warrantyExpires || "",
type: "date",
});
}
@ -253,7 +257,7 @@
},
{
name: "Purchase Date",
text: item.value.purchaseTime,
text: item.value?.purchaseTime || "",
type: "date",
},
];
@ -309,12 +313,12 @@
});
function openDialog(img: Photo) {
refDialog.value.showModal();
refDialog.value?.showModal();
dialoged.src = img.src;
}
function closeDialog() {
refDialog.value.close();
refDialog.value?.close();
}
const refDialogBody = ref<HTMLDivElement>();
@ -340,10 +344,7 @@
</div>
</dialog>
<section class="px-3">
<div class="flex justify-between items-center">
<div class="form-control"></div>
</div>
<div class="grid grid-cols-1 gap-3">
<div class="space-y-3">
<BaseCard>
<template #title>
<BaseSectionHeader>
@ -374,10 +375,16 @@
</template>
<template #title-actions>
<div class="modal-action mt-0">
<label class="label cursor-pointer mr-auto">
<label v-if="!hasNested" class="label cursor-pointer mr-auto">
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
<span class="label-text ml-4"> Show Empty </span>
</label>
<BaseButton v-else class="mr-auto" size="sm" @click="$router.go(-1)">
<template #icon>
<Icon name="mdi-arrow-left" class="h-5 w-5" />
</template>
Back
</BaseButton>
<BaseButton size="sm" :to="`/item/${itemId}/edit`">
<template #icon>
<Icon name="mdi-pencil" />
@ -390,75 +397,84 @@
</template>
Delete
</BaseButton>
<BaseButton size="sm" :to="`/item/${itemId}/log`">
<template #icon>
<Icon name="mdi-post" />
</template>
Log
</BaseButton>
</div>
</template>
<DetailsSection :details="itemDetails" />
<DetailsSection v-if="!hasNested" :details="itemDetails" />
</BaseCard>
<BaseCard v-if="photos && photos.length > 0">
<template #title> Photos </template>
<div
class="container border-t border-gray-300 p-4 flex flex-wrap gap-2 mx-auto max-h-[500px] overflow-y-scroll scroll-bg"
>
<button v-for="(img, i) in photos" :key="i" @click="openDialog(img)">
<img class="rounded max-h-[200px]" :src="img.src" />
</button>
</div>
</BaseCard>
<NuxtPage :item="item" :page-key="itemId" />
<div v-if="!hasNested">
<BaseCard v-if="photos && photos.length > 0">
<template #title> Photos </template>
<div
class="container border-t border-gray-300 p-4 flex flex-wrap gap-2 mx-auto max-h-[500px] overflow-y-scroll scroll-bg"
>
<button v-for="(img, i) in photos" :key="i" @click="openDialog(img)">
<img class="rounded max-h-[200px]" :src="img.src" />
</button>
</div>
</BaseCard>
<BaseCard v-if="showAttachments">
<template #title> Attachments </template>
<DetailsSection :details="attachmentDetails">
<template #manuals>
<ItemAttachmentsList
v-if="attachments.manuals.length > 0"
:attachments="attachments.manuals"
:item-id="item.id"
/>
</template>
<template #attachments>
<ItemAttachmentsList
v-if="attachments.attachments.length > 0"
:attachments="attachments.attachments"
:item-id="item.id"
/>
</template>
<template #warranty>
<ItemAttachmentsList
v-if="attachments.warranty.length > 0"
:attachments="attachments.warranty"
:item-id="item.id"
/>
</template>
<template #receipts>
<ItemAttachmentsList
v-if="attachments.receipts.length > 0"
:attachments="attachments.receipts"
:item-id="item.id"
/>
</template>
</DetailsSection>
</BaseCard>
<BaseCard v-if="showAttachments">
<template #title> Attachments </template>
<DetailsSection :details="attachmentDetails">
<template #manuals>
<ItemAttachmentsList
v-if="attachments.manuals.length > 0"
:attachments="attachments.manuals"
:item-id="item.id"
/>
</template>
<template #attachments>
<ItemAttachmentsList
v-if="attachments.attachments.length > 0"
:attachments="attachments.attachments"
:item-id="item.id"
/>
</template>
<template #warranty>
<ItemAttachmentsList
v-if="attachments.warranty.length > 0"
:attachments="attachments.warranty"
:item-id="item.id"
/>
</template>
<template #receipts>
<ItemAttachmentsList
v-if="attachments.receipts.length > 0"
:attachments="attachments.receipts"
:item-id="item.id"
/>
</template>
</DetailsSection>
</BaseCard>
<BaseCard v-if="showPurchase">
<template #title> Purchase Details </template>
<DetailsSection :details="purchaseDetails" />
</BaseCard>
<BaseCard v-if="showPurchase">
<template #title> Purchase Details </template>
<DetailsSection :details="purchaseDetails" />
</BaseCard>
<BaseCard v-if="showWarranty">
<template #title> Warranty Details </template>
<DetailsSection :details="warrantyDetails" />
</BaseCard>
<BaseCard v-if="showWarranty">
<template #title> Warranty Details </template>
<DetailsSection :details="warrantyDetails" />
</BaseCard>
<BaseCard v-if="showSold">
<template #title> Sold Details </template>
<DetailsSection :details="soldDetails" />
</BaseCard>
<BaseCard v-if="showSold">
<template #title> Sold Details </template>
<DetailsSection :details="soldDetails" />
</BaseCard>
</div>
</div>
</section>
<section class="my-6 px-3">
<section v-if="!hasNested" class="my-6 px-3">
<BaseSectionHeader v-if="item && item.children && item.children.length > 0"> Child Items </BaseSectionHeader>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ItemCard v-for="child in item.children" :key="child.id" :item="child" />

View file

@ -0,0 +1,173 @@
<script setup lang="ts">
import DatePicker from "~~/components/Form/DatePicker.vue";
import { ItemOut } from "~~/lib/api/types/data-contracts";
const props = defineProps<{
item: ItemOut;
}>();
const api = useUserApi();
const toast = useNotifier();
const { data: log, refresh: refreshLog } = useAsyncData(async () => {
const { data } = await api.items.maintenance.getLog(props.item.id);
return data;
});
const stats = computed(() => {
if (!log.value) return [];
return [
{
id: "total",
title: "Total Cost",
subtitle: "Sum over all entries",
value: fmtCurrency(log.value.costTotal),
},
{
id: "average",
title: "Monthly Average",
subtitle: "Average over all entries",
value: fmtCurrency(log.value.costAverage),
},
];
});
const entry = reactive({
modal: false,
name: "",
date: new Date(),
description: "",
cost: "",
});
function newEntry() {
entry.modal = true;
}
async function createEntry() {
const { error } = await api.items.maintenance.create(props.item.id, {
name: entry.name,
date: entry.date,
description: entry.description,
cost: entry.cost,
});
if (error) {
toast.error("Failed to create entry");
return;
}
entry.modal = false;
refreshLog();
}
async function deleteEntry(id: string) {
const { error } = await api.items.maintenance.delete(props.item.id, id);
if (error) {
toast.error("Failed to delete entry");
return;
}
refreshLog();
}
</script>
<template>
<div v-if="log">
<BaseModal v-model="entry.modal">
<template #title> Create Entry </template>
<form @submit.prevent="createEntry">
<FormTextField v-model="entry.name" autofocus label="Entry Name" />
<DatePicker v-model="entry.date" label="Date" />
<FormTextArea v-model="entry.description" label="Notes" />
<FormTextField v-model="entry.cost" autofocus label="Cost" />
<div class="py-2 flex justify-end">
<BaseButton type="submit" class="ml-2">
<template #icon>
<Icon name="mdi-post" />
</template>
Create
</BaseButton>
</div>
</form>
</BaseModal>
<div class="flex">
<BaseButton class="ml-auto" size="sm" @click="newEntry()">
<template #icon>
<Icon name="mdi-post" />
</template>
Log Maintenance
</BaseButton>
</div>
<section class="page-layout my-6">
<div class="main-slot container space-y-6">
<BaseCard v-for="e in log.entries" :key="e.id">
<BaseSectionHeader class="p-6 border-b border-b-gray-300">
<span class="text-base-content">
{{ e.name }}
</span>
<template #description>
<div class="flex gap-2">
<div class="badge p-3">
<Icon name="mdi-calendar" class="mr-2" />
<DateTime :date="e.date" format="human" />
</div>
<div class="tooltip tooltip-primary" data-tip="Cost">
<div class="badge badge-primary p-3">
<Currency :amount="e.cost" />
</div>
</div>
</div>
</template>
</BaseSectionHeader>
<div class="p-6">
<Markdown :source="e.description" />
</div>
<div class="flex justify-end p-4">
<BaseButton size="sm" @click="deleteEntry(e.id)">
<template #icon>
<Icon name="mdi-delete" />
</template>
Delete
</BaseButton>
</div>
</BaseCard>
</div>
<div class="side-slot space-y-6">
<div v-for="stat in stats" :key="stat.id" class="stats block shadow-xl border-l-primary">
<div class="stat">
<div class="stat-title">{{ stat.title }}</div>
<div class="stat-value text-primary">{{ stat.value }}</div>
<div class="stat-desc">{{ stat.subtitle }}</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.page-layout {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: auto;
grid-template-areas: "side main";
gap: 1rem;
}
.side-slot {
grid-area: side;
}
.main-slot {
grid-area: main;
}
.grid {
display: grid;
}
</style>