Category creation logic
This commit is contained in:
parent
444e20f5b1
commit
899ea04954
@ -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 }} />
|
||||||
|
|||||||
@ -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> */}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user