feat: items-editor (#5)

* format readme

* update logo

* format html

* add logo to docs

* repository for document and document tokens

* add attachments type and repository

* autogenerate types via scripts

* use autogenerated types

* attachment type updates

* add insured and quantity fields for items

* implement HasID interface for entities

* implement label updates for items

* implement service update method

* WIP item update client side actions

* check err on attachment

* finish types for basic items editor

* remove unused var

* house keeping
This commit is contained in:
Hayden 2022-09-12 14:47:27 -08:00 committed by GitHub
parent fbc364dcd2
commit 95ab14b866
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 15626 additions and 1791 deletions

View file

@ -55,7 +55,6 @@
function setFile(e: Event & { target: HTMLInputElement }) {
importCsv.value = e.target.files[0];
console.log("importCsv.value", importCsv.value);
}
const toast = useNotifier();

View file

@ -129,7 +129,7 @@
<div>
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
HomeB
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
<AppLogo class="w-12 -mb-4" />
x
</h2>
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Shit.</p>

View file

@ -1,4 +1,6 @@
<script setup lang="ts">
import { ItemUpdate } from "~~/lib/api/types/data-contracts";
definePageMeta({
layout: "home",
});
@ -6,9 +8,20 @@
const route = useRoute();
const api = useUserApi();
const toast = useNotifier();
const preferences = useViewPreferences();
const itemId = computed<string>(() => route.params.id as string);
const { data: locations } = useAsyncData(async () => {
const { data } = await api.locations.getAll();
return data.items;
});
const { data: labels } = useAsyncData(async () => {
const { data } = await api.labels.getAll();
return data.items;
});
const { data: item } = useAsyncData(async () => {
const { data, error } = await api.items.get(itemId.value);
if (error) {
@ -16,11 +29,30 @@
navigateTo("/home");
return;
}
return data;
});
async function saveItem() {
const payload: ItemUpdate = {
...item.value,
locationId: item.value.location?.id,
labelIds: item.value.labels.map(l => l.id),
};
const { error } = await api.items.update(itemId.value, payload);
if (error) {
toast.error("Failed to save item");
return;
}
toast.success("Item saved");
navigateTo("/item/" + itemId.value);
}
type FormField = {
type: "text" | "textarea" | "select" | "date";
type: "text" | "textarea" | "select" | "date" | "label" | "location" | "number" | "checkbox";
label: string;
ref: string;
};
@ -31,6 +63,11 @@
label: "Name",
ref: "name",
},
{
type: "number",
label: "Quantity",
ref: "quantity",
},
{
type: "textarea",
label: "Description",
@ -56,6 +93,11 @@
label: "Notes",
ref: "notes",
},
{
type: "checkbox",
label: "Insured",
ref: "insured",
},
];
const purchaseFields: FormField[] = [
@ -76,6 +118,24 @@
},
];
const warrantyFields: FormField[] = [
{
type: "checkbox",
label: "Lifetime Warranty",
ref: "lifetimeWarranty",
},
{
type: "date",
label: "Warranty Expires",
ref: "warrantyExpires",
},
{
type: "textarea",
label: "Warranty Notes",
ref: "warrantyDetails",
},
];
const soldFields = [
{
type: "text",
@ -97,51 +157,193 @@
<template>
<BaseContainer v-if="item" class="pb-8">
<div class="space-y-4">
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">Item Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div v-for="field in mainFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
<FormDatePicker v-else-if="field.type === 'date'" v-model="item[field.ref]" :label="field.label" inline />
</div>
<section class="px-3">
<div class="space-y-4">
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
<BaseSectionHeader v-if="item" class="p-5">
<Icon name="mdi-package-variant" class="-mt-1 mr-2 text-gray-600" />
<span class="text-gray-600">
{{ item.name }}
</span>
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">Quantity {{ item.quantity }}</p>
<template #after>
<div class="modal-action mt-3">
<div class="mr-auto tooltip" data-tip="Hide the cruft! ">
<label class="label cursor-pointer mr-auto">
<input v-model="preferences.editorSimpleView" type="checkbox" class="toggle toggle-primary" />
<span class="label-text ml-4"> Simple View </span>
</label>
</div>
<BaseButton size="sm" @click="saveItem">
<template #icon>
<Icon name="mdi-content-save-outline" />
</template>
Save
</BaseButton>
</div>
</template>
</BaseSectionHeader>
<div class="px-5 mb-6 grid md:grid-cols-2 gap-4">
<FormSelect v-model="item.location" label="Location" :items="locations ?? []" select-first />
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
</div>
</div>
</div>
<div class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">Purchase Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div v-for="field in purchaseFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
<FormDatePicker v-else-if="field.type === 'date'" v-model="item[field.ref]" :label="field.label" inline />
<div class="border-t border-gray-300 sm:p-0">
<div v-for="field in mainFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField
v-else-if="field.type === 'text'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormTextField
v-else-if="field.type === 'number'"
v-model.number="item[field.ref]"
type="number"
:label="field.label"
inline
/>
<FormDatePicker
v-else-if="field.type === 'date'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormCheckbox
v-else-if="field.type === 'checkbox'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
</div>
</div>
</div>
</div>
</div>
<div class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">Sold Details</h3>
<div v-if="!preferences.editorSimpleView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">Purchase Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div
v-for="field in purchaseFields"
:key="field.ref"
class="sm:divide-y sm:divide-gray-300 grid grid-cols-1"
>
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField
v-else-if="field.type === 'text'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormTextField
v-else-if="field.type === 'number'"
v-model.number="item[field.ref]"
type="number"
:label="field.label"
inline
/>
<FormDatePicker
v-else-if="field.type === 'date'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormCheckbox
v-else-if="field.type === 'checkbox'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div v-for="field in soldFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
<FormDatePicker v-else-if="field.type === 'date'" v-model="item[field.ref]" :label="field.label" inline />
<div v-if="!preferences.editorSimpleView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">Warranty Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div
v-for="field in warrantyFields"
:key="field.ref"
class="sm:divide-y sm:divide-gray-300 grid grid-cols-1"
>
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField
v-else-if="field.type === 'text'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormTextField
v-else-if="field.type === 'number'"
v-model.number="item[field.ref]"
type="number"
:label="field.label"
inline
/>
<FormDatePicker
v-else-if="field.type === 'date'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormCheckbox
v-else-if="field.type === 'checkbox'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
</div>
</div>
</div>
</div>
<div v-if="!preferences.editorSimpleView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">Sold Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div v-for="field in soldFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 px-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField
v-else-if="field.type === 'text'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormTextField
v-else-if="field.type === 'number'"
v-model.number="item[field.ref]"
type="number"
:label="field.label"
inline
/>
<FormDatePicker
v-else-if="field.type === 'date'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
<FormCheckbox
v-else-if="field.type === 'checkbox'"
v-model="item[field.ref]"
:label="field.label"
inline
/>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</BaseContainer>
</template>

View file

@ -10,7 +10,7 @@
const itemId = computed<string>(() => route.params.id as string);
const preferences = useViewPreferences();
const { data: item } = useAsyncData(async () => {
const { data: item, refresh } = useAsyncData(itemId.value, async () => {
const { data, error } = await api.items.get(itemId.value);
if (error) {
toast.error("Failed to load item");
@ -20,6 +20,11 @@
return data;
});
// Trigger Refresh on navigate
onMounted(() => {
refresh();
});
const itemSummary = computed(() => {
return {
Description: item.value?.description || "",
@ -27,6 +32,7 @@
"Model Number": item.value?.modelNumber || "",
Manufacturer: item.value?.manufacturer || "",
Notes: item.value?.notes || "",
Insured: item.value?.insured ? "Yes" : "No",
Attachments: "", // TODO: Attachments
};
});
@ -35,15 +41,15 @@
if (preferences.value.showEmpty) {
return true;
}
return item.value?.warrantyExpires !== undefined;
return validDate(item.value?.warrantyExpires);
});
const warrantyDetails = computed(() => {
const payload = {};
const payload = {
"Lifetime Warranty": item.value?.lifetimeWarranty ? "Yes" : "No",
};
if (item.value.lifetimeWarranty) {
payload["Lifetime Warranty"] = "Yes";
} else {
if (showWarranty.value) {
payload["Warranty Expires"] = item.value?.warrantyExpires || "";
}
@ -62,7 +68,7 @@
const purchaseDetails = computed(() => {
return {
"Purchased From": item.value?.purchaseFrom || "",
"Purchased Price": item.value?.purchasePrice || "",
"Purchased Price": item.value?.purchasePrice ? fmtCurrency(item.value.purchasePrice) : "",
"Purchased At": item.value?.purchaseTime || "",
};
});
@ -78,7 +84,7 @@
const soldDetails = computed(() => {
return {
"Sold To": item.value?.soldTo || "",
"Sold Price": item.value?.soldPrice || "",
"Sold Price": item.value?.soldPrice ? fmtCurrency(item.value.soldPrice) : "",
"Sold At": item.value?.soldTime || "",
};
});
@ -103,7 +109,7 @@
</script>
<template>
<BaseContainer class="pb-8">
<BaseContainer v-if="item" class="pb-8">
<section class="px-3">
<div class="flex justify-between items-center">
<div class="form-control"></div>
@ -116,11 +122,14 @@
<span class="text-gray-600">
{{ item.name }}
</span>
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">
{{ item.location.name }} - Quantity {{ item.quantity }}
</p>
<template #after>
<div class="flex flex-wrap gap-3 mt-3">
<div v-if="item.labels && item.labels.length > 0" class="flex flex-wrap gap-3 mt-3">
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
</div>
<div class="modal-action">
<div class="modal-action mt-3">
<label 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>
@ -166,12 +175,21 @@
</BaseDetails>
<BaseDetails v-if="showPurchase" :details="purchaseDetails">
<template #title> Purchase Details </template>
<template #PurchasedAt>
<DateTime :date="item.purchaseTime" />
</template>
</BaseDetails>
<BaseDetails v-if="showWarranty" :details="warrantyDetails">
<template #title> Warranty </template>
<template #WarrantyExpires>
<DateTime :date="item.warrantyExpires" />
</template>
</BaseDetails>
<BaseDetails v-if="showSold" :details="soldDetails">
<template #title> Sold </template>
<template #SoldAt>
<DateTime :date="item.soldTime" />
</template>
</BaseDetails>
</div>
</section>