forked from mirrors/homebox
90813abf76
implenmented a store for each global item and tied it to an event bus and used the listener on the requests object to intercept responses from the API and run the appripriate get call when updates were detected.
180 lines
5.4 KiB
Vue
180 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import { useItemStore } from "~~/stores/items";
|
|
import { useLabelStore } from "~~/stores/labels";
|
|
import { useLocationStore } from "~~/stores/locations";
|
|
|
|
definePageMeta({
|
|
layout: "home",
|
|
});
|
|
useHead({
|
|
title: "Homebox | Home",
|
|
});
|
|
|
|
const api = useUserApi();
|
|
|
|
const itemsStore = useItemStore();
|
|
const items = computed(() => itemsStore.items);
|
|
|
|
const locationStore = useLocationStore();
|
|
const locations = computed(() => locationStore.locations);
|
|
|
|
const labelsStore = useLabelStore();
|
|
const labels = computed(() => labelsStore.labels);
|
|
|
|
const totalItems = computed(() => items.value?.length || 0);
|
|
const totalLocations = computed(() => locations.value?.length || 0);
|
|
const totalLabels = computed(() => labels.value?.length || 0);
|
|
|
|
const stats = [
|
|
{
|
|
label: "Locations",
|
|
value: totalLocations,
|
|
},
|
|
{
|
|
label: "Items",
|
|
value: totalItems,
|
|
},
|
|
{
|
|
label: "Labels",
|
|
value: totalLabels,
|
|
},
|
|
];
|
|
|
|
const importDialog = ref(false);
|
|
const importCsv = ref(null);
|
|
const importLoading = ref(false);
|
|
const importRef = ref<HTMLInputElement>(null);
|
|
whenever(
|
|
() => !importDialog.value,
|
|
() => {
|
|
importCsv.value = null;
|
|
}
|
|
);
|
|
|
|
function setFile(e: Event & { target: HTMLInputElement }) {
|
|
importCsv.value = e.target.files[0];
|
|
}
|
|
|
|
const toast = useNotifier();
|
|
|
|
function openDialog() {
|
|
importDialog.value = true;
|
|
}
|
|
|
|
function uploadCsv() {
|
|
importRef.value.click();
|
|
}
|
|
|
|
const eventBus = useEventBus();
|
|
|
|
async function submitCsvFile() {
|
|
importLoading.value = true;
|
|
|
|
const { error } = await api.items.import(importCsv.value);
|
|
|
|
if (error) {
|
|
toast.error("Import failed. Please try again later.");
|
|
}
|
|
|
|
// Reset
|
|
importDialog.value = false;
|
|
importLoading.value = false;
|
|
importCsv.value = null;
|
|
importRef.value.value = null;
|
|
|
|
eventBus.emit(EventTypes.ClearStores);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<BaseContainer class="space-y-16 pb-16">
|
|
<BaseModal v-model="importDialog">
|
|
<template #title> Import CSV File </template>
|
|
<p>
|
|
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
|
|
required format.
|
|
</p>
|
|
|
|
<form @submit.prevent="submitCsvFile">
|
|
<div class="flex flex-col gap-2 py-6">
|
|
<input ref="importRef" type="file" class="hidden" accept=".csv" @change="setFile" />
|
|
<BaseButton type="button" @click="uploadCsv">
|
|
<Icon class="h-5 w-5 mr-2" name="mdi-upload" />
|
|
Upload
|
|
</BaseButton>
|
|
<p class="text-center pt-4 -mb-5">
|
|
{{ importCsv?.name }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
|
|
</div>
|
|
</form>
|
|
</BaseModal>
|
|
|
|
<section aria-labelledby="profile-overview-title" class="mt-8">
|
|
<div class="overflow-hidden rounded-lg bg-white shadow">
|
|
<h2 id="profile-overview-title" class="sr-only">Profile Overview</h2>
|
|
<div class="bg-white p-6">
|
|
<div class="sm:flex sm:items-center sm:justify-between">
|
|
<div class="sm:flex sm:space-x-5">
|
|
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
|
|
<p class="text-sm font-medium text-gray-600">Welcome back,</p>
|
|
<p class="text-xl font-bold text-gray-900 sm:text-2xl">Hayden Kotelman</p>
|
|
<p class="text-sm font-medium text-gray-600">User</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5 flex justify-center sm:mt-0">
|
|
<a
|
|
href="#"
|
|
class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
|
|
>View profile</a
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="grid grid-cols-1 divide-y divide-gray-200 border-t border-gray-200 bg-gray-50 sm:grid-cols-3 sm:divide-y-0 sm:divide-x"
|
|
>
|
|
<div v-for="stat in stats" :key="stat.label" class="px-6 py-5 text-center text-sm font-medium">
|
|
<span class="text-gray-900">{{ stat.value.value }}</span>
|
|
{{ " " }}
|
|
<span class="text-gray-600">{{ stat.label }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
|
<LocationCard v-for="location in locations" :key="location.id" :location="location" />
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<BaseSectionHeader class="mb-5">
|
|
Items
|
|
<template #description>
|
|
<div class="tooltip" data-tip="Import CSV File">
|
|
<button class="btn btn-primary btn-sm" @click="openDialog">
|
|
<Icon name="mdi-database" class="mr-2"></Icon>
|
|
Import
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</BaseSectionHeader>
|
|
<div class="grid sm:grid-cols-2 gap-4">
|
|
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
|
|
</div>
|
|
</section>
|
|
</BaseContainer>
|
|
</template>
|