mirror of
https://github.com/hay-kot/homebox.git
synced 2024-12-21 14:26:31 +00:00
ae0ba770d5
When using the maintenance dialog, the order title/scheduled/completed/costs/notes seems to be more sensible than title/completed/scheduled/notes/costs. Though notes/costs vs. costs/notes certainly can be argued about, in most of my cases this order would be more useful than the other way around.
284 lines
7.9 KiB
Vue
284 lines
7.9 KiB
Vue
<script setup lang="ts">
|
|
import DatePicker from "~~/components/Form/DatePicker.vue";
|
|
import type { StatsFormat } from "~~/components/global/StatCard/types";
|
|
import type { ItemOut, MaintenanceEntry } from "~~/lib/api/types/data-contracts";
|
|
import MdiPost from "~icons/mdi/post";
|
|
import MdiPlus from "~icons/mdi/plus";
|
|
import MdiCheck from "~icons/mdi/check";
|
|
import MdiDelete from "~icons/mdi/delete";
|
|
import MdiEdit from "~icons/mdi/edit";
|
|
import MdiCalendar from "~icons/mdi/calendar";
|
|
import MdiWrenchClock from "~icons/mdi/wrench-clock";
|
|
|
|
const props = defineProps<{
|
|
item: ItemOut;
|
|
}>();
|
|
|
|
const api = useUserApi();
|
|
const toast = useNotifier();
|
|
|
|
const scheduled = ref(true);
|
|
|
|
watch(
|
|
() => scheduled.value,
|
|
() => {
|
|
refreshLog();
|
|
}
|
|
);
|
|
|
|
const { data: log, refresh: refreshLog } = useAsyncData(async () => {
|
|
const { data } = await api.items.maintenance.getLog(props.item.id, {
|
|
scheduled: scheduled.value,
|
|
completed: !scheduled.value,
|
|
});
|
|
return data;
|
|
});
|
|
|
|
const count = computed(() => {
|
|
if (!log.value) return 0;
|
|
return log.value.entries.length;
|
|
});
|
|
const stats = computed(() => {
|
|
if (!log.value) return [];
|
|
|
|
return [
|
|
{
|
|
id: "count",
|
|
title: "Total Entries",
|
|
value: count.value || 0,
|
|
type: "number" as StatsFormat,
|
|
},
|
|
{
|
|
id: "total",
|
|
title: "Total Cost",
|
|
value: log.value.costTotal || 0,
|
|
type: "currency" as StatsFormat,
|
|
},
|
|
{
|
|
id: "average",
|
|
title: "Monthly Average",
|
|
value: log.value.costAverage || 0,
|
|
type: "currency" as StatsFormat,
|
|
},
|
|
];
|
|
});
|
|
|
|
const entry = reactive({
|
|
id: null as string | null,
|
|
modal: false,
|
|
name: "",
|
|
completedDate: null as Date | null,
|
|
scheduledDate: null as Date | null,
|
|
description: "",
|
|
cost: "",
|
|
});
|
|
|
|
function newEntry() {
|
|
entry.modal = true;
|
|
}
|
|
|
|
function resetEntry() {
|
|
console.log("Resetting entry");
|
|
entry.id = null;
|
|
entry.name = "";
|
|
entry.completedDate = null;
|
|
entry.scheduledDate = null;
|
|
entry.description = "";
|
|
entry.cost = "";
|
|
}
|
|
|
|
watch(
|
|
() => entry.modal,
|
|
(v, pv) => {
|
|
if (pv === true && v === false) {
|
|
resetEntry();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Calls either edit or create depending on entry.id being set
|
|
async function dispatchFormSubmit() {
|
|
if (entry.id) {
|
|
await editEntry();
|
|
return;
|
|
}
|
|
|
|
await createEntry();
|
|
}
|
|
|
|
async function createEntry() {
|
|
const { error } = await api.items.maintenance.create(props.item.id, {
|
|
name: entry.name,
|
|
completedDate: entry.completedDate ?? "",
|
|
scheduledDate: entry.scheduledDate ?? "",
|
|
description: entry.description,
|
|
cost: parseFloat(entry.cost) ? entry.cost : "0",
|
|
});
|
|
|
|
if (error) {
|
|
toast.error("Failed to create entry");
|
|
return;
|
|
}
|
|
|
|
entry.modal = false;
|
|
|
|
refreshLog();
|
|
resetEntry();
|
|
}
|
|
|
|
const confirm = useConfirm();
|
|
|
|
async function deleteEntry(id: string) {
|
|
const result = await confirm.open("Are you sure you want to delete this entry?");
|
|
if (result.isCanceled) {
|
|
return;
|
|
}
|
|
|
|
const { error } = await api.items.maintenance.delete(props.item.id, id);
|
|
|
|
if (error) {
|
|
toast.error("Failed to delete entry");
|
|
return;
|
|
}
|
|
refreshLog();
|
|
}
|
|
|
|
function openEditDialog(e: MaintenanceEntry) {
|
|
entry.id = e.id;
|
|
entry.name = e.name;
|
|
entry.completedDate = new Date(e.completedDate);
|
|
entry.scheduledDate = new Date(e.scheduledDate);
|
|
entry.description = e.description;
|
|
entry.cost = e.cost;
|
|
entry.modal = true;
|
|
}
|
|
|
|
async function editEntry() {
|
|
if (!entry.id) {
|
|
return;
|
|
}
|
|
|
|
const { error } = await api.items.maintenance.update(props.item.id, entry.id, {
|
|
name: entry.name,
|
|
completedDate: entry.completedDate ?? "null",
|
|
scheduledDate: entry.scheduledDate ?? "null",
|
|
description: entry.description,
|
|
cost: entry.cost,
|
|
});
|
|
|
|
if (error) {
|
|
toast.error("Failed to update entry");
|
|
return;
|
|
}
|
|
|
|
entry.modal = false;
|
|
refreshLog();
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="log">
|
|
<BaseModal v-model="entry.modal">
|
|
<template #title>
|
|
{{ entry.id ? "Edit Entry" : "New Entry" }}
|
|
</template>
|
|
<form @submit.prevent="dispatchFormSubmit">
|
|
<FormTextField v-model="entry.name" autofocus label="Entry Name" />
|
|
<DatePicker v-model="entry.scheduledDate" label="Scheduled Date" />
|
|
<DatePicker v-model="entry.completedDate" label="Completed Date" />
|
|
<FormTextField v-model="entry.cost" autofocus label="Cost" />
|
|
<FormTextArea v-model="entry.description" label="Notes" />
|
|
<div class="py-2 flex justify-end">
|
|
<BaseButton type="submit" class="ml-2 mt-2">
|
|
<template #icon>
|
|
<MdiPost />
|
|
</template>
|
|
{{ entry.id ? "Update" : "Create" }}
|
|
</BaseButton>
|
|
</div>
|
|
</form>
|
|
</BaseModal>
|
|
|
|
<section class="space-y-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<StatCard
|
|
v-for="stat in stats"
|
|
:key="stat.id"
|
|
class="stats block shadow-xl border-l-primary"
|
|
:title="stat.title"
|
|
:value="stat.value"
|
|
:type="stat.type"
|
|
/>
|
|
</div>
|
|
<div class="flex">
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm" :class="`${scheduled ? 'btn-active' : ''}`" @click="scheduled = true">
|
|
Scheduled
|
|
</button>
|
|
<button class="btn btn-sm" :class="`${scheduled ? '' : 'btn-active'}`" @click="scheduled = false">
|
|
Completed
|
|
</button>
|
|
</div>
|
|
<BaseButton class="ml-auto" size="sm" @click="newEntry()">
|
|
<template #icon>
|
|
<MdiPlus />
|
|
</template>
|
|
New
|
|
</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">
|
|
<span class="text-base-content">
|
|
{{ e.name }}
|
|
</span>
|
|
<template #description>
|
|
<div class="flex flex-wrap gap-2">
|
|
<div v-if="validDate(e.completedDate)" class="badge p-3">
|
|
<MdiCheck class="mr-2" />
|
|
<DateTime :date="e.completedDate" format="human" datetime-type="date" />
|
|
</div>
|
|
<div v-else-if="validDate(e.scheduledDate)" class="badge p-3">
|
|
<MdiCalendar class="mr-2" />
|
|
<DateTime :date="e.scheduledDate" format="human" datetime-type="date" />
|
|
</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 gap-1">
|
|
<BaseButton size="sm" @click="openEditDialog(e)">
|
|
<template #icon>
|
|
<MdiEdit />
|
|
</template>
|
|
Edit
|
|
</BaseButton>
|
|
<BaseButton size="sm" @click="deleteEntry(e.id)">
|
|
<template #icon>
|
|
<MdiDelete />
|
|
</template>
|
|
Delete
|
|
</BaseButton>
|
|
</div>
|
|
</BaseCard>
|
|
<div class="hidden first:block">
|
|
<button
|
|
type="button"
|
|
class="relative block w-full rounded-lg border-2 border-dashed border-base-content p-12 text-center"
|
|
@click="newEntry()"
|
|
>
|
|
<MdiWrenchClock class="h-16 w-16 inline" />
|
|
<span class="mt-2 block text-sm font-medium text-gray-900"> Create Your First Entry </span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|