diff --git a/apps/web/app/api/v1/measurements/route.ts b/apps/web/app/api/v1/measurements/route.ts new file mode 100644 index 0000000..f6d5825 --- /dev/null +++ b/apps/web/app/api/v1/measurements/route.ts @@ -0,0 +1,43 @@ +import { NextRequest } from "next/server"; +import { zMeasurementInputSchema } from "@lifetracker/shared/types/metrics"; + +import { buildHandler } from "../utils/handler"; + +import spacetime from "spacetime"; + +export const POST = (req: NextRequest) => + buildHandler({ + req, + bodySchema: zMeasurementInputSchema, + handler: async ({ api, body }) => { + + const datetime = spacetime( + body?.dateTimeQuery || new Date(), + body?.timezone || "Etc/UTC" + ).goto("Etc/UTC"); + + // const dayId = (await api.days.get({ + // dateQuery: datetime.format("iso-short") + // })).id; + + const hour = await api.hours.get({ + dateQuery: datetime.format("iso-short"), + time: datetime.hour(), + }); + + const obj = { + metricName: body!.metricName, + hourId: hour.id!, + dayId: hour.dayId + }; + + const measurement = + body?.value + ? await api.measurements.setValue({ + ...obj, + value: body.value!, + }) + : await api.measurements.incrementCount(obj); + return { status: 201, resp: measurement }; + }, + }); \ No newline at end of file diff --git a/packages/shared/types/metrics.ts b/packages/shared/types/metrics.ts index 832ae67..024b398 100644 --- a/packages/shared/types/metrics.ts +++ b/packages/shared/types/metrics.ts @@ -13,6 +13,13 @@ export const zMeasurementSchema = z.object({ }); export type ZMeasurement = z.infer; +export const zMeasurementInputSchema = z.object({ + metricName: z.string(), + dateTimeQuery: z.string().nullish(), + timezone: z.string().nullish(), + value: z.coerce.number().nullish(), +}); + export const zMetricSchema = z.object({ id: z.string().optional(), name: z.string(), diff --git a/packages/trpc/routers/hours.ts b/packages/trpc/routers/hours.ts index 7939491..4819b93 100644 --- a/packages/trpc/routers/hours.ts +++ b/packages/trpc/routers/hours.ts @@ -92,15 +92,16 @@ export const hoursAppRouter = router({ })) .output(zHourSchema) .query(async ({ input, ctx }) => { - const date = dateFromInput({ dateQuery: input.dateQuery }); - const hourRes = await getHourSelectQuery(ctx, date, input.time, - and(eq(hours.time, input.time), eq(days.date, date)) - ); - - return { - date: format(date, "yyyy-MM-dd"), - ...hourRes[0] - }; + const hourRes = await ctx.db.select().from(hours) + .leftJoin(days, eq(days.id, hours.dayId)) + .where( + and( + eq(hours.time, input.time), + eq(days.date, input.dateQuery), + eq(hours.userId, ctx.user!.id) + ) + ); + return hourRes[0].hour; }), update: authedProcedure .input( @@ -151,7 +152,8 @@ export const hoursAppRouter = router({ }; if (newProps.comment == "") { newProps.comment = null } - + console.log(input.dayId); + console.log(input.hourTime); const hourRes = await ctx.db .update(hours) .set(newProps) diff --git a/packages/trpc/routers/measurements.ts b/packages/trpc/routers/measurements.ts index 2c10e92..d15a454 100644 --- a/packages/trpc/routers/measurements.ts +++ b/packages/trpc/routers/measurements.ts @@ -8,6 +8,23 @@ import { zMetricSchema, zMeasurementSchema } from "@lifetracker/shared/types/met 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 @@ -25,19 +42,23 @@ export const measurementsAppRouter = router({ return dbMeasurements; }), setValue: authedProcedure - .input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string(), value: z.number() })) + .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 }) => { - const metric = await ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId)); - if (!metric[0]) { + if (!input.metricId && !input.metricName) { throw new TRPCError({ - code: "NOT_FOUND", - message: "Metric not found", + 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, input.metricId), + eq(measurements.metricId, metric.id), eq(measurements.hourId, input.hourId), )); if (existingMeasurement[0]) { @@ -47,12 +68,12 @@ export const measurementsAppRouter = router({ return { ...updatedMeasurement[0], - icon: metric[0].icon, - metricName: metric[0].name, + icon: metric.icon, + metricName: metric.name, }; } else { const newMeasurement = await ctx.db.insert(measurements).values({ - metricId: input.metricId, + metricId: metric.id, hourId: input.hourId, dayId: input.dayId, value: input.value.toString(), @@ -60,16 +81,16 @@ export const measurementsAppRouter = router({ }).returning(); return { ...newMeasurement[0], - icon: metric[0].icon, - metricName: metric[0].name, + icon: metric.icon, + metricName: metric.name, }; } }), incrementCount: authedProcedure - .input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string() })) + .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 ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId)); + const metric = await getMetricFromInput(ctx, input); if (!metric[0]) { throw new TRPCError({ code: "NOT_FOUND", @@ -144,4 +165,20 @@ export const measurementsAppRouter = router({ }); } }), + 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; + } + }), }); \ No newline at end of file diff --git a/packages/trpc/routers/metrics.ts b/packages/trpc/routers/metrics.ts index 9686a5a..7fa7894 100644 --- a/packages/trpc/routers/metrics.ts +++ b/packages/trpc/routers/metrics.ts @@ -8,6 +8,7 @@ import { zMetricSchema, zMeasurementSchema } from "@lifetracker/shared/types/met import type { Context } from "../index"; import { authedProcedure, router } from "../index"; import { zColorSchema } from "@lifetracker/shared/types/colors"; +import { titleCase } from "title-case"; export const metricsAppRouter = router({ list: authedProcedure @@ -29,7 +30,7 @@ export const metricsAppRouter = router({ const result = await trx .insert(metrics) .values({ - name: input.name, + name: titleCase(input.name), userId: ctx.user!.id, unit: input.unit ?? null, type: input.type,