import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; import { and, desc, eq, inArray, notExists } from "drizzle-orm"; import { z } from "zod"; import { db, SqliteError } from "@lifetracker/db"; import { categories, days, hours, users } from "@lifetracker/db/schema"; import { zDaySchema, ZDay } 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 } from "./hours"; import spacetime from "spacetime"; import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours"; import { hourColors } from "./hours"; async function createDay(date: string, ctx: Context) { return await ctx.db.transaction(async (trx) => { try { // Create the Day object const dayRes = await trx .insert(days) .values({ userId: ctx.user!.id, date: date, }) .returning({ id: days.id, date: days.date, mood: days.mood, comment: days.comment, }); return dayRes; } catch (e) { if (e instanceof SqliteError) { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { throw new TRPCError({ code: "BAD_REQUEST", message: "This day already exists", }); } } throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Something went wrong", }); } }); } async function createHour(day, time, ctx,) { const newHour = (await ctx.db.insert(hours).values({ dayId: day.id, time: time, userId: ctx.user!.id, }).returning()); console.log(newHour); 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({ id: days.id, date: days.date, mood: days.mood, comment: days.comment, }) .from(days) .where(eq(days.date, date)); const day = dayRes.length == 0 ? (await createDay(date, ctx))[0] : (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]; })); return { hours: dayHours, ...day } } export const daysAppRouter = router({ get: authedProcedure .input(z.object({ dateQuery: z.string(), timezone: z.string().optional(), })) .output(zDaySchema) .query(async ({ input, ctx }) => { const timezone = input.timezone ?? await getTimezone(ctx); const date = dateFromInput({ dateQuery: input.dateQuery, timezone: timezone }); const allHours = hoursListInUTC({ timezone, ...input }); const dayRange = [...new Set(allHours.map(({ date: date, time: _time }) => date))]; const allDayIds = await Promise.all(dayRange.map(async function (date) { const dayObj = await getDay(input, ctx, date); return { id: dayObj.id, date: dayObj.date } })); const dayHours = await Promise.all(allHours.map(async function (map: { date, time }, i) { const dayId = allDayIds.find((dayIds: { id, date }) => map.date == dayIds.date)!.id; const hourMatch = await ctx.db.select({ id: hours.id, dayId: hours.dayId, time: hours.time, categoryId: hours.categoryId, categoryCode: categories.code, categoryName: categories.name, categoryDesc: categories.description, comment: hours.comment, date: days.date }).from(hours) .leftJoin(categories, eq(categories.id, hours.categoryId)) .leftJoin(days, eq(days.id, hours.dayId)) .where(and( eq(hours.time, map.time), eq(hours.dayId, dayId))); // console.log({ // ...allHours, // dayId: dayId // }); // console.log("Search values:: ", `allDayIds: ${allDayIds}, d: ${date}, t: ${time}, dayId: ${dayId}`) // console.log("hourMatch", hourMatch[0]); // console.log(hourMatch[0].categoryDesc); const dayHour = { ...hourMatch[0], ...(await hourColors(hourMatch[0], ctx)), }; const localDateTime = spacetime(date, timezone).add(i, "hour"); return { ...dayHour, datetime: `${localDateTime.format('{hour} {ampm}')}`, // datetime: `${localDateTime.format('{nice}')} ${timezone} (${localDateTime.goto("UTC").format('{nice}')} UTC)`, }; })); // console.log(dayHours.flat()); return { ...await getDay(input, ctx, date), hours: dayHours.flat(), }; }), update: authedProcedure .input( z.object({ mood: z.string().optional().or(z.number()), comment: z.string().optional(), dateQuery: z.string(), timezone: z.string().optional(), }), ) .mutation(async ({ input, ctx }) => { const { dateQuery, timezone, ...updatedProps } = input; // Convert mood to number, if it exists if (updatedProps.mood) { updatedProps.mood = parseInt(updatedProps.mood); } await ctx.db .update(days) .set(updatedProps) .where(eq(days.date, dateFromInput({ dateQuery: dateQuery, timezone: timezone ?? ctx.user.timezone }))); }), });