Category creation logic

This commit is contained in:
ryan 2025-02-06 11:00:33 -08:00
parent 444e20f5b1
commit 899ea04954
5 changed files with 69 additions and 24 deletions

View File

@ -2,6 +2,22 @@
import { LineChart, Line, YAxis, XAxis, CartesianGrid, Tooltip, Area, Label } from 'recharts'; import { LineChart, Line, YAxis, XAxis, CartesianGrid, Tooltip, Area, Label } from 'recharts';
import spacetime from 'spacetime'; import spacetime from 'spacetime';
import regression from 'regression'; import regression from 'regression';
import { PureComponent } from 'react';
class CustomizedAxisTick extends PureComponent {
render() {
const { x, y, stroke, payload } = this.props;
if (spacetime(payload.value).day() == 1) {
return (
<g transform={`translate(${x},${y})`}>
<text x={0} y={0} dy={16} textAnchor="end" fill="#666" transform="rotate(-35)">
{spacetime(payload.value).format('{date}')}
</text>
</g>
);
}
}
}
interface DataPoint { interface DataPoint {
id: string; // measurement.id id: string; // measurement.id
@ -22,12 +38,8 @@ function polynomialTrendline(data: DataPoint[], degree: number) {
} }
function formatXAxis(tickItem: [number, number]) { function formatXAxis(tickItem: [number, number]) {
const month = spacetime(tickItem).format('{month-short}');
if (spacetime(tickItem).day() === 1) { return spacetime(tickItem).format('{date}');
return spacetime(tickItem).format('{month-short} {date}');
} else {
return spacetime(tickItem).format('{date}');
}
} }
function yAxisTicks(data: DataPoint[]) { function yAxisTicks(data: DataPoint[]) {
@ -38,6 +50,19 @@ function yAxisTicks(data: DataPoint[]) {
return Array.from({ length: (upperBound - lowerBound) / 2 + 1 }, (_, i) => lowerBound + 2 * i); return Array.from({ length: (upperBound - lowerBound) / 2 + 1 }, (_, i) => lowerBound + 2 * i);
} }
function xAxisTicks(data: DataPoint[]) {
const bounds = data.sort().filter((_v, i, a) => i === 0 || i === a.length - 1).map(dp => spacetime(dp.datetime));
// Create array from bounds[0] to bounds[1] showing only Mondays
const ticks = [];
let tick = bounds[0].startOf('week').add(1, 'day');
while (tick.isBefore(bounds[1])) {
ticks.push(tick.format('{date}'));
tick = tick.add(2, 'day');
}
console.log(ticks);
return ticks;
}
export default function TimeseriesChart({ data, timeseriesName }: { data: DataPoint[], timeseriesName: string }) { export default function TimeseriesChart({ data, timeseriesName }: { data: DataPoint[], timeseriesName: string }) {
const unit = data[0] ? data[0].unit : ''; const unit = data[0] ? data[0].unit : '';
@ -63,8 +88,18 @@ export default function TimeseriesChart({ data, timeseriesName }: { data: DataPo
formatter={(value, name, props) => [value, name === 'smoothed' ? 'Trend' : timeseriesName]} formatter={(value, name, props) => [value, name === 'smoothed' ? 'Trend' : timeseriesName]}
contentStyle={{ backgroundColor: 'hsl(var(--border))', color: 'black' }} contentStyle={{ backgroundColor: 'hsl(var(--border))', color: 'black' }}
/> />
<XAxis dataKey="datetime" tickFormatter={formatXAxis} tickMargin={10} tickSize={10} /> <XAxis
<YAxis unit={` ${unit}`} width={120} domain={yAxisTicks(data).filter((v, i) => i === 0 || i === yAxisTicks(data).length - 1)} dataKey="datetime"
type='number'
height={80}
tickMargin={10} tickSize={10}
domain={['dataMin', 'dataMax']}
tick={<CustomizedAxisTick />}
interval={0}
// tickFormatter={formatXAxis}
/>
<YAxis unit={` ${unit}`} width={120}
domain={yAxisTicks(data).filter((v, i) => i === 0 || i === yAxisTicks(data).length - 1)}
allowDecimals={false} allowDecimals={false}
ticks={yAxisTicks(data)} /> ticks={yAxisTicks(data)} />
<Line type="natural" dataKey="value" stroke="white" activeDot={{ stroke: 'pink', strokeWidth: 2, r: 5 }} dot={{ fill: 'red', stroke: 'red', strokeWidth: 1 }} /> <Line type="natural" dataKey="value" stroke="white" activeDot={{ stroke: 'pink', strokeWidth: 2, r: 5 }} dot={{ fill: 'red', stroke: 'red', strokeWidth: 1 }} />

View File

@ -33,10 +33,10 @@ import { TRPCClientError } from "@trpc/client";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { zCategorySchema } from "@lifetracker/shared/types/categories"; import { zCategorySchema, zCreateCategorySchema } from "@lifetracker/shared/types/categories";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
type CreateCategorySchema = z.infer<typeof zCategorySchema>; type CreateCategorySchema = z.infer<typeof zCreateCategorySchema>;
export default function AddCategoryDialog({ export default function AddCategoryDialog({
children, children,
@ -46,9 +46,9 @@ export default function AddCategoryDialog({
const apiUtils = api.useUtils(); const apiUtils = api.useUtils();
const [isOpen, onOpenChange] = useState(false); const [isOpen, onOpenChange] = useState(false);
const form = useForm<CreateCategorySchema>({ const form = useForm<CreateCategorySchema>({
resolver: zodResolver(zCategorySchema), resolver: zodResolver(zCreateCategorySchema),
}); });
const { mutate, isPending } = api.categories.createCategory.useMutation({ const { mutate: createCategory, isPending } = api.categories.create.useMutation({
onSuccess: () => { onSuccess: () => {
toast({ toast({
description: "Category created successfully", description: "Category created successfully",
@ -84,7 +84,7 @@ export default function AddCategoryDialog({
<DialogTitle>Create Category</DialogTitle> <DialogTitle>Create Category</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((val) => mutate(val))}> <form onSubmit={form.handleSubmit((val) => createCategory(val))}>
<div className="flex w-full flex-col space-y-2"> <div className="flex w-full flex-col space-y-2">
<div style={{ display: "grid", gridTemplateColumns: "5em 1fr 75px", gap: "10px" }}> <div style={{ display: "grid", gridTemplateColumns: "5em 1fr 75px", gap: "10px" }}>
<FormField <FormField
@ -125,7 +125,7 @@ export default function AddCategoryDialog({
/> />
<FormField <FormField
control={form.control} control={form.control}
name="color" name="colorName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
{/* <FormLabel>Color</FormLabel> */} {/* <FormLabel>Color</FormLabel> */}

View File

@ -14,7 +14,7 @@ export const zCreateCategorySchema = z.object({
code: z.coerce.number(), code: z.coerce.number(),
name: z.string(), name: z.string(),
description: z.string().optional(), description: z.string().optional(),
color: z.string(), colorName: z.string(),
parentId: z.string().optional(), parentId: z.string().optional(),
}); });
export type ZCreateCategories = z.infer<typeof zCreateCategorySchema>; export type ZCreateCategories = z.infer<typeof zCreateCategorySchema>;

View File

@ -3,7 +3,7 @@ import { z } from "zod";
export const zColorSchema = z.object({ export const zColorSchema = z.object({
name: z.string(), name: z.string(),
hexcode: z.string(), hexcode: z.string(),
inverse: z.string().optional(), inverse: z.string().nullish(),
id: z.string().optional(), id: z.string().nullish(),
}); });
export type ZColor = z.infer<typeof zColorSchema>; export type ZColor = z.infer<typeof zColorSchema>;

View File

@ -1,5 +1,5 @@
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
import { and, desc, eq, inArray, notExists } from "drizzle-orm"; import { and, asc, desc, eq, inArray, notExists } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { DatabaseError } from "@lifetracker/db"; import { DatabaseError } from "@lifetracker/db";
@ -102,11 +102,15 @@ export const categoriesAppRouter = router({
.leftJoin(colors, eq(categories.colorId, colors.id)) .leftJoin(colors, eq(categories.colorId, colors.id))
; ;
const categoryParents = dbCategories
.filter((category) => category.code == parseInt(category.code) && category.code < 11)
.sort((a, b) => a.code - b.code);
const sortedCategories = categoryParents.map((parent) => {
return dbCategories
.filter((child) => child.code.toString().startsWith(parent.code.toString()))
});
return { return {
categories: dbCategories.map(({ color, ...category }) => ({ categories: sortedCategories.flat(),
...category,
color,
})),
}; };
}), }),
get: authedProcedure get: authedProcedure
@ -173,7 +177,7 @@ export const categoriesAppRouter = router({
return category; return category;
}), }),
create: authedProcedure create: authedProcedure
.input(zCategorySchema) .input(zCreateCategorySchema)
.output( .output(
z.object({ z.object({
id: z.string(), id: z.string(),
@ -184,7 +188,13 @@ export const categoriesAppRouter = router({
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
return createCategory(input, ctx); const color = await ctx.db.select().from(colors).where(
and(
eq(colors.name, input.colorName),
eq(colors.userId, ctx.user.id),
)
);
return createCategory({ ...input, color: color[0] }, ctx);
}), }),
update: authedProcedure update: authedProcedure
.input(zUpdateCategoryRequestSchema) .input(zUpdateCategoryRequestSchema)