WIP item update client side actions

This commit is contained in:
Hayden 2022-09-11 21:07:51 -08:00
parent 16934b5bcc
commit b8cee37fdc
20 changed files with 595 additions and 93 deletions

View file

@ -5,7 +5,6 @@ tasks:
cmds:
- |
cd backend && ent generate ./ent/schema \
--template=ent/schema/templates/stringer.tmpl \
--template=ent/schema/templates/has_id.tmpl
- cd backend/app/api/ && swag fmt
- cd backend/app/api/ && swag init --dir=./,../../internal,../../pkgs
@ -17,18 +16,12 @@ tasks:
--path ./backend/app/api/docs/swagger.json \
--output ./frontend/lib/api/types
# Output cleanup for the generated types
# 1. Remove the prefix `Types` from each type generated
# 2. Remove the ?: from the type definition since types are not properly annotated in the swagger.json
python3 ./scripts/process-types.py ./frontend/lib/api/types/data-contracts.ts
api:
cmds:
- task: generate
- cd backend && go run ./app/api/ {{.CLI_ARGS}}
silent: false
sources:
- ./backend/**/*.go
api:build:
cmds:
@ -40,6 +33,10 @@ tasks:
- cd backend && go test ./app/api/
silent: true
api:watch:
cmds:
- cd backend && gotestsum --watch ./...
api:coverage:
cmds:
- cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
@ -53,12 +50,12 @@ tasks:
- cd frontend && pnpm run test:ci
silent: true
docker:build:
frontend:watch:
desc: Starts the vitest test runner in watch mode
cmds:
- cd backend && docker-compose up --build
silent: true
- cd frontend && pnpm vitest --watch
generate:types:
frontend:
desc: Run frontend development server
cmds:
- cd backend && go run ./app/generator
silent: true
- cd frontend && pnpm dev

View file

