import { TRPCError } from "@trpc/server"; import { and, desc, eq, inArray, notExists } from "drizzle-orm"; import { z, ZodNull } from "zod"; import { SqliteError } from "@lifetracker/db"; import { colors, metrics, measurements, hours } from "@lifetracker/db/schema"; import { zMetricSchema, zMeasurementSchema } from "@lifetracker/shared/types/metrics"; import type { Context } from "../index"; import { authedProcedure, router } from "../index"; import { zColorSchema } from "@lifetracker/shared/types/colors"; import { titleCase } from "title-case"; const getMetricFromInput = async (ctx: Context, input: { metricId?: string | null | undefined; metricName?: string | null | undefined; }) => { const metric = input.metricName ? await ctx.db.select().from(metrics).where(eq(metrics.name, titleCase(input.metricName))) : await ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId!)); if (!metric[0]) { throw new TRPCError({ code: "NOT_FOUND", message: "Metric not found", }); } return metric[0]; } export const measurementsAppRouter = router({ list: authedProcedure .input(z.object({ hourId: z.string() })) .output(z.array(zMeasurementSchema)) .query(async ({ input, ctx }) => { const dbMeasurements = await ctx.db .select() .from(measurements) .leftJoin(hours, eq(measurements.hourId, hours.id)) .where(and(eq(measurements.userId, ctx.user.id), eq(hours.id, input.hourId)) ); console.log(dbMeasurements.length); return dbMeasurements; }), setValue: authedProcedure .input(z.object({ metricId: z.string().nullish(), metricName: z.string().nullish(), hourId: z.string(), dayId: z.string(), value: z.number() })) .output(zMeasurementSchema) .mutation(async ({ input, ctx }) => { if (!input.metricId && !input.metricName) { throw new TRPCError({ code: "BAD_REQUEST", message: "Metric name or id is required", }); } const metric = await getMetricFromInput(ctx, input); // Check if there is a measurement for this metric in this hour, if so, update it, if not, create it const existingMeasurement = await ctx.db.select().from(measurements).where(and( eq(measurements.metricId, metric.id), eq(measurements.hourId, input.hourId), )); if (existingMeasurement[0]) { const updatedMeasurement = await ctx.db.update(measurements).set({ value: input.value.toString(), }).where(eq(measurements.id, existingMeasurement[0].id)).returning(); return { ...updatedMeasurement[0], icon: metric.icon, metricName: metric.name, }; } else { const newMeasurement = await ctx.db.insert(measurements).values({ metricId: metric.id, hourId: input.hourId, dayId: input.dayId, value: input.value.toString(), userId: ctx.user.id, }).returning(); return { ...newMeasurement[0], icon: metric.icon, metricName: metric.name, }; } }), incrementCount: authedProcedure .input(z.object({ metricId: z.string().nullish(), metricName: z.string().nullish(), hourId: z.string(), dayId: z.string() })) .output(zMeasurementSchema) .mutation(async ({ input, ctx }) => { const metric = await getMetricFromInput(ctx, input); if (!metric[0]) { throw new TRPCError({ code: "NOT_FOUND", message: "Metric not found", }); } // Check if there is a measurement for this metric in this hour, if so, increment it, if not, create it with value 1 const existingMeasurement = await ctx.db.select().from(measurements).where(and( eq(measurements.metricId, input.metricId), eq(measurements.hourId, input.hourId), )); if (existingMeasurement[0]) { const updatedMeasurement = await ctx.db.update(measurements).set({ value: (parseInt(existingMeasurement[0].value) + 1).toString(), }).where(eq(measurements.id, existingMeasurement[0].id)).returning(); return { ...updatedMeasurement[0], icon: metric[0].icon, metricName: metric[0].name, }; } else { const newMeasurement = await ctx.db.insert(measurements).values({ metricId: input.metricId, hourId: input.hourId, dayId: input.dayId, value: 1, userId: ctx.user.id, }).returning(); return { ...newMeasurement[0], icon: metric[0].icon, metricName: metric[0].name, }; } }), decrementCount: authedProcedure .input(z.object({ metricId: z.string(), hourId: z.string() })) .output(zMeasurementSchema.optional()) .mutation(async ({ input, ctx }) => { const metric = await ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId)); if (!metric[0]) { throw new TRPCError({ code: "NOT_FOUND", message: "Metric not found", }); } // Check if there is a measurement for this metric in this hour, if so, decrement it, if not, throw an error const existingMeasurement = await ctx.db.select().from(measurements).where(and( eq(measurements.metricId, input.metricId), eq(measurements.hourId, input.hourId), )); if (existingMeasurement[0]) { if (parseInt(existingMeasurement[0].value) > 1) { const updatedMeasurement = await ctx.db.update(measurements).set({ value: (parseInt(existingMeasurement[0].value) - 1).toString(), }).where(eq(measurements.id, existingMeasurement[0].id)).returning(); return { ...updatedMeasurement[0], icon: metric[0].icon, metricName: metric[0].name, }; } else { // Delete the measurement if it's zerooo await ctx.db.delete(measurements).where(eq(measurements.id, existingMeasurement[0].id)); } } else { throw new TRPCError({ code: "NOT_FOUND", message: "Measurement not found", }); } }), timeSinceLastMeasurement: authedProcedure .input(z.object({ metricId: z.string() })) .output(z.number()) .query(async ({ input, ctx }) => { const lastMeasurement = await ctx.db.select().from(measurements).where(and( eq(measurements.metricId, input.metricId), eq(measurements.userId, ctx.user.id), )).orderBy(desc(measurements.createdAt)).limit(1); if (lastMeasurement[0]) { const lastMeasurementTime = new Date(lastMeasurement[0].createdAt).getTime(); const currentTime = new Date().getTime(); return (currentTime - lastMeasurementTime) / 1000; } else { return 0; } }), });