223 lines
8.9 KiB
TypeScript
223 lines
8.9 KiB
TypeScript
"use client";
|
|
import LoadingSpinner from "@/components/ui/spinner";
|
|
import { api } from "@/lib/trpc";
|
|
import { use, useEffect, useRef, useState } from "react";
|
|
import spacetime from "spacetime";
|
|
import { predefinedRanges } from "@/lib/dates";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { parseISO, format as fmt } from "date-fns";
|
|
import { DatePickerInput } from '@mantine/dates';
|
|
import { Calendar1, MenuIcon } from "lucide-react";
|
|
import { Anchor, Button, Menu } from "@mantine/core";
|
|
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
|
import PieChart from "./PieChart";
|
|
|
|
import { useTimezone } from "@/lib/userLocalSettings/client";
|
|
|
|
const parseDateRangeFromQuery = (): Date[] | undefined => {
|
|
const searchParams = useSearchParams();
|
|
if (!searchParams.has("dateRange")) return undefined;
|
|
|
|
const range = searchParams.get("dateRange")!
|
|
.split(",")
|
|
.map((date) => parseISO(date));
|
|
return range;
|
|
|
|
}
|
|
|
|
const updateHistory = (dateRange: Date[]) => {
|
|
const start = dateRange[0];
|
|
const end = dateRange[1];
|
|
const startStr = spacetime(start).format("iso-short");
|
|
const endStr = spacetime(end).format("iso-short");
|
|
const searchParams = new URLSearchParams();
|
|
searchParams.set("dateRange", `${startStr},${endStr}`);
|
|
history.replaceState(null, "", `?${searchParams.toString()}`);
|
|
};
|
|
|
|
export default function AnalyticsView() {
|
|
const datePickerRef = useRef<HTMLButtonElement>(null);
|
|
|
|
const { data: metrics } = api.metrics.list.useQuery();
|
|
const drugsList = metrics?.filter((metric) => metric.type === "drug");
|
|
|
|
const initialDateRange: [Date, Date] = (() => {
|
|
const range = parseDateRangeFromQuery();
|
|
if (range && range.length === 2) {
|
|
return range as [Date, Date];
|
|
}
|
|
return [
|
|
// spacetime.now().subtract(1, "week").toNativeDate(),
|
|
parseISO(spacetime.now(useTimezone()).format("iso-short")),
|
|
parseISO(spacetime.now(useTimezone()).format("iso-short")),
|
|
];
|
|
})();
|
|
|
|
const [dateRange, setDateRange] = useState(initialDateRange);
|
|
const [datePickerRange, setDatePickerRange] = useState(initialDateRange);
|
|
|
|
useEffect(() => {
|
|
if (datePickerRef.current?.getAttribute("aria-expanded") === "false") {
|
|
setDateRange(datePickerRange);
|
|
updateHistory(datePickerRange);
|
|
}
|
|
}, [datePickerRange]);
|
|
|
|
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
|
// TooltipInPortal is rendered in a separate child of <body /> and positioned
|
|
// with page coordinates which should be updated on scroll. consider using
|
|
// Tooltip or TooltipWithBounds if you don't need to render inside a Portal
|
|
scroll: true,
|
|
});
|
|
|
|
const categoryFrequencies = api.hours.categoryFrequencies.useQuery({
|
|
dateRange,
|
|
timezone: useTimezone(),
|
|
}).data ?? [];
|
|
|
|
const weightData = api.measurements.getTimeseries.useQuery({
|
|
dateRange,
|
|
timezone: useTimezone(),
|
|
metricName: "weight"
|
|
}).data ?? [];
|
|
|
|
return (
|
|
<div className="px-4 flex flex-col gap-y-4">
|
|
<div className="flex justify-between">
|
|
<h1>Analytics for
|
|
{dateRange[0].getUTCDate() == dateRange[1].getUTCDate()
|
|
? spacetime(dateRange[0]).unixFmt("MMM dd, yyyy")
|
|
: dateRange.map((d) => spacetime(d).unixFmt("MMM dd, yyyy")).join(" to ")
|
|
}
|
|
</h1>
|
|
{/* <DateRangePicker
|
|
showOneCalendar
|
|
appearance="subtle"
|
|
size="lg"
|
|
ranges={predefinedRanges}
|
|
character=" – "
|
|
format="MMM dd, yyyy"
|
|
value={dateRange}
|
|
onChange={setDateRange}
|
|
style={{ width: 300, textAlignLast: "center" }}
|
|
placement="bottomEnd"
|
|
renderValue={(value, format) => {
|
|
if (fmt(value[0], format) === fmt(value[1], format)) {
|
|
return fmt(value[0], format);
|
|
}
|
|
return `${fmt(value[0], format)} - ${fmt(value[1], format)}`;
|
|
}}
|
|
/> */}
|
|
<div className="flex">
|
|
<DatePickerInput
|
|
ref={datePickerRef}
|
|
type="range"
|
|
value={datePickerRange}
|
|
onChange={setDatePickerRange}
|
|
classNames={{
|
|
calendarHeader: "text-accent",
|
|
}}
|
|
highlightToday
|
|
allowSingleDateInRange
|
|
leftSection={<Calendar1 size={18} strokeWidth={1} />}
|
|
leftSectionPointerEvents="none"
|
|
/>
|
|
<Menu position="bottom-end">
|
|
<Menu.Target>
|
|
<Button>
|
|
<MenuIcon size={18} />
|
|
</Button>
|
|
</Menu.Target>
|
|
|
|
<Menu.Dropdown>
|
|
{predefinedRanges.map((range) => (
|
|
<Menu.Item
|
|
className="py-1 px-2"
|
|
onClick={() => setDatePickerRange(range.value)}
|
|
>
|
|
<Anchor
|
|
size="xs"
|
|
>
|
|
{range.label}
|
|
</Anchor>
|
|
</Menu.Item>
|
|
))}
|
|
</Menu.Dropdown>
|
|
</Menu></div>
|
|
</div>
|
|
|
|
<div className="flex ">
|
|
<PieChart width={500} height={500}
|
|
data={categoryFrequencies}
|
|
/>
|
|
<div className="grow">
|
|
<div className="grid font-bold border-b border-gray-200"
|
|
style={{
|
|
gridTemplateColumns: "1fr 1fr"
|
|
}}
|
|
>
|
|
<div className="whitespace-nowrap ">
|
|
Total
|
|
</div>
|
|
<div>
|
|
{categoryFrequencies.reduce((acc, category) => acc + category.count, 0)} hours
|
|
</div></div>
|
|
{
|
|
categoryFrequencies
|
|
.sort((a, b) => b.count - a.count)
|
|
.map((category, i) => (
|
|
|
|
<div key={i} className="grid"
|
|
style={{
|
|
gridTemplateColumns: "1fr 1fr"
|
|
}}
|
|
>
|
|
<div className="whitespace-nowrap">
|
|
{category.categoryName ?? "[Unallocated]"}
|
|
</div>
|
|
<div>
|
|
{category.count} hours
|
|
</div></div>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-4">
|
|
<h2>Weight</h2>
|
|
<div>
|
|
{
|
|
!weightData ? <LoadingSpinner /> :
|
|
<ul>
|
|
{weightData.map((measurement) => (
|
|
<li key={measurement.id}>
|
|
{measurement.value} {measurement.unit}
|
|
,
|
|
{
|
|
spacetime(measurement.datetime)
|
|
.goto(useTimezone())
|
|
.format("{iso-short} at {hour} {ampm}")
|
|
}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-4">
|
|
<h2 className="">Drugs</h2>
|
|
<div>
|
|
{
|
|
!drugsList ? <LoadingSpinner /> :
|
|
<ul>
|
|
{drugsList.map((drug) => (
|
|
<li key={drug.id}>
|
|
{drug.name}:
|
|
</li>
|
|
))}
|
|
</ul>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div >
|
|
);
|
|
} |