@ -175,6 +175,15 @@ const docTemplate = `{
"name": "id",
"in": "path",
"required": true
},
{
"description": "Item Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ItemUpdate"
}
}
],
"responses": {
@ -1053,6 +1062,83 @@ const docTemplate = `{
}
}
},
"types.ItemUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"insured": {
"type": "boolean"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"lifetimeWarranty": {
"description": "Warranty",
"type": "boolean"
},
"locationId": {
"description": "Edges",
"type": "string"
},
"manufacturer": {
"type": "string"
},
"modelNumber": {
"type": "string"
},
"name": {
"type": "string"
},
"notes": {
"description": "Extras",
"type": "string"
},
"purchaseFrom": {
"type": "string"
},
"purchasePrice": {
"type": "number"
},
"purchaseTime": {
"description": "Purchase",
"type": "string"
},
"quantity": {
"type": "integer"
},
"serialNumber": {
"description": "Identifications",
"type": "string"
},
"soldNotes": {
"type": "string"
},
"soldPrice": {
"type": "number"
},
"soldTime": {
"description": "Sold",
"type": "string"
},
"soldTo": {
"type": "string"
},
"warrantyDetails": {
"type": "string"
},
"warrantyExpires": {
"type": "string"
}
}
},
"types.LabelCreate": {
"type": "object",
"properties": {

View file

@ -167,6 +167,15 @@
"name": "id",
"in": "path",
"required": true
},
{
"description": "Item Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ItemUpdate"
}
}
],
"responses": {
@ -1045,6 +1054,83 @@
}
}
},
"types.ItemUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"insured": {
"type": "boolean"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"lifetimeWarranty": {
"description": "Warranty",
"type": "boolean"
},
"locationId": {
"description": "Edges",
"type": "string"
},
"manufacturer": {
"type": "string"
},
"modelNumber": {
"type": "string"
},
"name": {
"type": "string"
},
"notes": {
"description": "Extras",
"type": "string"
},
"purchaseFrom": {
"type": "string"
},
"purchasePrice": {
"type": "number"
},
"purchaseTime": {
"description": "Purchase",
"type": "string"
},
"quantity": {
"type": "integer"
},
"serialNumber": {
"description": "Identifications",
"type": "string"
},
"soldNotes": {
"type": "string"
},
"soldPrice": {
"type": "number"
},
"soldTime": {
"description": "Sold",
"type": "string"
},
"soldTo": {
"type": "string"
},
"warrantyDetails": {
"type": "string"
},
"warrantyExpires": {
"type": "string"
}
}
},
"types.LabelCreate": {
"type": "object",
"properties": {

View file

@ -179,6 +179,59 @@ definitions:
warrantyExpires:
type: string
type: object
types.ItemUpdate:
properties:
description:
type: string
id:
type: string
insured:
type: boolean
labelIds:
items:
type: string
type: array
lifetimeWarranty:
description: Warranty
type: boolean
locationId:
description: Edges
type: string
manufacturer:
type: string
modelNumber:
type: string
name:
type: string
notes:
description: Extras
type: string
purchaseFrom:
type: string
purchasePrice:
type: number
purchaseTime:
description: Purchase
type: string
quantity:
type: integer
serialNumber:
description: Identifications
type: string
soldNotes:
type: string
soldPrice:
type: number
soldTime:
description: Sold
type: string
soldTo:
type: string
warrantyDetails:
type: string
warrantyExpires:
type: string
type: object
types.LabelCreate:
properties:
color:
@ -415,6 +468,12 @@ paths:
name: id
required: true
type: string
- description: Item Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/types.ItemUpdate'
produces:
- application/json
responses:

View file

@ -64,7 +64,7 @@ func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
// @Summary deletes a item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param id path string true "Item ID"
// @Success 204
// @Router /v1/items/{id} [DELETE]
// @Security Bearer
@ -90,7 +90,7 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} types.ItemOut
// @Success 200 {object} types.ItemOut
// @Router /v1/items/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
@ -115,6 +115,7 @@ func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body types.ItemUpdate true "Item Data"
// @Success 200 {object} types.ItemOut
// @Router /v1/items/{id} [PUT]
// @Security Bearer

View file

@ -9,7 +9,7 @@ import (
// body is decoded into the provided value.
func Decode(r *http.Request, val interface{}) error {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
// decoder.DisallowUnknownFields()
if err := decoder.Decode(val); err != nil {
return err
}

View file

@ -15,7 +15,7 @@
{{ dKey }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<slot :name="dKey" v-bind="{ key: dKey, value: dValue }">
<slot :name="rmSpace(dKey)" v-bind="{ key: dKey, value: dValue }">
{{ dValue }}
</slot>
</dd>
@ -28,6 +28,10 @@
<script setup lang="ts">
type StringLike = string | number | boolean;
function rmSpace(str: string) {
return str.replace(" ", "");
}
defineProps({
details: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -52,9 +52,14 @@
const selected = useVModel(props, "modelValue", emit);
const dateText = computed(() => {
if (!validDate(selected.value)) {
return "";
}
if (selected.value) {
return selected.value.toLocaleDateString();
}
return "";
});

View file

@ -17,7 +17,7 @@
v-for="(obj, idx) in items"
:key="idx"
:class="{
bordered: selectedIndexes[idx],
bordered: selected[idx],
}"
>
<button type="button" @click="toggle(idx)">
@ -56,28 +56,23 @@
},
});
const selectedIndexes = ref<Record<number, boolean>>({});
const value = useVModel(props, "modelValue", emit);
const selected = computed<Record<number, boolean>>(() => {
const obj: Record<number, boolean> = {};
value.value.forEach(itm => {
const idx = props.items.findIndex(item => item[props.name] === itm.name);
obj[idx] = true;
});
return obj;
});
function toggle(index: number) {
selectedIndexes.value[index] = !selectedIndexes.value[index];
const item = props.items[index];
if (selectedIndexes.value[index]) {
value.value = [...value.value, item];
if (selected.value[index]) {
value.value = value.value.filter(itm => itm.name !== item.name);
} else {
value.value = value.value.filter(itm => itm !== item);
value.value = [...value.value, item];
}
}
watchOnce(
() => props.items,
() => {
if (props.selectFirst && props.items.length > 0) {
value.value = props.items[0];
}
}
);
const value = useVModel(props, "modelValue", emit);
</script>

View file

@ -3,9 +3,9 @@
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<select v-model="value" class="select select-bordered">
<select v-model="selectedIdx" class="select select-bordered">
<option disabled selected>Pick one</option>
<option v-for="obj in items" :key="name != '' ? obj[name] : obj" :value="obj">
<option v-for="(obj, idx) in items" :key="name != '' ? obj[name] : obj" :value="idx">
{{ name != "" ? obj[name] : obj }}
</option>
</select>
@ -47,10 +47,16 @@
() => props.items,
() => {
if (props.selectFirst && props.items.length > 0) {
value.value = props.items[0];
selectedIdx.value = 0;
}
}
);
const value = useVModel(props, "modelValue", emit);
const selectedIdx = ref(0);
watch(
() => selectedIdx.value,
() => {
emit("update:modelValue", props.items[selectedIdx.value]);
}
);
</script>

View file

@ -22,11 +22,11 @@
</template>
<script setup lang="ts">
import { ItemOut } from "~~/lib/api/types/data-contracts";
import { ItemOut, ItemSummary } from "~~/lib/api/types/data-contracts";
const props = defineProps({
item: {
type: Object as () => ItemOut,
type: Object as () => ItemOut | ItemSummary,
required: true,
},
});

View file

@ -26,7 +26,8 @@
</template>
<script setup lang="ts">
import { type Location } from "~~/lib/api/classes/locations";
import { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
const props = defineProps({
modelValue: {
type: Boolean,
@ -40,7 +41,7 @@
const loading = ref(false);
const focused = ref(false);
const form = reactive({
location: {} as Location,
location: {} as LocationOut,
name: "",
description: "",
color: "", // Future!
@ -80,7 +81,7 @@
return;
}
const out = {
const out: ItemCreate = {
name: form.name,
description: form.description,
locationId: form.location.id as string,

View file

@ -0,0 +1,52 @@
<template>
{{ value }}
</template>
<script setup lang="ts">
enum DateTimeFormat {
RELATIVE = "relative",
LONG = "long",
SHORT = "short",
}
const value = computed(() => {
if (!props.date) {
return "";
}
const dt = typeof props.date === "string" ? new Date(props.date) : props.date;
if (!dt) {
return "";
}
if (nullDate(dt)) {
return "";
}
switch (props.format) {
case DateTimeFormat.RELATIVE:
return useTimeAgo(dt).value + useDateFormat(dt, " (MM-DD-YYYY)").value;
case DateTimeFormat.LONG:
return useDateFormat(dt, "YYYY-MM-DD (dddd)").value;
case DateTimeFormat.SHORT:
return useDateFormat(dt, "YYYY-MM-DD").value;
default:
return "";
}
});
function nullDate(dt: Date) {
return dt.getFullYear() === 1;
}
const props = defineProps({
date: {
type: [Date, String],
required: true,
},
format: {
type: String as () => DateTimeFormat,
default: "relative",
},
});
</script>

View file

@ -3,6 +3,7 @@ import { Ref } from "vue";
export type LocationViewPreferences = {
showDetails: boolean;
showEmpty: boolean;
editorSimpleView: boolean;
};
/**
@ -15,6 +16,7 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
{
showDetails: true,
showEmpty: true,
editorSimpleView: true,
},
{ mergeDefaults: true }
);

View file

@ -0,0 +1,22 @@
export function validDate(dt: Date | string | null | undefined): boolean {
if (!dt) {
return false;
}
// If it's a string, try to parse it
if (typeof dt === "string") {
const parsed = new Date(dt);
if (isNaN(parsed.getTime())) {
return false;
}
}
// If it's a date, check if it's valid
if (dt instanceof Date) {
if (dt.getFullYear() < 1000) {
return false;
}
}
return true;
}

View file

@ -36,4 +36,26 @@ export class BaseAPI {
constructor(requests: Requests) {
this.http = requests;
}
/**
* dropFields will remove any fields that are specified in the fields array
* additionally, it will remove the `createdAt` and `updatedAt` fields if they
* are present. This is useful for when you want to send a subset of fields to
* the server like when performing an update.
*/
dropFields<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj };
console.log("dropFields", result);
[...keys, "createdAt", "updatedAt"].forEach(key => {
console.log(key);
// @ts-ignore - we are checking for the key above
if (hasKey(result, key)) {
// @ts-ignore - we are guarding against this above
delete result[key];
console.log("dropping", key);
}
});
console.log("dropFields", result);
return result;
}
}

View file

@ -1,6 +1,6 @@
import { BaseAPI, route } from "../base";
import { parseDate } from "../base/base-api";
import { ItemCreate, ItemOut } from "../types/data-contracts";
import { ItemCreate, ItemOut, ItemSummary, ItemUpdate } from "../types/data-contracts";
import { Results } from "./types";
export class ItemsApi extends BaseAPI {
@ -9,7 +9,7 @@ export class ItemsApi extends BaseAPI {
}
create(item: ItemCreate) {
return this.http.post<ItemCreate, ItemOut>({ url: route("/items"), body: item });
return this.http.post<ItemCreate, ItemSummary>({ url: route("/items"), body: item });
}
async get(id: string) {
@ -28,8 +28,17 @@ export class ItemsApi extends BaseAPI {
return this.http.delete<void>({ url: route(`/items/${id}`) });
}
update(id: string, item: ItemCreate) {
return this.http.put<ItemCreate, ItemOut>({ url: route(`/items/${id}`), body: item });
async update(id: string, item: ItemUpdate) {
const payload = await this.http.put<ItemCreate, ItemOut>({
url: route(`/items/${id}`),
body: this.dropFields(item),
});
if (!payload.data) {
return payload;
}
payload.data = parseDate(payload.data, ["purchaseTime", "soldTime", "warrantyExpires"]);
return payload;
}
import(file: File) {

View file

@ -127,6 +127,42 @@ export interface ItemSummary {
warrantyExpires: Date;
}
export interface ItemUpdate {
description: string;
id: string;
insured: boolean;
labelIds: string[];
/** Warranty */
lifetimeWarranty: boolean;
/** Edges */
locationId: string;
manufacturer: string;
modelNumber: string;
name: string;
/** Extras */
notes: string;
purchaseFrom: string;
purchasePrice: number;
/** Purchase */
purchaseTime: Date;
quantity: number;
/** Identifications */
serialNumber: string;
soldNotes: string;
soldPrice: number;
/** Sold */
soldTime: Date;
soldTo: string;
warrantyDetails: string;
warrantyExpires: Date;
}
export interface LabelCreate {
color: string;
description: string;

View file

@ -1,4 +1,6 @@
<script setup lang="ts">
import { ItemUpdate } from "~~/lib/api/types/data-contracts";
definePageMeta({
layout: "home",
});
@ -6,9 +8,25 @@
const route = useRoute();
const api = useUserApi();
const toast = useNotifier();
const preferences = useViewPreferences();
const itemId = computed<string>(() => route.params.id as string);
const originalItem = ref({
name: "",
quantity: 0,
});
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 +34,37 @@
navigateTo("/home");
return;
}
originalItem.value = {
name: data.name,
quantity: data.quantity,
};
return data;
});
async function saveItem() {
const payload: ItemUpdate = {
...item.value,
locationId: item.value.location?.id,
labelIds: item.value.labels.map(l => l.id),
};
console.log(payload);
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";
label: string;
ref: string;
};
@ -97,51 +141,115 @@
<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">
{{ originalItem.name }}
</span>
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">Quantity {{ originalItem.quantity }}</p>
<template #after>
<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 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">
<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 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>
</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 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>
</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">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 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>
</div>
</div>
</div>
</div>
</div>
</section>
</BaseContainer>
</template>

View file

@ -27,6 +27,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 +36,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 || "";
}
@ -103,7 +104,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 +117,12 @@
<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="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 +168,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>