Update timezone logic, pull correct hours for AnalyticsView
This commit is contained in:
parent
aa4a843349
commit
6e96eed3f1
@ -4,6 +4,8 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import DayView from "@/components/dashboard/days/DayView";
|
import DayView from "@/components/dashboard/days/DayView";
|
||||||
import { getServerAuthSession } from "@/server/auth";
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
import LoadingSpinner from "@/components/ui/spinner";
|
import LoadingSpinner from "@/components/ui/spinner";
|
||||||
|
import { useTimezone } from "@/lib/userLocalSettings/client";
|
||||||
|
import { getUserLocalSettings } from "@/lib/userLocalSettings/userLocalSettings";
|
||||||
|
|
||||||
export default async function DayPage({ params }: { params: { dateQuery: string }; }) {
|
export default async function DayPage({ params }: { params: { dateQuery: string }; }) {
|
||||||
const session = await getServerAuthSession();
|
const session = await getServerAuthSession();
|
||||||
@ -11,15 +13,12 @@ export default async function DayPage({ params }: { params: { dateQuery: string
|
|||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let day;
|
let day;
|
||||||
const { dateQuery } = await params;
|
|
||||||
const timezone = await api.users.getTimezone();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
day = await api.days.get({
|
day = await api.days.get({
|
||||||
dateQuery: dateQuery,
|
dateQuery: (await params).dateQuery,
|
||||||
timezone: timezone,
|
timezone: (await getUserLocalSettings()).timezone,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof TRPCError) {
|
if (e instanceof TRPCError) {
|
||||||
@ -31,7 +30,7 @@ export default async function DayPage({ params }: { params: { dateQuery: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div className="container">
|
||||||
{
|
{
|
||||||
day == undefined ?
|
day == undefined ?
|
||||||
<LoadingSpinner /> :
|
<LoadingSpinner /> :
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import { getServerAuthSession } from "@/server/auth";
|
|||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
import { clientConfig } from "@lifetracker/shared/config";
|
import { clientConfig } from "@lifetracker/shared/config";
|
||||||
|
import { useTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
||||||
|
import { getUserLocalSettings } from "@/lib/userLocalSettings/userLocalSettings";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@ -61,9 +63,7 @@ export default async function RootLayout({
|
|||||||
session={session}
|
session={session}
|
||||||
clientConfig={clientConfig}
|
clientConfig={clientConfig}
|
||||||
userLocalSettings={
|
userLocalSettings={
|
||||||
parseUserLocalSettings(
|
await getUserLocalSettings()
|
||||||
(await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME)?.value
|
|
||||||
) ?? defaultUserLocalSettings()
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -4,27 +4,27 @@ import Link from "next/link";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { TZDate } from "@date-fns/tz";
|
import { TZDate } from "@date-fns/tz";
|
||||||
import { timezones } from '@lifetracker/shared/utils/timezones';
|
import { timezones } from '@lifetracker/shared/utils/timezones';
|
||||||
import { useTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
|
||||||
import LoadingSpinner from "@/components/ui/spinner";
|
import LoadingSpinner from "@/components/ui/spinner";
|
||||||
import { db } from "@lifetracker/db";
|
import { db } from "@lifetracker/db";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import spacetime from "spacetime";
|
import spacetime from "spacetime";
|
||||||
|
import { useTimezone } from "@/lib/userLocalSettings/client";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function TimezoneDisplay() {
|
export default function TimezoneDisplay() {
|
||||||
const clientTime = spacetime.now();
|
const timezone = useTimezone();
|
||||||
return (
|
return (
|
||||||
<div className="text-center w-full">
|
<div className="text-center w-full">
|
||||||
<Link href="/settings/app">
|
<Link href="/settings/app">
|
||||||
<b className="whitespace-nowrap">
|
<b className="whitespace-nowrap">
|
||||||
{clientTime.format('nice')}
|
{format(TZDate.tz(timezone), 'MMM dd, hh:mm aa')}
|
||||||
</b>
|
</b>
|
||||||
<br />
|
<br />
|
||||||
<span>in </span>
|
<span>in </span>
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
<b>
|
<b>
|
||||||
{timezones[clientTime.timezone().name] || clientTime.timezone().name}
|
{timezones[timezone] || "Unknown Timezone"}
|
||||||
</b>
|
</b>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useBookmarkLayout } from "@/lib/userLocalSettings/bookmarksLayout";
|
import { useBookmarkLayout } from "@/lib/userLocalSettings/client";
|
||||||
import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
|
import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import { Anchor, Button, Menu } from "@mantine/core";
|
|||||||
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
||||||
import PieChart from "./PieChart";
|
import PieChart from "./PieChart";
|
||||||
|
|
||||||
|
import { useTimezone } from "@/lib/userLocalSettings/client";
|
||||||
|
|
||||||
const parseDateRangeFromQuery = (): Date[] | undefined => {
|
const parseDateRangeFromQuery = (): Date[] | undefined => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
if (!searchParams.has("dateRange")) return undefined;
|
if (!searchParams.has("dateRange")) return undefined;
|
||||||
@ -46,8 +48,8 @@ export default function AnalyticsView() {
|
|||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
// spacetime.now().subtract(1, "week").toNativeDate(),
|
// spacetime.now().subtract(1, "week").toNativeDate(),
|
||||||
spacetime.now().toNativeDate(),
|
spacetime.now(useTimezone()).toNativeDate(),
|
||||||
spacetime.now().toNativeDate()
|
spacetime.now(useTimezone()).toNativeDate()
|
||||||
];
|
];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -70,14 +72,18 @@ export default function AnalyticsView() {
|
|||||||
|
|
||||||
const categoryFrequencies = api.hours.categoryFrequencies.useQuery({
|
const categoryFrequencies = api.hours.categoryFrequencies.useQuery({
|
||||||
dateRange,
|
dateRange,
|
||||||
|
timezone: useTimezone(),
|
||||||
}).data ?? [];
|
}).data ?? [];
|
||||||
|
|
||||||
console.log(categoryFrequencies);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 flex flex-col gap-y-4">
|
<div className="px-4 flex flex-col gap-y-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<h1>Analytics</h1>
|
<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
|
{/* <DateRangePicker
|
||||||
showOneCalendar
|
showOneCalendar
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
@ -139,6 +145,17 @@ export default function AnalyticsView() {
|
|||||||
data={categoryFrequencies}
|
data={categoryFrequencies}
|
||||||
/>
|
/>
|
||||||
<div className="grow">
|
<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
|
categoryFrequencies
|
||||||
.sort((a, b) => b.count - a.count)
|
.sort((a, b) => b.count - a.count)
|
||||||
@ -150,7 +167,7 @@ export default function AnalyticsView() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="whitespace-nowrap">
|
<div className="whitespace-nowrap">
|
||||||
{category.categoryName}
|
{category.categoryName ?? "[Unallocated]"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{category.count} hours
|
{category.count} hours
|
||||||
|
|||||||
@ -33,8 +33,6 @@ export default function PieChart({
|
|||||||
const centerY = innerHeight / 2;
|
const centerY = innerHeight / 2;
|
||||||
const centerX = innerWidth / 2;
|
const centerX = innerWidth / 2;
|
||||||
|
|
||||||
// console.log(data);
|
|
||||||
|
|
||||||
return data.length == 0 ? <LoadingSpinner /> : (
|
return data.length == 0 ? <LoadingSpinner /> : (
|
||||||
<svg width={width} height={height}>
|
<svg width={width} height={height}>
|
||||||
<rect rx={14} width={width} height={height} fill="transparent" />
|
<rect rx={14} width={width} height={height} fill="transparent" />
|
||||||
@ -55,7 +53,8 @@ export default function PieChart({
|
|||||||
console.log('clicked: ', category.categoryName)
|
console.log('clicked: ', category.categoryName)
|
||||||
}
|
}
|
||||||
getColor={(arc) => {
|
getColor={(arc) => {
|
||||||
return arc.data.categoryColor;
|
if (arc.data.background == "inherit") { return "color-mix(in srgb, hsl(var(--muted)) 95%, black 5%)" };
|
||||||
|
return arc.data.background ?? 'transparent';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -142,7 +141,7 @@ function AnimatedPie<Datum>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getForeground(arc: PieArcDatum<Datum>) {
|
function getForeground(arc: PieArcDatum<Datum>) {
|
||||||
return arc.data.categoryForeground;
|
return arc.data.foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import GlobalActions from "@/components/dashboard/GlobalActions";
|
|||||||
import ProfileOptions from "@/components/dashboard/header/ProfileOptions";
|
import ProfileOptions from "@/components/dashboard/header/ProfileOptions";
|
||||||
import HoarderLogo from "@/components/HoarderIcon";
|
import HoarderLogo from "@/components/HoarderIcon";
|
||||||
import { getServerAuthSession } from "@/server/auth";
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
|
import TimezoneDisplay from "../../TimezoneDisplay";
|
||||||
|
|
||||||
export default async function Header() {
|
export default async function Header() {
|
||||||
const session = await getServerAuthSession();
|
const session = await getServerAuthSession();
|
||||||
@ -19,7 +20,7 @@ export default async function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 gap-2">
|
<div className="flex flex-1 gap-2">
|
||||||
<GlobalActions />
|
<TimezoneDisplay />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ProfileOptions />
|
<ProfileOptions />
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Icon } from "@/components/ui/icon";
|
import { Icon } from "@/components/ui/icon";
|
||||||
import { titleCase } from "title-case";
|
import { titleCase } from "title-case";
|
||||||
import { useDecrementCount } from "@lifetracker/shared-react/hooks/measurements";
|
import { useDecrementCount } from "@lifetracker/shared-react/hooks/measurements";
|
||||||
|
import { useTimezone } from "@/lib/userLocalSettings/client";
|
||||||
|
|
||||||
|
|
||||||
export default function EditableHour({
|
export default function EditableHour({
|
||||||
hour: initialHour,
|
hour: initialHour,
|
||||||
@ -97,7 +99,7 @@ export default function EditableHour({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const tzOffset = spacetime().offset() / 60;
|
const tzOffset = spacetime.now(useTimezone()).offset() / 60;
|
||||||
|
|
||||||
|
|
||||||
const localDateTime = (h: ZHour): string => {
|
const localDateTime = (h: ZHour): string => {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Archive, ArrowRightFromLine, Calendar, CheckCheck, Gauge, GaugeCircleIc
|
|||||||
import serverConfig from "@lifetracker/shared/config";
|
import serverConfig from "@lifetracker/shared/config";
|
||||||
|
|
||||||
import AllLists from "./AllLists";
|
import AllLists from "./AllLists";
|
||||||
import TimezoneDisplay from "./TimezoneDisplay";
|
import TimezoneDisplay from "../../TimezoneDisplay";
|
||||||
import { ActionButtonWithTooltip } from "@/components/ui/action-button";
|
import { ActionButtonWithTooltip } from "@/components/ui/action-button";
|
||||||
|
|
||||||
export default async function Sidebar() {
|
export default async function Sidebar() {
|
||||||
@ -76,8 +76,8 @@ export default async function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="mt-auto flex items-center border-t pt-2 text-sm text-gray-400">
|
<div className="mt-auto flex border-t pt-2 text-sm text-gray-400">
|
||||||
<TimezoneDisplay />
|
<span className="text-center w-full">Server version {serverConfig.serverVersion}</span>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
}
|
}
|
||||||
@ -108,6 +108,6 @@ export default async function Sidebar() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsedView />
|
<FullView />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default async function AppSettings() {
|
|||||||
<div className="mb-2" >
|
<div className="mb-2" >
|
||||||
<div className="mb-2 text-sm font-medium">Timezone</div>
|
<div className="mb-2 text-sm font-medium">Timezone</div>
|
||||||
<div className="select-wrapper">
|
<div className="select-wrapper">
|
||||||
|
|
||||||
<ChangeTimezone
|
<ChangeTimezone
|
||||||
userTimezone={userTimezone}
|
userTimezone={userTimezone}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { timezones } from "@lifetracker/shared/utils/timezones";
|
import { timezones } from "@lifetracker/shared/utils/timezones";
|
||||||
import TimezoneSelect, { type ITimezone } from 'react-timezone-select'
|
import TimezoneSelect, { type ITimezone } from 'react-timezone-select'
|
||||||
import { useUpdateUserTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
import { useUpdateUserTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
||||||
|
import { updateTimezoneCookie } from "@/lib/userLocalSettings/userLocalSettings";
|
||||||
|
|
||||||
export default function ChangeTimezone({ userTimezone }) {
|
export default function ChangeTimezone({ userTimezone }) {
|
||||||
const [selectedTimezone, setSelectedTimezone] = useState<ITimezone>(userTimezone);
|
const [selectedTimezone, setSelectedTimezone] = useState<ITimezone>(userTimezone);
|
||||||
const { mutate: updateUserTimezone, isPending } = useUpdateUserTimezone({
|
const { mutate: updateUserTimezone, isPending } = useUpdateUserTimezone({
|
||||||
onSuccess: () => {
|
onSuccess: (_data, { newTimezone }) => {
|
||||||
|
updateTimezoneCookie(newTimezone);
|
||||||
toast({
|
toast({
|
||||||
description: "User DB Timezone updated!",
|
description: "User DB Timezone updated!",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { getServerAuthSession } from "@/server/auth";
|
|||||||
import serverConfig from "@lifetracker/shared/config";
|
import serverConfig from "@lifetracker/shared/config";
|
||||||
|
|
||||||
import { settingsSidebarItems } from "./items";
|
import { settingsSidebarItems } from "./items";
|
||||||
import TimezoneDisplay from "@/components/dashboard/sidebar/TimezoneDisplay";
|
import TimezoneDisplay from "@/components/TimezoneDisplay";
|
||||||
|
|
||||||
export default async function Sidebar() {
|
export default async function Sidebar() {
|
||||||
const session = await getServerAuthSession();
|
const session = await getServerAuthSession();
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
|||||||
|
|
||||||
import { DatesProvider } from "@mantine/dates";
|
import { DatesProvider } from "@mantine/dates";
|
||||||
|
|
||||||
import { UserLocalSettingsCtx } from "@/lib/userLocalSettings/bookmarksLayout";
|
import { UserLocalSettingsCtx } from "@/lib/userLocalSettings/client";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { httpBatchLink, loggerLink } from "@trpc/client";
|
import { httpBatchLink, loggerLink } from "@trpc/client";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
|||||||
@ -3,20 +3,27 @@
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
import type { BookmarksLayoutTypes, zUserLocalSettings } from "./types";
|
import type { BookmarksLayoutTypes, Timezones, zUserLocalSettings } from "./types";
|
||||||
|
|
||||||
const defaultLayout: BookmarksLayoutTypes = "masonry";
|
const defaultLayout: BookmarksLayoutTypes = "masonry";
|
||||||
|
const defaultTimezone: Timezones = "Etc/UTC";
|
||||||
|
|
||||||
export const UserLocalSettingsCtx = createContext<
|
export const UserLocalSettingsCtx = createContext<
|
||||||
z.infer<typeof zUserLocalSettings>
|
z.infer<typeof zUserLocalSettings>
|
||||||
>({
|
>({
|
||||||
bookmarkGridLayout: defaultLayout,
|
bookmarkGridLayout: defaultLayout,
|
||||||
|
timezone: defaultTimezone,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useUserLocalSettings() {
|
function useUserLocalSettings() {
|
||||||
return useContext(UserLocalSettingsCtx);
|
return useContext(UserLocalSettingsCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTimezone() {
|
||||||
|
const settings = useUserLocalSettings();
|
||||||
|
return settings.timezone;
|
||||||
|
}
|
||||||
|
|
||||||
export function useBookmarkLayout() {
|
export function useBookmarkLayout() {
|
||||||
const settings = useUserLocalSettings();
|
const settings = useUserLocalSettings();
|
||||||
return settings.bookmarkGridLayout;
|
return settings.bookmarkGridLayout;
|
||||||
@ -1,12 +1,19 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { timezones } from "@lifetracker/shared/utils/timezones";
|
||||||
|
|
||||||
export const USER_LOCAL_SETTINGS_COOKIE_NAME = "hoarder-user-local-settings";
|
export const USER_LOCAL_SETTINGS_COOKIE_NAME = "lifetracker-user-local-settings";
|
||||||
|
|
||||||
const zBookmarkGridLayout = z.enum(["grid", "list", "masonry", "compact"]);
|
const zBookmarkGridLayout = z.enum(["grid", "list", "masonry", "compact"]);
|
||||||
export type BookmarksLayoutTypes = z.infer<typeof zBookmarkGridLayout>;
|
export type BookmarksLayoutTypes = z.infer<typeof zBookmarkGridLayout>;
|
||||||
|
|
||||||
|
const zTimezone = z.enum(
|
||||||
|
["", ...Object.keys(timezones)] as [string, ...string[]]
|
||||||
|
);
|
||||||
|
export type Timezones = z.infer<typeof zTimezone>;
|
||||||
|
|
||||||
export const zUserLocalSettings = z.object({
|
export const zUserLocalSettings = z.object({
|
||||||
bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
|
bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
|
||||||
|
timezone: zTimezone.optional().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;
|
export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
import type { BookmarksLayoutTypes } from "./types";
|
import type { BookmarksLayoutTypes, Timezones } from "./types";
|
||||||
import {
|
import {
|
||||||
|
defaultUserLocalSettings,
|
||||||
parseUserLocalSettings,
|
parseUserLocalSettings,
|
||||||
USER_LOCAL_SETTINGS_COOKIE_NAME,
|
USER_LOCAL_SETTINGS_COOKIE_NAME,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@ -18,3 +19,33 @@ export async function updateBookmarksLayout(layout: BookmarksLayoutTypes) {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateTimezoneCookie(timezone: Timezones) {
|
||||||
|
const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME);
|
||||||
|
const parsed = parseUserLocalSettings(userSettings?.value);
|
||||||
|
(await cookies()).set({
|
||||||
|
name: USER_LOCAL_SETTINGS_COOKIE_NAME,
|
||||||
|
value: JSON.stringify({ ...parsed, timezone: timezone }),
|
||||||
|
maxAge: 34560000, // Chrome caps max age to 400 days
|
||||||
|
sameSite: "lax",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserLocalSettings() {
|
||||||
|
const parsed = parseUserLocalSettings(
|
||||||
|
(await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME)?.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const session = (await getServerAuthSession());
|
||||||
|
const dbTimezone = session?.user?.timezone as string | undefined;
|
||||||
|
|
||||||
|
// if (parsed?.timezone) {
|
||||||
|
// // console.log("Found timezone in cookie:", parsed.timezone);
|
||||||
|
// return parsed;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
bookmarkGridLayout: parsed?.bookmarkGridLayout ?? defaultUserLocalSettings().bookmarkGridLayout,
|
||||||
|
timezone: parsed!.timezone || dbTimezone || defaultUserLocalSettings().timezone,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from "@lifetracker/db/schema";
|
} from "@lifetracker/db/schema";
|
||||||
import serverConfig from "@lifetracker/shared/config";
|
import serverConfig from "@lifetracker/shared/config";
|
||||||
import { logAuthenticationError, validatePassword } from "@lifetracker/trpc/auth";
|
import { logAuthenticationError, validatePassword } from "@lifetracker/trpc/auth";
|
||||||
|
import { Timezones } from "@/lib/userLocalSettings/types";
|
||||||
|
|
||||||
type UserRole = "admin" | "user";
|
type UserRole = "admin" | "user";
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ declare module "next-auth" {
|
|||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
|
timezone: Timezones | undefined;
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +182,11 @@ export const authOptions: NextAuthOptions = {
|
|||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
session.user = { ...token.user };
|
const userTimezone = await db.query.users.findFirst({
|
||||||
|
columns: { timezone: true },
|
||||||
|
where: eq(users.id, token.user.id),
|
||||||
|
}).then((user) => user?.timezone);
|
||||||
|
session.user = { ...token.user, timezone: userTimezone };
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const zDaySchema = z.object({
|
|||||||
date: z.string(),
|
date: z.string(),
|
||||||
mood: z.number().nullable(),
|
mood: z.number().nullable(),
|
||||||
comment: z.string().nullable(),
|
comment: z.string().nullable(),
|
||||||
hours: z.array(zHourSchema),
|
hours: z.array(zHourSchema).optional(),
|
||||||
|
|
||||||
});
|
});
|
||||||
export type ZDay = z.infer<typeof zDaySchema>;
|
export type ZDay = z.infer<typeof zDaySchema>;
|
||||||
|
|||||||
@ -38,10 +38,12 @@ function generateHour(d, t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hoursListInUTC(input: { dateQuery: string, timezone: string }) {
|
export function hoursListInUTC(input: { dateQuery: string, timezone: string }) {
|
||||||
const midnight = spacetime(input.dateQuery, input.timezone).time('00:00');
|
const localMidnight = spacetime(input.dateQuery, input.timezone).startOf("day");
|
||||||
const hours = Array.from({ length: 24 }, function (_, i) {
|
return Array.from({ length: 24 }).map((_, i) => {
|
||||||
const utcMoment = midnight.add(i, 'hour').goto('UTC');
|
const utcTime = localMidnight.goto("UTC").add(i, "hours");
|
||||||
return { date: utcMoment.format('hh'), time: utcMoment.hour() };
|
return {
|
||||||
|
time: utcTime.hour(),
|
||||||
|
date: utcTime.format("iso-short"),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
return hours;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const timezones = {
|
|||||||
"America/Los_Angeles": "Pacific Time",
|
"America/Los_Angeles": "Pacific Time",
|
||||||
// "Atlantic/Azores": "Azores",
|
// "Atlantic/Azores": "Azores",
|
||||||
// "Atlantic/Cape_Verde": "Cape Verde Islands",
|
// "Atlantic/Cape_Verde": "Cape Verde Islands",
|
||||||
// GMT: "UTC",
|
"Etc/UTC": "UTC",
|
||||||
"Europe/London": "The UK",
|
"Europe/London": "The UK",
|
||||||
"Europe/Dublin": "Ireland",
|
"Europe/Dublin": "Ireland",
|
||||||
// "Europe/Lisbon": "Lisbon",
|
// "Europe/Lisbon": "Lisbon",
|
||||||
|
|||||||
@ -5,18 +5,19 @@ import { z } from "zod";
|
|||||||
import { db, SqliteError } from "@lifetracker/db";
|
import { db, SqliteError } from "@lifetracker/db";
|
||||||
import { categories, days, hours, users } from "@lifetracker/db/schema";
|
import { categories, days, hours, users } from "@lifetracker/db/schema";
|
||||||
import {
|
import {
|
||||||
zDaySchema, ZDay
|
zDaySchema, ZDay,
|
||||||
|
ZHour
|
||||||
} from "@lifetracker/shared/types/days";
|
} from "@lifetracker/shared/types/days";
|
||||||
import type { Context } from "../index";
|
import type { Context } from "../index";
|
||||||
import { authedProcedure, router } from "../index";
|
import { authedProcedure, router } from "../index";
|
||||||
import { dateFromInput, hoursListInUTC } from "@lifetracker/shared/utils/days";
|
import { dateFromInput, hoursListInUTC } from "@lifetracker/shared/utils/days";
|
||||||
import { closestIndexTo, format } from "date-fns";
|
import { closestIndexTo, format } from "date-fns";
|
||||||
import { TZDate } from "@date-fns/tz";
|
import { TZDate } from "@date-fns/tz";
|
||||||
import { hoursAppRouter, hourColors, hourJoinsQuery } from "./hours";
|
import { hoursAppRouter, hourColors, hourJoinsQuery, getHours, createHour } from "./hours";
|
||||||
import spacetime from "spacetime";
|
import spacetime from "spacetime";
|
||||||
import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours";
|
import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours";
|
||||||
|
|
||||||
async function createDay(date: string, ctx: Context) {
|
export async function createDay(date: string, ctx: Context) {
|
||||||
return await ctx.db.transaction(async (trx) => {
|
return await ctx.db.transaction(async (trx) => {
|
||||||
try {
|
try {
|
||||||
// Create the Day object
|
// Create the Day object
|
||||||
@ -33,7 +34,7 @@ async function createDay(date: string, ctx: Context) {
|
|||||||
comment: days.comment,
|
comment: days.comment,
|
||||||
});
|
});
|
||||||
|
|
||||||
return dayRes;
|
return dayRes[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SqliteError) {
|
if (e instanceof SqliteError) {
|
||||||
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
|
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
|
||||||
@ -51,24 +52,8 @@ async function createDay(date: string, ctx: Context) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createHour(day: ZDay, time: number, ctx: Context,) {
|
export async function getDay(ctx: Context, input: { dateQuery: string, timezone: string }) {
|
||||||
const newHour = (await ctx.db.insert(hours).values({
|
const dayDate = dateFromInput(input);
|
||||||
dayId: day.id,
|
|
||||||
time: time,
|
|
||||||
userId: ctx.user!.id,
|
|
||||||
}).returning());
|
|
||||||
return newHour[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTimezone(ctx: Context) {
|
|
||||||
const dbTimezone = await ctx.db.select({
|
|
||||||
timezone: users.timezone
|
|
||||||
}).from(users).where(eq(users.id, ctx.user!.id));
|
|
||||||
|
|
||||||
return dbTimezone[0].timezone as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDay(input: { dateQuery: string }, ctx: Context, date: string) {
|
|
||||||
const dayRes = await ctx.db.select({
|
const dayRes = await ctx.db.select({
|
||||||
id: days.id,
|
id: days.id,
|
||||||
date: days.date,
|
date: days.date,
|
||||||
@ -76,28 +61,13 @@ async function getDay(input: { dateQuery: string }, ctx: Context, date: string)
|
|||||||
comment: days.comment,
|
comment: days.comment,
|
||||||
})
|
})
|
||||||
.from(days)
|
.from(days)
|
||||||
.where(eq(days.date, date));
|
.where(eq(days.date, dayDate));
|
||||||
|
|
||||||
const day = dayRes.length == 0
|
const day = dayRes.length == 0
|
||||||
? (await createDay(date, ctx))[0]
|
? (await createDay(dayDate, ctx))
|
||||||
: (dayRes[0]);
|
: (dayRes[0]);
|
||||||
|
|
||||||
const dayHours = await Promise.all(
|
const dayHours: ZHour[] = await getHours(ctx, input);
|
||||||
Array.from({ length: 24 }).map(async function (_, i) {
|
|
||||||
const existing = await ctx.db.select({
|
|
||||||
dayId: hours.dayId,
|
|
||||||
time: hours.time,
|
|
||||||
userId: hours.userId,
|
|
||||||
date: days.date
|
|
||||||
}).from(hours)
|
|
||||||
.leftJoin(days, eq(days.id, hours.dayId)) // Ensure days table is joined first
|
|
||||||
.where(and(
|
|
||||||
eq(hours.userId, ctx.user!.id),
|
|
||||||
eq(hours.dayId, day.id),
|
|
||||||
eq(hours.time, i)
|
|
||||||
))
|
|
||||||
return existing.length == 0 ? createHour(day, i, ctx) : existing[0];
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hours: dayHours,
|
hours: dayHours,
|
||||||
@ -109,49 +79,12 @@ export const daysAppRouter = router({
|
|||||||
get: authedProcedure
|
get: authedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
dateQuery: z.string(),
|
dateQuery: z.string(),
|
||||||
timezone: z.string().optional(),
|
timezone: z.string(),
|
||||||
}))
|
}))
|
||||||
.output(zDaySchema)
|
.output(zDaySchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
// Get a Day
|
// Get a Day
|
||||||
|
return await getDay(ctx, input);
|
||||||
// Use timezone and date string to get the local date
|
|
||||||
const timezone = input.timezone ?? await getTimezone(ctx);
|
|
||||||
const date = dateFromInput({
|
|
||||||
dateQuery: input.dateQuery,
|
|
||||||
timezone: timezone
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the list of UTC hours corresponding to this day
|
|
||||||
const utcHours = hoursListInUTC({
|
|
||||||
timezone,
|
|
||||||
...input
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(`utcHours:\n,${utcHours.map(({ date, time }) => `${date} ${time}\n`)}`);
|
|
||||||
|
|
||||||
// Flatten the 24 hours to the 2 unique days
|
|
||||||
const uniqueDays = [...new Set(utcHours.map(({ date: date, time: _time }) => date))];
|
|
||||||
// ...and get their IDs
|
|
||||||
const uniqueDayIds = await Promise.all(uniqueDays.map(async function (date) {
|
|
||||||
const dayObj = await getDay(input, ctx, date);
|
|
||||||
return {
|
|
||||||
id: dayObj.id,
|
|
||||||
date: dayObj.date
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Finally, use the two unique day IDs and the 24 hours to get the actual Hour objects for each day
|
|
||||||
const dayHours = await Promise.all(utcHours.map(async function (map: { date: string, time: number }, i) {
|
|
||||||
const dayId = uniqueDayIds.find((dayIds: { id: string, date: string }) => map.date == dayIds.date)!.id;
|
|
||||||
|
|
||||||
return hourJoinsQuery(ctx, dayId, map.time);
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...await getDay(input, ctx, date),
|
|
||||||
hours: dayHours.flat(),
|
|
||||||
};
|
|
||||||
|
|
||||||
}),
|
}),
|
||||||
update: authedProcedure
|
update: authedProcedure
|
||||||
|
|||||||
@ -11,10 +11,21 @@ import type { Context } from "../index";
|
|||||||
import { authedProcedure, router } from "../index";
|
import { authedProcedure, router } from "../index";
|
||||||
import { addDays, format, parseISO } from "date-fns";
|
import { addDays, format, parseISO } from "date-fns";
|
||||||
import { TZDate } from "@date-fns/tz";
|
import { TZDate } from "@date-fns/tz";
|
||||||
import { dateFromInput } from "@lifetracker/shared/utils/days";
|
|
||||||
import { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
import { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||||
import { zCategorySchema } from "@lifetracker/shared/types/categories";
|
import { zCategorySchema } from "@lifetracker/shared/types/categories";
|
||||||
import spacetime from "spacetime";
|
import spacetime from "spacetime";
|
||||||
|
import { dateFromInput, hoursListInUTC } from "@lifetracker/shared/utils/days";
|
||||||
|
import { createDay, getDay } from "./days";
|
||||||
|
|
||||||
|
|
||||||
|
export async function createHour(day: ZDay | { id: string }, time: number, ctx: Context,) {
|
||||||
|
const newHour = (await ctx.db.insert(hours).values({
|
||||||
|
dayId: day.id!,
|
||||||
|
time: time,
|
||||||
|
userId: ctx.user!.id,
|
||||||
|
}).returning());
|
||||||
|
return newHour[0];
|
||||||
|
}
|
||||||
|
|
||||||
export async function hourColors(hour: ZHour, ctx: Context) {
|
export async function hourColors(hour: ZHour, ctx: Context) {
|
||||||
const categoryColor = await ctx.db.select()
|
const categoryColor = await ctx.db.select()
|
||||||
@ -41,10 +52,19 @@ export async function hourColors(hour: ZHour, ctx: Context) {
|
|||||||
|
|
||||||
export async function hourJoinsQuery(
|
export async function hourJoinsQuery(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
dayId: string,
|
|
||||||
time: number,
|
time: number,
|
||||||
|
dayId?: string,
|
||||||
|
day?: ZDay,
|
||||||
) {
|
) {
|
||||||
const hourMatch = await ctx.db.select({
|
if (!dayId && !day?.id) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "dayId string or Day object with an ID is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const id = dayId! ?? day!.id;
|
||||||
|
|
||||||
|
const res = await ctx.db.select({
|
||||||
id: hours.id,
|
id: hours.id,
|
||||||
dayId: hours.dayId,
|
dayId: hours.dayId,
|
||||||
time: hours.time,
|
time: hours.time,
|
||||||
@ -59,9 +79,12 @@ export async function hourJoinsQuery(
|
|||||||
.leftJoin(days, eq(days.id, hours.dayId))
|
.leftJoin(days, eq(days.id, hours.dayId))
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(hours.time, time),
|
eq(hours.time, time),
|
||||||
eq(hours.dayId, dayId)
|
eq(hours.dayId, id)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const hourMatch = res[0] ?? createHour({ id: id }, time, ctx);
|
||||||
|
|
||||||
|
|
||||||
const hourMeasurements = await ctx.db.select({
|
const hourMeasurements = await ctx.db.select({
|
||||||
id: measurements.id,
|
id: measurements.id,
|
||||||
metricId: measurements.metricId,
|
metricId: measurements.metricId,
|
||||||
@ -73,18 +96,48 @@ export async function hourJoinsQuery(
|
|||||||
})
|
})
|
||||||
.from(measurements)
|
.from(measurements)
|
||||||
.leftJoin(metrics, eq(metrics.id, measurements.metricId))
|
.leftJoin(metrics, eq(metrics.id, measurements.metricId))
|
||||||
.where(eq(measurements.hourId, hourMatch[0].id));
|
.where(eq(measurements.hourId, hourMatch.id));
|
||||||
|
|
||||||
const dayHour = {
|
const dayHour = {
|
||||||
measurements: hourMeasurements,
|
measurements: hourMeasurements,
|
||||||
...hourMatch[0],
|
...hourMatch,
|
||||||
...(await hourColors(hourMatch[0], ctx)),
|
...(await hourColors(hourMatch, ctx)),
|
||||||
};
|
};
|
||||||
|
|
||||||
return dayHour;
|
return dayHour;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function getHours(ctx: Context, input: { dateQuery: string | [Date, Date], timezone: string }) {
|
||||||
|
const dateRange = Array.isArray(input.dateQuery) ? listOfDates(input.dateQuery) : [input.dateQuery];
|
||||||
|
const utcHours = dateRange.map(date => { return hoursListInUTC({ dateQuery: date, timezone: input.timezone }) });
|
||||||
|
// Dedup utcHours
|
||||||
|
const uniqueHours = [...new Set(utcHours.flat().map((x) => x))];
|
||||||
|
|
||||||
|
// Flatten the list of hours to the unique days
|
||||||
|
const uniqueDays = [...new Set(uniqueHours.map(({ date: date, time: _time }) => date))];
|
||||||
|
// ...and get their IDs
|
||||||
|
const uniqueDayIds = (await Promise.all(uniqueDays.map(async function (date) {
|
||||||
|
const dayObj = await ctx.db.select({
|
||||||
|
id: days.id,
|
||||||
|
date: days.date,
|
||||||
|
})
|
||||||
|
.from(days)
|
||||||
|
.where(eq(days.date, date));
|
||||||
|
return dayObj[0] ?? createDay(date, ctx);
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Finally, use the two unique day IDs and the 24 hours to get the actual Hour objects for each day
|
||||||
|
const dayHours = await Promise.all(uniqueHours.map(async function (map: { date: string, time: number }, i) {
|
||||||
|
const dayId = uniqueDayIds.find((dayIds: { id: string, date: string }) => map.date == dayIds.date)!.id;
|
||||||
|
|
||||||
|
return hourJoinsQuery(ctx, map.time, dayId);
|
||||||
|
}));
|
||||||
|
return dayHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function listOfDates(dateRange: [Date, Date]) {
|
function listOfDates(dateRange: [Date, Date]) {
|
||||||
const [start, end] = dateRange.map((date) => dateFromInput({
|
const [start, end] = dateRange.map((date) => dateFromInput({
|
||||||
dateQuery: spacetime(date, "UTC").goto("UTC").format("iso-short"),
|
dateQuery: spacetime(date, "UTC").goto("UTC").format("iso-short"),
|
||||||
@ -96,7 +149,7 @@ function listOfDates(dateRange: [Date, Date]) {
|
|||||||
dates.push(format(currentDate, "yyyy-MM-dd"));
|
dates.push(format(currentDate, "yyyy-MM-dd"));
|
||||||
currentDate = addDays(currentDate, 1);
|
currentDate = addDays(currentDate, 1);
|
||||||
}
|
}
|
||||||
return dates.length === 0 ? parseISO(start) : dates;
|
return dates.length === 0 ? [format(parseISO(start), "yyyy-MM-dd")] : dates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -185,53 +238,33 @@ export const hoursAppRouter = router({
|
|||||||
// ...hourRes[0]
|
// ...hourRes[0]
|
||||||
// };
|
// };
|
||||||
|
|
||||||
return hourJoinsQuery(ctx, input.dayId, input.hourTime);
|
return hourJoinsQuery(ctx, input.hourTime, input.dayId,);
|
||||||
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
categoryFrequencies: authedProcedure
|
categoryFrequencies: authedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
dateRange: z.tuple([z.date(), z.date()])
|
dateRange: z.tuple([z.date(), z.date()]),
|
||||||
|
timezone: z.string()
|
||||||
}))
|
}))
|
||||||
.output(z.array(z.object(
|
.output(z.array(z.object(
|
||||||
{
|
{
|
||||||
count: z.number(),
|
count: z.number(),
|
||||||
date: z.string(),
|
percentage: z.number(),
|
||||||
time: z.number(),
|
...zHourSchema.shape
|
||||||
categoryName: z.string(),
|
|
||||||
categoryCode: z.number(),
|
|
||||||
categoryDescription: z.string(),
|
|
||||||
categoryColor: z.string(),
|
|
||||||
categoryForeground: z.string(),
|
|
||||||
percentage: z.number()
|
|
||||||
}
|
}
|
||||||
)))
|
)))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const hoursList = (await ctx.db.select({
|
const hoursList = await getHours(ctx, {
|
||||||
date: days.date,
|
dateQuery: input.dateRange,
|
||||||
time: hours.time,
|
timezone: input.timezone
|
||||||
categoryName: categories.name,
|
});
|
||||||
categoryCode: categories.code,
|
|
||||||
categoryDescription: categories.description,
|
|
||||||
categoryColor: colors.hexcode,
|
|
||||||
categoryForeground: colors.inverse,
|
|
||||||
})
|
|
||||||
.from(hours)
|
|
||||||
.leftJoin(days, eq(days.id, hours.dayId))
|
|
||||||
.leftJoin(categories, eq(categories.id, hours.categoryId))
|
|
||||||
.leftJoin(colors, eq(colors.id, categories.colorId))
|
|
||||||
.where(and(
|
|
||||||
eq(hours.userId, ctx.user!.id),
|
|
||||||
inArray(days.date, listOfDates(input.dateRange))
|
|
||||||
))).filter(h => h.categoryCode != null);
|
|
||||||
|
|
||||||
// Count total hours in the filtered range
|
// Count total hours in the filtered range
|
||||||
const totalHours = hoursList.length;
|
const totalHours = hoursList.length;
|
||||||
|
|
||||||
console.log(hoursList);
|
|
||||||
|
|
||||||
// Group hours by category and count occurrences
|
// Group hours by category and count occurrences
|
||||||
const categoriesList = {};
|
const categoriesList: { [key: string]: any } = {};
|
||||||
hoursList.forEach(h => {
|
hoursList.forEach(h => {
|
||||||
if (!categoriesList[h.categoryCode]) {
|
if (!categoriesList[h.categoryCode]) {
|
||||||
categoriesList[h.categoryCode] = {
|
categoriesList[h.categoryCode] = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user