mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
* feat: implement example of data table * feat: load item data into table * chore: begin switching dialogs * feat: implement old dialog for controlling headers and page size * feat: get table into relatively usable state * feat: enhance dropdown actions for multi-selection and CSV download * feat: enhance table cell and dropdown button styles for better usability * feat: json download for table * feat: add expanded row component for item details in data table * chore: add translation support * feat: restore table on home page * fix: oops need ids * feat: move card view to use tanstack to allow for pagination * feat: switch the items search to use ItemViewSelectable * fix: update pagination handling and improve button click logic * feat: improve selectable table * feat: add indeterminate to checkbox * feat: overhaul maintenance dialog to use new system and add maintenance options to table * feat: add label ids and location id to item patch api * feat: change location and labels in table view * feat: add quick actions preference and enable toggle in table settings * fix: lint * fix: remove sized 1 pages * fix: attempt to fix type error * fix: various issues * fix: remove * fix: refactor item fetching logic to use useAsyncData for improved reactivity and improve use confirm * fix: sort backend issues * fix: enhance CSV export functionality by escaping fields to prevent formula injection * fix: put aria sort on th not button * chore: update api types
281 lines
9.6 KiB
Vue
281 lines
9.6 KiB
Vue
<script setup lang="ts">
|
|
import { useI18n } from "vue-i18n";
|
|
import type { MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
|
|
import { MaintenanceFilterStatus } from "~~/lib/api/types/data-contracts";
|
|
import type { StatsFormat } from "~~/components/global/StatCard/types";
|
|
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 MdiPlus from "~icons/mdi/plus";
|
|
import MdiWrenchClock from "~icons/mdi/wrench-clock";
|
|
import MdiContentDuplicate from "~icons/mdi/content-duplicate";
|
|
import MaintenanceEditModal from "~~/components/Maintenance/EditModal.vue";
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button, ButtonGroup } from "@/components/ui/button";
|
|
import StatCard from "~/components/global/StatCard/StatCard.vue";
|
|
import BaseCard from "@/components/Base/Card.vue";
|
|
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
|
|
import DateTime from "~/components/global/DateTime.vue";
|
|
import Currency from "~/components/global/Currency.vue";
|
|
import Markdown from "~/components/global/Markdown.vue";
|
|
import { toast } from "@/components/ui/sonner";
|
|
import { useDialog } from "@/components/ui/dialog-provider";
|
|
import { DialogID } from "../ui/dialog-provider/utils";
|
|
|
|
const maintenanceFilterStatus = ref(MaintenanceFilterStatus.MaintenanceFilterStatusScheduled);
|
|
|
|
const api = useUserApi();
|
|
const { t } = useI18n();
|
|
const confirm = useConfirm();
|
|
const { openDialog } = useDialog();
|
|
|
|
const props = defineProps({
|
|
currentItemId: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
});
|
|
|
|
const { data: maintenanceDataList, refresh: refreshList } = useAsyncData(
|
|
async () => {
|
|
const { data } =
|
|
props.currentItemId !== undefined
|
|
? await api.items.maintenance.getLog(props.currentItemId, { status: maintenanceFilterStatus.value })
|
|
: await api.maintenance.getAll({ status: maintenanceFilterStatus.value });
|
|
console.log(data);
|
|
return data;
|
|
},
|
|
{
|
|
watch: [maintenanceFilterStatus],
|
|
}
|
|
);
|
|
|
|
const stats = computed(() => {
|
|
console.log(maintenanceDataList);
|
|
if (!maintenanceDataList.value) return [];
|
|
|
|
const count = maintenanceDataList.value ? maintenanceDataList.value.length || 0 : 0;
|
|
let total = 0;
|
|
maintenanceDataList.value.forEach(item => {
|
|
total += parseFloat(item.cost);
|
|
});
|
|
|
|
const average = count > 0 ? total / count : 0;
|
|
|
|
return [
|
|
{
|
|
id: "count",
|
|
title: t("maintenance.total_entries"),
|
|
value: count,
|
|
type: "number" as StatsFormat,
|
|
},
|
|
{
|
|
id: "total",
|
|
title: t("maintenance.total_cost"),
|
|
value: total,
|
|
type: "currency" as StatsFormat,
|
|
},
|
|
{
|
|
id: "average",
|
|
title: t("maintenance.monthly_average"),
|
|
value: average,
|
|
type: "currency" as StatsFormat,
|
|
},
|
|
];
|
|
});
|
|
|
|
async function deleteEntry(id: string) {
|
|
const result = await confirm.open(t("maintenance.modal.delete_confirmation"));
|
|
if (result.isCanceled) {
|
|
return;
|
|
}
|
|
|
|
const { error } = await api.maintenance.delete(id);
|
|
|
|
if (error) {
|
|
toast.error(t("maintenance.toast.failed_to_delete"));
|
|
return;
|
|
}
|
|
refreshList();
|
|
}
|
|
|
|
async function completeEntry(maintenanceEntry: MaintenanceEntry) {
|
|
const { error } = await api.maintenance.update(maintenanceEntry.id, {
|
|
name: maintenanceEntry.name,
|
|
completedDate: new Date(Date.now()),
|
|
scheduledDate: maintenanceEntry.scheduledDate ?? "null",
|
|
description: maintenanceEntry.description,
|
|
cost: maintenanceEntry.cost,
|
|
});
|
|
if (error) {
|
|
toast.error(t("maintenance.toast.failed_to_update"));
|
|
}
|
|
refreshList();
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="space-y-6">
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
<StatCard v-for="stat in stats" :key="stat.id" :title="stat.title" :value="stat.value" :type="stat.type" />
|
|
</div>
|
|
<div class="flex">
|
|
<ButtonGroup>
|
|
<Button
|
|
size="sm"
|
|
:variant="
|
|
maintenanceFilterStatus == MaintenanceFilterStatus.MaintenanceFilterStatusScheduled ? 'default' : 'outline'
|
|
"
|
|
@click="maintenanceFilterStatus = MaintenanceFilterStatus.MaintenanceFilterStatusScheduled"
|
|
>
|
|
{{ $t("maintenance.filter.scheduled") }}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
:variant="
|
|
maintenanceFilterStatus == MaintenanceFilterStatus.MaintenanceFilterStatusCompleted ? 'default' : 'outline'
|
|
"
|
|
@click="maintenanceFilterStatus = MaintenanceFilterStatus.MaintenanceFilterStatusCompleted"
|
|
>
|
|
{{ $t("maintenance.filter.completed") }}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
:variant="
|
|
maintenanceFilterStatus == MaintenanceFilterStatus.MaintenanceFilterStatusBoth ? 'default' : 'outline'
|
|
"
|
|
@click="maintenanceFilterStatus = MaintenanceFilterStatus.MaintenanceFilterStatusBoth"
|
|
>
|
|
{{ $t("maintenance.filter.both") }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
<Button
|
|
v-if="props.currentItemId"
|
|
class="ml-auto"
|
|
size="sm"
|
|
@click="
|
|
openDialog(DialogID.EditMaintenance, {
|
|
params: { type: 'create', itemId: props.currentItemId },
|
|
onClose: result => {
|
|
if (result) {
|
|
refreshList();
|
|
}
|
|
},
|
|
})
|
|
"
|
|
>
|
|
<MdiPlus />
|
|
{{ $t("maintenance.list.new") }}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
<section>
|
|
<!-- begin -->
|
|
<MaintenanceEditModal ref="maintenanceEditModal" @changed="refreshList" />
|
|
<div class="container space-y-6">
|
|
<BaseCard v-for="e in maintenanceDataList" :key="e.id">
|
|
<BaseSectionHeader class="border-b p-6">
|
|
<span class="mb-2">
|
|
<span v-if="!props.currentItemId">
|
|
<NuxtLink class="hover:underline" :to="`/item/${(e as MaintenanceEntryWithDetails).itemID}/maintenance`">
|
|
{{ (e as MaintenanceEntryWithDetails).itemName }}
|
|
</NuxtLink>
|
|
-
|
|
</span>
|
|
{{ e.name }}
|
|
</span>
|
|
<template #description>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Badge v-if="validDate(e.completedDate)" variant="outline">
|
|
<MdiCheck class="mr-2" />
|
|
<DateTime :date="e.completedDate" format="human" datetime-type="date" />
|
|
</Badge>
|
|
<Badge v-else-if="validDate(e.scheduledDate)" variant="outline">
|
|
<MdiCalendar class="mr-2" />
|
|
<DateTime :date="e.scheduledDate" format="human" datetime-type="date" />
|
|
</Badge>
|
|
<TooltipProvider :delay-duration="0">
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Badge>
|
|
<Currency :amount="e.cost" />
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent> Cost </TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
</template>
|
|
</BaseSectionHeader>
|
|
<div :class="{ 'p-6': e.description }">
|
|
<Markdown :source="e.description" />
|
|
</div>
|
|
<ButtonGroup class="flex flex-wrap justify-end p-4">
|
|
<Button
|
|
size="sm"
|
|
@click="
|
|
openDialog(DialogID.EditMaintenance, {
|
|
params: { type: 'update', maintenanceEntry: e },
|
|
onClose: result => {
|
|
if (result) {
|
|
refreshList();
|
|
}
|
|
},
|
|
})
|
|
"
|
|
>
|
|
<MdiEdit />
|
|
{{ $t("maintenance.list.edit") }}
|
|
</Button>
|
|
<Button v-if="!validDate(e.completedDate)" size="sm" variant="outline" @click="completeEntry(e)">
|
|
<MdiCheck />
|
|
{{ $t("maintenance.list.complete") }}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
@click="
|
|
openDialog(DialogID.EditMaintenance, {
|
|
params: { type: 'duplicate', maintenanceEntry: e, itemId: props.currentItemId! },
|
|
onClose: result => {
|
|
if (result) {
|
|
refreshList();
|
|
}
|
|
},
|
|
})
|
|
"
|
|
>
|
|
<MdiContentDuplicate />
|
|
{{ $t("maintenance.list.duplicate") }}
|
|
</Button>
|
|
<Button size="sm" variant="destructive" @click="deleteEntry(e.id)">
|
|
<MdiDelete />
|
|
{{ $t("maintenance.list.delete") }}
|
|
</Button>
|
|
</ButtonGroup>
|
|
</BaseCard>
|
|
<div v-if="props.currentItemId" class="hidden first:block">
|
|
<button
|
|
type="button"
|
|
class="relative block w-full rounded-lg border-2 border-dashed p-12 text-center"
|
|
@click="
|
|
openDialog(DialogID.EditMaintenance, {
|
|
params: { type: 'create', itemId: props.currentItemId },
|
|
onClose: result => {
|
|
if (result) {
|
|
refreshList();
|
|
}
|
|
},
|
|
})
|
|
"
|
|
>
|
|
<MdiWrenchClock class="inline size-16" />
|
|
<span class="mt-2 block text-sm font-medium text-gray-900"> {{ $t("maintenance.list.create_first") }} </span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|