feat: new dashboard implementation (#168)

* wip: charts.js experimental work

* update lock file

* wip: frontend redesign

* wip: more UI fixes for consistency across themes

* cleanup

* improve UI log

* style updates

* fix lint errors
This commit is contained in:
Hayden 2022-12-29 17:19:15 -08:00 committed by GitHub
parent a3954dab0f
commit 6a8a25e3f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1690 additions and 296 deletions

View file

@ -0,0 +1,71 @@
import { TChartData } from "vue-chartjs/dist/types";
import { UserClient } from "~~/lib/api/user";
export function purchasePriceOverTimeChart(api: UserClient) {
const { data: timeseries } = useAsyncData(async () => {
const { data } = await api.stats.totalPriceOverTime();
return data;
});
const primary = useCssVar("--p");
return computed(() => {
if (!timeseries.value) {
return {
labels: ["Purchase Price"],
datasets: [
{
label: "Purchase Price",
data: [],
backgroundColor: primary.value,
borderColor: primary.value,
},
],
} as TChartData<"line", number[], unknown>;
}
let start = timeseries.value?.valueAtStart;
return {
labels: timeseries?.value.entries.map(t => new Date(t.date).toDateString()) || [],
datasets: [
{
label: "Purchase Price",
data:
timeseries.value?.entries.map(t => {
start += t.value;
return start;
}) || [],
backgroundColor: primary.value,
borderColor: primary.value,
},
],
} as TChartData<"line", number[], unknown>;
});
}
export function inventoryByLocationChart(api: UserClient) {
const { data: donutSeries } = useAsyncData(async () => {
const { data } = await api.stats.locations();
return data;
});
const primary = useCssVar("--p");
const secondary = useCssVar("--s");
const neutral = useCssVar("--n");
return computed(() => {
return {
labels: donutSeries.value?.map(l => l.name) || [],
datasets: [
{
label: "Value",
data: donutSeries.value?.map(l => l.total) || [],
backgroundColor: [primary.value, secondary.value, neutral.value],
borderColor: [primary.value, secondary.value, neutral.value],
hoverOffset: 4,
},
],
};
});
}

View file

@ -0,0 +1,120 @@
<script setup lang="ts">
import { statCardData } from "./statistics";
import { itemsTable } from "./table";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
definePageMeta({
middleware: ["auth"],
});
useHead({
title: "Homebox | Home",
});
const api = useUserApi();
const breakpoints = useBreakpoints();
const locationStore = useLocationStore();
const locations = computed(() => locationStore.parentLocations);
const labelsStore = useLabelStore();
const labels = computed(() => labelsStore.labels);
const itemTable = itemsTable(api);
const stats = statCardData(api);
// const purchasePriceOverTime = purchasePriceOverTimeChart(api);
// const inventoryByLocation = inventoryByLocationChart(api);
// const refDonutEl = ref<HTMLDivElement>();
// const donutElWidth = computed(() => {
// return refDonutEl.value?.clientWidth || 0;
// });
</script>
<template>
<div>
<BaseContainer class="flex flex-col gap-12 pb-16">
<!-- <section class="grid grid-cols-6 gap-6">
<article class="col-span-4">
<Subtitle> Inventory Value Over Time </Subtitle>
<BaseCard>
<div class="p-10 h-[300px]">
<ClientOnly>
<ChartLine :chart-data="purchasePriceOverTime" />
</ClientOnly>
</div>
</BaseCard>
</article>
<article class="col-span-2">
<Subtitle>
Inventory By
<span class="btn-group">
<button class="btn btn-xs btn-active text-no-transform">Locations</button>
<button class="btn btn-xs text-no-transform">Labels</button>
</span>
</Subtitle>
<BaseCard class="h-[300px]">
<div ref="refDonutEl" class="grid place-content-center h-full">
<ClientOnly>
<ChartDonut
chart-id="donut"
:width="donutElWidth - 50"
:height="265"
:chart-data="inventoryByLocation"
/>
</ClientOnly>
</div>
</BaseCard>
</article>
</section> -->
<section>
<Subtitle> Quick Statistics </Subtitle>
<div class="grid grid-cols-2 gap-2 md:grid-cols-4 md:gap-6">
<StatCard v-for="(stat, i) in stats" :key="i" :title="stat.label" :value="stat.value" :type="stat.type" />
</div>
</section>
<section>
<Subtitle> Recently Added </Subtitle>
<BaseCard v-if="breakpoints.lg">
<Table :headers="itemTable.headers" :data="itemTable.items">
<template #cell-warranty="{ item }">
<Icon v-if="item.warranty" name="mdi-check" class="text-green-500 h-5 w-5" />
<Icon v-else name="mdi-close" class="text-red-500 h-5 w-5" />
</template>
<template #cell-purchasePrice="{ item }">
<Currency :amount="item.purchasePrice" />
</template>
<template #cell-location_Name="{ item }">
<NuxtLink class="badge badge-sm badge-primary p-3" :to="`/location/${item.location.id}`">
{{ item.location?.name }}
</NuxtLink>
</template>
</Table>
</BaseCard>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
<ItemCard v-for="item in itemTable.items" :key="item.id" :item="item" />
</div>
</section>
<section>
<Subtitle> Storage Locations </Subtitle>
<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>
<Subtitle> Labels </Subtitle>
<div class="flex gap-4 flex-wrap">
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" class="shadow-md" />
</div>
</section>
</BaseContainer>
</div>
</template>

View file

@ -0,0 +1,39 @@
import { UserClient } from "~~/lib/api/user";
type StatCard = {
label: string;
value: number;
type: "currency" | "number";
};
export function statCardData(api: UserClient) {
const { data: statistics } = useAsyncData(async () => {
const { data } = await api.stats.group();
return data;
});
return computed(() => {
return [
{
label: "Total Value",
value: statistics.value?.totalItemPrice || 0,
type: "currency",
},
{
label: "Total Items",
value: statistics.value?.totalItems || 0,
type: "number",
},
{
label: "Total Locations",
value: statistics.value?.totalLocations || 0,
type: "number",
},
{
label: "Total Labels",
value: statistics.value?.totalLabels || 0,
type: "number",
},
] as StatCard[];
});
}

View file

@ -0,0 +1,42 @@
import { TableHeader } from "~~/components/global/Table.types";
import { UserClient } from "~~/lib/api/user";
export function itemsTable(api: UserClient) {
const { data: items } = useAsyncData(async () => {
const { data } = await api.items.getAll({
page: 1,
pageSize: 5,
});
return data.items;
});
const headers = [
{
text: "Name",
sortable: true,
value: "name",
},
{
text: "Location",
value: "location.name",
},
{
text: "Warranty",
value: "warranty",
align: "center",
},
{
text: "Price",
value: "purchasePrice",
align: "center",
},
] as TableHeader[];
return computed(() => {
return {
headers,
items: items.value || [],
};
});
}