257 lines
8.4 KiB
TypeScript
257 lines
8.4 KiB
TypeScript
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
|
|
import { and, desc, eq, inArray, notExists } from "drizzle-orm";
|
|
import { date, z } from "zod";
|
|
|
|
import { SqliteError } from "@lifetracker/db";
|
|
import { categories, days, hours, colors, measurements, metrics } from "@lifetracker/db/schema";
|
|
import {
|
|
zDaySchema, ZDay, ZHour, zHourSchema
|
|
} from "@lifetracker/shared/types/days";
|
|
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";
|
|
|
|
export async function hourColors(hour: ZHour, ctx: Context) {
|
|
const categoryColor = await ctx.db.select()
|
|
.from(colors)
|
|
.leftJoin(categories, eq(categories.id, hour.categoryId))
|
|
.where(and(
|
|
eq(colors.id, categories.colorId),
|
|
eq(colors.userId, ctx.user!.id)
|
|
))
|
|
|
|
if (!categoryColor[0]) {
|
|
return {
|
|
background: "inherit",
|
|
foreground: "inherit"
|
|
}
|
|
}
|
|
else {
|
|
return {
|
|
background: categoryColor[0].color.hexcode,
|
|
foreground: categoryColor[0].color.inverse,
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function hourJoinsQuery(
|
|
ctx: Context,
|
|
dayId: string,
|
|
time: number,
|
|
) {
|
|
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, time),
|
|
eq(hours.dayId, dayId)
|
|
));
|
|
|
|
const hourMeasurements = await ctx.db.select({
|
|
id: measurements.id,
|
|
metricId: measurements.metricId,
|
|
value: measurements.value,
|
|
unit: metrics.unit,
|
|
icon: metrics.icon,
|
|
metricName: metrics.name,
|
|
metricType: metrics.type,
|
|
})
|
|
.from(measurements)
|
|
.leftJoin(metrics, eq(metrics.id, measurements.metricId))
|
|
.where(eq(measurements.hourId, hourMatch[0].id));
|
|
|
|
const dayHour = {
|
|
measurements: hourMeasurements,
|
|
...hourMatch[0],
|
|
...(await hourColors(hourMatch[0], ctx)),
|
|
};
|
|
|
|
return dayHour;
|
|
|
|
};
|
|
|
|
function listOfDates(dateRange: [Date, Date]) {
|
|
const [start, end] = dateRange.map((date) => dateFromInput({
|
|
dateQuery: spacetime(date, "UTC").goto("UTC").format("iso-short"),
|
|
timezone: "Etc/UTC"
|
|
}));
|
|
const dates = [];
|
|
let currentDate = parseISO(start);
|
|
while (currentDate <= parseISO(end)) {
|
|
dates.push(format(currentDate, "yyyy-MM-dd"));
|
|
currentDate = addDays(currentDate, 1);
|
|
}
|
|
return dates.length === 0 ? parseISO(start) : dates;
|
|
}
|
|
|
|
|
|
export const hoursAppRouter = router({
|
|
get: authedProcedure
|
|
.input(z.object({
|
|
dateQuery: z.string(),
|
|
time: z.number()
|
|
}))
|
|
.output(zHourSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
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(
|
|
z.object({
|
|
date: z.string(),
|
|
hourTime: z.number(),
|
|
dayId: z.string(),
|
|
code: z.string().nullish(),
|
|
comment: z.string().nullable().optional(),
|
|
}),
|
|
)
|
|
.output(zHourSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { code, ...updatedProps } = input;
|
|
let dateCondition;
|
|
|
|
if (input.dayId) {
|
|
dateCondition = eq(days.id, input.dayId)
|
|
}
|
|
else {
|
|
throw new TRPCError({ code: "BAD_REQUEST", message: "dayId is required" });
|
|
}
|
|
|
|
const category =
|
|
code == "" ? [{
|
|
id: null,
|
|
name: null
|
|
}]
|
|
: await ctx.db.select(
|
|
{
|
|
id: categories.id,
|
|
name: categories.name,
|
|
description: categories.description
|
|
}
|
|
)
|
|
.from(categories)
|
|
.where(
|
|
and(
|
|
eq(categories.code, code),
|
|
eq(categories.userId, ctx.user!.id),
|
|
)
|
|
);
|
|
|
|
const newProps = {
|
|
categoryId: category[0].id,
|
|
code: code,
|
|
...updatedProps
|
|
};
|
|
|
|
if (newProps.comment == "") { newProps.comment = null }
|
|
|
|
const hourRes = await ctx.db
|
|
.update(hours)
|
|
.set(newProps)
|
|
.where(
|
|
and(
|
|
eq(hours.time, input.hourTime),
|
|
eq(hours.dayId, input.dayId),
|
|
eq(hours.userId, ctx.user!.id)
|
|
)
|
|
).returning();
|
|
|
|
// return {
|
|
// date: input.date,
|
|
// ...hourRes[0]
|
|
// };
|
|
|
|
return hourJoinsQuery(ctx, input.dayId, input.hourTime);
|
|
|
|
}),
|
|
|
|
categoryFrequencies: authedProcedure
|
|
.input(z.object({
|
|
dateRange: z.tuple([z.date(), z.date()])
|
|
}))
|
|
.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()
|
|
}
|
|
)))
|
|
.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);
|
|
|
|
// Count total hours in the filtered range
|
|
const totalHours = hoursList.length;
|
|
|
|
console.log(hoursList);
|
|
|
|
// Group hours by category and count occurrences
|
|
const categoriesList = {};
|
|
hoursList.forEach(h => {
|
|
if (!categoriesList[h.categoryCode]) {
|
|
categoriesList[h.categoryCode] = {
|
|
count: 0,
|
|
...h
|
|
};
|
|
}
|
|
const old = (categoriesList[h.categoryCode].count);
|
|
categoriesList[h.categoryCode].count = old + 1;
|
|
});
|
|
// Calculate percentages
|
|
const categoryPercentages: any = Object.keys(categoriesList).map(categoryCode => {
|
|
const count = categoriesList[categoryCode].count;
|
|
const percentage = (count / totalHours);
|
|
return {
|
|
...categoriesList[categoryCode],
|
|
percentage: percentage
|
|
};
|
|
});
|
|
return categoryPercentages;
|
|
}),
|
|
});
|