forked from mirrors/homebox
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:
parent
fbc364dcd2
commit
95ab14b866
125 changed files with 15626 additions and 1791 deletions
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue