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 { getServerAuthSession } from "@/server/auth";
|
||||
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 }; }) {
|
||||
const session = await getServerAuthSession();
|
||||
@ -11,15 +13,12 @@ export default async function DayPage({ params }: { params: { dateQuery: string
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
|
||||
let day;
|
||||
const { dateQuery } = await params;
|
||||
const timezone = await api.users.getTimezone();
|
||||
|
||||
try {
|
||||
day = await api.days.get({
|
||||
dateQuery: dateQuery,
|
||||
timezone: timezone,
|
||||
dateQuery: (await params).dateQuery,
|
||||
timezone: (await getUserLocalSettings()).timezone,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof TRPCError) {
|
||||
@ -31,7 +30,7 @@ export default async function DayPage({ params }: { params: { dateQuery: string
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="container">
|
||||
<div className="container">
|
||||
{
|
||||
day == undefined ?
|
||||
<LoadingSpinner /> :
|
||||
|
||||
@ -20,6 +20,8 @@ import { getServerAuthSession } from "@/server/auth";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
import { clientConfig } from "@lifetracker/shared/config";
|
||||
import { useTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
||||
import { getUserLocalSettings } from "@/lib/userLocalSettings/userLocalSettings";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@ -61,9 +63,7 @@ export default async function RootLayout({
|
||||
session={session}
|
||||
clientConfig={clientConfig}
|
||||
userLocalSettings={
|
||||
parseUserLocalSettings(
|
||||
(await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME)?.value
|
||||
) ?? defaultUserLocalSettings()
|
||||
await getUserLocalSettings()
|
||||
}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -4,27 +4,27 @@ import Link from "next/link";
|
||||
import { format } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { timezones } from '@lifetracker/shared/utils/timezones';
|
||||
import { useTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
||||
import LoadingSpinner from "@/components/ui/spinner";
|
||||
import { db } from "@lifetracker/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import spacetime from "spacetime";
|
||||
import { useTimezone } from "@/lib/userLocalSettings/client";
|
||||
|
||||
|
||||
|
||||
export default function TimezoneDisplay() {
|
||||
const clientTime = spacetime.now();
|
||||
const timezone = useTimezone();
|
||||
return (
|
||||
<div className="text-center w-full">
|
||||
<Link href="/settings/app">
|
||||
<b className="whitespace-nowrap">
|
||||
{clientTime.format('nice')}
|
||||
{format(TZDate.tz(timezone), 'MMM dd, hh:mm aa')}
|
||||
</b>
|
||||
<br />
|
||||
<span>in </span>
|
||||
<span className="whitespace-nowrap">
|
||||
<b>
|
||||
{timezones[clientTime.timezone().name] || clientTime.timezone().name}
|
||||
{timezones[timezone] || "Unknown Timezone"}
|
||||
</b>
|
||||
</span>
|
||||
</Link>
|
||||
@ -8,7 +8,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useBookmarkLayout } from "@/lib/userLocalSettings/bookmarksLayout";
|
||||
import { useBookmarkLayout } from "@/lib/userLocalSettings/client";
|
||||
import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
|
||||
import {
|
||||
Check,
|
||||
|
||||
@ -12,6 +12,8 @@ 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;
|
||||
@ -46,8 +48,8 @@ export default function AnalyticsView() {
|
||||
}
|
||||
return [
|
||||
// spacetime.now().subtract(1, "week").toNativeDate(),
|
||||
spacetime.now().toNativeDate(),
|
||||
spacetime.now().toNativeDate()
|
||||
spacetime.now(useTimezone()).toNativeDate(),
|
||||
spacetime.now(useTimezone()).toNativeDate()
|
||||
];
|
||||
})();
|
||||
|
||||
@ -70,14 +72,18 @@ export default function AnalyticsView() {
|
||||
|
||||
const categoryFrequencies = api.hours.categoryFrequencies.useQuery({
|
||||
dateRange,
|
||||
timezone: useTimezone(),
|
||||
}).data ?? [];
|
||||
|
||||
console.log(categoryFrequencies);
|
||||
|
||||
return (
|
||||
<div className="px-4 flex flex-col gap-y-4">
|
||||
<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
|
||||
showOneCalendar
|
||||
appearance="subtle"
|
||||
@ -139,6 +145,17 @@ export default function AnalyticsView() {
|
||||
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)
|
||||
@ -150,7 +167,7 @@ export default function AnalyticsView() {
|
||||
}}
|
||||
>
|
||||
<div className="whitespace-nowrap">
|
||||
{category.categoryName}
|
||||
{category.categoryName ?? "[Unallocated]"}
|
||||
</div>
|
||||
<div>
|
||||
{category.count} hours
|
||||
|
||||
@ -33,8 +33,6 @@ export default function PieChart({
|
||||
const centerY = innerHeight / 2;
|
||||
const centerX = innerWidth / 2;
|
||||
|
||||
// console.log(data);
|
||||
|
||||
return data.length == 0 ? <LoadingSpinner /> : (
|
||||
<svg width={width} height={height}>
|
||||
<rect rx={14} width={width} height={height} fill="transparent" />
|
||||
@ -55,7 +53,8 @@ export default function PieChart({
|
||||
console.log('clicked: ', category.categoryName)
|
||||
}
|
||||
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>) {
|
||||
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 HoarderLogo from "@/components/HoarderIcon";
|
||||
import { getServerAuthSession } from "@/server/auth";
|
||||
import TimezoneDisplay from "../../TimezoneDisplay";
|
||||
|
||||
export default async function Header() {
|
||||
const session = await getServerAuthSession();
|
||||
@ -19,7 +20,7 @@ export default async function Header() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 gap-2">
|
||||
<GlobalActions />
|
||||
<TimezoneDisplay />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<ProfileOptions />
|
||||
|
||||
@ -23,6 +23,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { titleCase } from "title-case";
|
||||
import { useDecrementCount } from "@lifetracker/shared-react/hooks/measurements";
|
||||
import { useTimezone } from "@/lib/userLocalSettings/client";
|
||||
|
||||
|
||||
export default function EditableHour({
|
||||
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 => {
|
||||
|
||||
@ -7,7 +7,7 @@ import { Archive, ArrowRightFromLine, Calendar, CheckCheck, Gauge, GaugeCircleIc
|
||||
import serverConfig from "@lifetracker/shared/config";
|
||||
|
||||
import AllLists from "./AllLists";
|
||||
import TimezoneDisplay from "./TimezoneDisplay";
|
||||
import TimezoneDisplay from "../../TimezoneDisplay";
|
||||
import { ActionButtonWithTooltip } from "@/components/ui/action-button";
|
||||
|
||||
export default async function Sidebar() {
|
||||
@ -76,8 +76,8 @@ export default async function Sidebar() {
|
||||
</ul>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="mt-auto flex items-center border-t pt-2 text-sm text-gray-400">
|
||||
<TimezoneDisplay />
|
||||
<div className="mt-auto flex border-t pt-2 text-sm text-gray-400">
|
||||
<span className="text-center w-full">Server version {serverConfig.serverVersion}</span>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
@ -108,6 +108,6 @@ export default async function Sidebar() {
|
||||
|
||||
|
||||
return (
|
||||
<CollapsedView />
|
||||
<FullView />
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export default async function AppSettings() {
|
||||
<div className="mb-2" >
|
||||
<div className="mb-2 text-sm font-medium">Timezone</div>
|
||||
<div className="select-wrapper">
|
||||
|
||||
<ChangeTimezone
|
||||
userTimezone={userTimezone}
|
||||
/>
|
||||
|
||||
@ -4,11 +4,13 @@ import React, { useState, useEffect } from "react";
|
||||
import { timezones } from "@lifetracker/shared/utils/timezones";
|
||||
import TimezoneSelect, { type ITimezone } from 'react-timezone-select'
|
||||
import { useUpdateUserTimezone } from "@lifetracker/shared-react/hooks/timezones";
|
||||
import { updateTimezoneCookie } from "@/lib/userLocalSettings/userLocalSettings";
|
||||
|
||||
export default function ChangeTimezone({ userTimezone }) {
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<ITimezone>(userTimezone);
|
||||
const { mutate: updateUserTimezone, isPending } = useUpdateUserTimezone({
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, { newTimezone }) => {
|
||||
updateTimezoneCookie(newTimezone);
|
||||
toast({
|
||||
description: "User DB Timezone updated!",
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@ import { getServerAuthSession } from "@/server/auth";
|
||||
import serverConfig from "@lifetracker/shared/config";
|
||||
|
||||
import { settingsSidebarItems } from "./items";
|
||||
import TimezoneDisplay from "@/components/dashboard/sidebar/TimezoneDisplay";
|
||||
import TimezoneDisplay from "@/components/TimezoneDisplay";
|
||||
|
||||
export default async function Sidebar() {
|
||||
const session = await getServerAuthSession();
|
||||
|
||||
@ -8,7 +8,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
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 { httpBatchLink, loggerLink } from "@trpc/client";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
@ -3,20 +3,27 @@
|
||||
import type { z } from "zod";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { BookmarksLayoutTypes, zUserLocalSettings } from "./types";
|
||||
import type { BookmarksLayoutTypes, Timezones, zUserLocalSettings } from "./types";
|
||||
|
||||
const defaultLayout: BookmarksLayoutTypes = "masonry";
|
||||
const defaultTimezone: Timezones = "Etc/UTC";
|
||||
|
||||
export const UserLocalSettingsCtx = createContext<
|
||||
z.infer<typeof zUserLocalSettings>
|
||||
>({
|
||||
bookmarkGridLayout: defaultLayout,
|
||||
timezone: defaultTimezone,
|
||||
});
|
||||
|
||||
function useUserLocalSettings() {
|
||||
return useContext(UserLocalSettingsCtx);
|
||||
}
|
||||
|
||||
export function useTimezone() {
|
||||
const settings = useUserLocalSettings();
|
||||
return settings.timezone;
|
||||
}
|
||||
|
||||
export function useBookmarkLayout() {
|
||||
const settings = useUserLocalSettings();
|
||||
return settings.bookmarkGridLayout;
|
||||
@ -1,12 +1,19 @@
|
||||
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"]);
|
||||
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({
|
||||
bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
|
||||
timezone: zTimezone.optional().default(""),
|
||||
});
|
||||
|
||||
export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import type { BookmarksLayoutTypes } from "./types";
|
||||
import { getServerAuthSession } from "@/server/auth";
|
||||
import type { BookmarksLayoutTypes, Timezones } from "./types";
|
||||
import {
|
||||
defaultUserLocalSettings,
|
||||
parseUserLocalSettings,
|
||||
USER_LOCAL_SETTINGS_COOKIE_NAME,
|
||||
} from "./types";
|
||||
@ -18,3 +19,33 @@ export async function updateBookmarksLayout(layout: BookmarksLayoutTypes) {
|
||||
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";
|
||||
import serverConfig from "@lifetracker/shared/config";
|
||||
import { logAuthenticationError, validatePassword } from "@lifetracker/trpc/auth";
|
||||
import { Timezones } from "@/lib/userLocalSettings/types";
|
||||
|
||||
type UserRole = "admin" | "user";
|
||||
|
||||
@ -39,6 +40,7 @@ declare module "next-auth" {
|
||||
user: {
|
||||
id: string;
|
||||
role: UserRole;
|
||||
timezone: Timezones | undefined;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
@ -180,7 +182,11 @@ export const authOptions: NextAuthOptions = {
|
||||
return 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;
|
||||
},
|
||||
},
|
||||
|
||||
@ -24,7 +24,7 @@ export const zDaySchema = z.object({
|
||||
date: z.string(),
|
||||
mood: z.number().nullable(),
|
||||
comment: z.string().nullable(),
|
||||
hours: z.array(zHourSchema),
|
||||
hours: z.array(zHourSchema).optional(),
|
||||
|
||||
});
|
||||
export type ZDay = z.infer<typeof zDaySchema>;
|
||||
|
||||
@ -38,10 +38,12 @@ function generateHour(d, t) {
|
||||
}
|
||||
|
||||
export function hoursListInUTC(input: { dateQuery: string, timezone: string }) {
|
||||
const midnight = spacetime(input.dateQuery, input.timezone).time('00:00');
|
||||
const hours = Array.from({ length: 24 }, function (_, i) {
|
||||
const utcMoment = midnight.add(i, 'hour').goto('UTC');
|
||||
return { date: utcMoment.format('hh'), time: utcMoment.hour() };
|
||||
const localMidnight = spacetime(input.dateQuery, input.timezone).startOf("day");
|
||||
return Array.from({ length: 24 }).map((_, i) => {
|
||||
const utcTime = localMidnight.goto("UTC").add(i, "hours");
|
||||
return {
|
||||
time: utcTime.hour(),
|
||||
date: utcTime.format("iso-short"),
|
||||
};
|
||||
});
|
||||
return hours;
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export const timezones = {
|
||||
"America/Los_Angeles": "Pacific Time",
|
||||
// "Atlantic/Azores": "Azores",
|
||||
// "Atlantic/Cape_Verde": "Cape Verde Islands",
|
||||
// GMT: "UTC",
|
||||
"Etc/UTC": "UTC",
|
||||
"Europe/London": "The UK",
|
||||
"Europe/Dublin": "Ireland",
|
||||
// "Europe/Lisbon": "Lisbon",
|
||||
|
||||
@ -5,18 +5,19 @@ import { z } from "zod";
|
||||
import { db, SqliteError } from "@lifetracker/db";
|
||||
import { categories, days, hours, users } from "@lifetracker/db/schema";
|
||||
import {
|
||||
zDaySchema, ZDay
|
||||
zDaySchema, ZDay,
|
||||
ZHour
|
||||
} from "@lifetracker/shared/types/days";
|
||||
import type { Context } from "../index";
|
||||
import { authedProcedure, router } from "../index";
|
||||
import { dateFromInput, hoursListInUTC } from "@lifetracker/shared/utils/days";
|
||||
import { closestIndexTo, format } from "date-fns";
|
||||
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 { 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) => {
|
||||
try {
|
||||
// Create the Day object
|
||||
@ -33,7 +34,7 @@ async function createDay(date: string, ctx: Context) {
|
||||
comment: days.comment,
|
||||
});
|
||||
|
||||
return dayRes;
|
||||
return dayRes[0];
|
||||
} catch (e) {
|
||||
if (e instanceof SqliteError) {
|
||||
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,) {
|
||||
const newHour = (await ctx.db.insert(hours).values({
|
||||
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) {
|
||||
export async function getDay(ctx: Context, input: { dateQuery: string, timezone: string }) {
|
||||
const dayDate = dateFromInput(input);
|
||||
const dayRes = await ctx.db.select({
|
||||
id: days.id,
|
||||
date: days.date,
|
||||
@ -76,28 +61,13 @@ async function getDay(input: { dateQuery: string }, ctx: Context, date: string)
|
||||
comment: days.comment,
|
||||
})
|
||||
.from(days)
|
||||
.where(eq(days.date, date));
|
||||
.where(eq(days.date, dayDate));
|
||||
|
||||
const day = dayRes.length == 0
|
||||
? (await createDay(date, ctx))[0]
|
||||
? (await createDay(dayDate, ctx))
|
||||
: (dayRes[0]);
|
||||
|
||||
const dayHours = await Promise.all(
|
||||
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];
|
||||
}));
|
||||
const dayHours: ZHour[] = await getHours(ctx, input);
|
||||
|
||||
return {
|
||||
hours: dayHours,
|
||||
@ -109,49 +79,12 @@ export const daysAppRouter = router({
|
||||
get: authedProcedure
|
||||
.input(z.object({
|
||||
dateQuery: z.string(),
|
||||
timezone: z.string().optional(),
|
||||
timezone: z.string(),
|
||||
}))
|
||||
.output(zDaySchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// Get a Day
|
||||
|
||||
// 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(),
|
||||
};
|
||||
return await getDay(ctx, input);
|
||||
|
||||
}),
|
||||
update: authedProcedure
|
||||
|
||||
@ -11,10 +11,21 @@ import type { Context } from "../index";
|
||||
import { authedProcedure, router } from "../index";
|
||||
import { addDays, format, parseISO } from "date-fns";
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { dateFromInput } from "@lifetracker/shared/utils/days";
|
||||
import { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||
import { zCategorySchema } from "@lifetracker/shared/types/categories";
|
||||
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) {
|
||||
const categoryColor = await ctx.db.select()
|
||||
@ -41,10 +52,19 @@ export async function hourColors(hour: ZHour, ctx: Context) {
|
||||
|
||||
export async function hourJoinsQuery(
|
||||
ctx: Context,
|
||||
dayId: string,
|
||||
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,
|
||||
dayId: hours.dayId,
|
||||
time: hours.time,
|
||||
@ -59,9 +79,12 @@ export async function hourJoinsQuery(
|
||||
.leftJoin(days, eq(days.id, hours.dayId))
|
||||
.where(and(
|
||||
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({
|
||||
id: measurements.id,
|
||||
metricId: measurements.metricId,
|
||||
@ -73,18 +96,48 @@ export async function hourJoinsQuery(
|
||||
})
|
||||
.from(measurements)
|
||||
.leftJoin(metrics, eq(metrics.id, measurements.metricId))
|
||||
.where(eq(measurements.hourId, hourMatch[0].id));
|
||||
.where(eq(measurements.hourId, hourMatch.id));
|
||||
|
||||
const dayHour = {
|
||||
measurements: hourMeasurements,
|
||||
...hourMatch[0],
|
||||
...(await hourColors(hourMatch[0], ctx)),
|
||||
...hourMatch,
|
||||
...(await hourColors(hourMatch, ctx)),
|
||||
};
|
||||
|
||||
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]) {
|
||||
const [start, end] = dateRange.map((date) => dateFromInput({
|
||||
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"));
|
||||
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]
|
||||
// };
|
||||
|
||||
return hourJoinsQuery(ctx, input.dayId, input.hourTime);
|
||||
return hourJoinsQuery(ctx, input.hourTime, input.dayId,);
|
||||
|
||||
}),
|
||||
|
||||
categoryFrequencies: authedProcedure
|
||||
.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(
|
||||
{
|
||||
count: z.number(),
|
||||
date: z.string(),
|
||||
time: z.number(),
|
||||
categoryName: z.string(),
|
||||
categoryCode: z.number(),
|
||||
categoryDescription: z.string(),
|
||||
categoryColor: z.string(),
|
||||
categoryForeground: z.string(),
|
||||
percentage: z.number()
|
||||
percentage: z.number(),
|
||||
...zHourSchema.shape
|
||||
}
|
||||
)))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const hoursList = (await ctx.db.select({
|
||||
date: days.date,
|
||||
time: hours.time,
|
||||
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);
|
||||
const hoursList = await getHours(ctx, {
|
||||
dateQuery: input.dateRange,
|
||||
timezone: input.timezone
|
||||
});
|
||||
|
||||
// Count total hours in the filtered range
|
||||
const totalHours = hoursList.length;
|
||||
|
||||
console.log(hoursList);
|
||||
|
||||
// Group hours by category and count occurrences
|
||||
const categoriesList = {};
|
||||
const categoriesList: { [key: string]: any } = {};
|
||||
hoursList.forEach(h => {
|
||||
if (!categoriesList[h.categoryCode]) {
|
||||
categoriesList[h.categoryCode] = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user