Compare commits
No commits in common. "39f250c0eceff3aeebbf80c828435013ee47a9e2" and "2c3a2b520fc6993b1b4eddf2530971abc3dfb582" have entirely different histories.
39f250c0ec
...
2c3a2b520f
@ -1,5 +1,5 @@
|
||||
import Header from "@/components/dashboard/header/Header";
|
||||
import MobileSidebar from "@/components/dashboard/sidebar/MobileSidebar";
|
||||
import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar";
|
||||
import Sidebar from "@/components/dashboard/sidebar/Sidebar";
|
||||
import DemoModeBanner from "@/components/DemoModeBanner";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
@ -7,7 +7,7 @@ async function fetchDays(view: string, dateQuery: string) {
|
||||
const timezone = await api.users.getTimezone();
|
||||
|
||||
const today = spacetime(dateQuery ?? "today");
|
||||
const firstDay = today.subtract(2, "week").startOf("week");
|
||||
const firstDay = today.subtract(3, "day").last("week").startOf("week");
|
||||
|
||||
const days = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Header from "@/components/dashboard/header/Header";
|
||||
import DemoModeBanner from "@/components/DemoModeBanner";
|
||||
import MobileSidebar from "@/components/settings/sidebar/MobileSidebar";
|
||||
import MobileSidebar from "@/components/settings/sidebar/ModileSidebar";
|
||||
import Sidebar from "@/components/settings/sidebar/Sidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import ValidAccountCheck from "@/components/utils/ValidAccountCheck";
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
"use client"; // Mark as client component
|
||||
|
||||
import { Separator } from "@radix-ui/react-dropdown-menu";
|
||||
import { Dispatch, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function DatabaseSettings() {
|
||||
const [uploadStatus, setUploadStatus] = useState<string | null>(null);
|
||||
const [remoteCopyStatus, setRemoteCopyStatus] = useState<string | null>(null);
|
||||
|
||||
const uploadDB = async (formData: FormData, setStatus: Dispatch<string>) => {
|
||||
// Handle form submission
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); // Prevent default form submission
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/db/upload", {
|
||||
method: "POST",
|
||||
@ -15,57 +19,15 @@ export default function DatabaseSettings() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setStatus("Database uploaded successfully!");
|
||||
setUploadStatus("Database uploaded successfully!");
|
||||
} else {
|
||||
const error = await response.json();
|
||||
setStatus(error.message || "An error occurred during upload.");
|
||||
setUploadStatus(error.message || "An error occurred during upload.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading the database:", error);
|
||||
setStatus("An error occurred during upload.");
|
||||
setUploadStatus("An error occurred during upload.");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); // Prevent default form submission
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
uploadDB(formData, setUploadStatus);
|
||||
};
|
||||
|
||||
const remoteCopy = async () => {
|
||||
|
||||
const remoteUrl = "https://lifetracker.ryanpandya.com/api/db/download";
|
||||
|
||||
setRemoteCopyStatus("Downloading...");
|
||||
const download = await fetch(remoteUrl);
|
||||
if (!download.ok) {
|
||||
throw new Error(`Failed to fetch from ${remoteUrl}: ${download.statusText}`);
|
||||
}
|
||||
|
||||
const dbData = await download.blob();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('sqliteFile', dbData, 'remoteDB-file.db');
|
||||
uploadDB(formData, setRemoteCopyStatus);
|
||||
|
||||
// try {
|
||||
// const response = await fetch("/api/db/upload", {
|
||||
// method: "POST",
|
||||
// body: formData,
|
||||
// });
|
||||
|
||||
// if (response.ok) {
|
||||
// setUploadStatus("Database uploaded successfully!");
|
||||
// } else {
|
||||
// const error = await response.json();
|
||||
// setUploadStatus(error.message || "An error occurred during upload.");
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Error uploading the database:", error);
|
||||
// setUploadStatus("An error occurred during upload.");
|
||||
// }
|
||||
};
|
||||
|
||||
return (
|
||||
@ -83,7 +45,7 @@ export default function DatabaseSettings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 flex w-full flex-col sm:flex-row">
|
||||
<div className="mb-8 flex w-full flex-col sm:flex-row">
|
||||
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
|
||||
Download SQLite Database
|
||||
</div>
|
||||
@ -95,19 +57,6 @@ export default function DatabaseSettings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{process.env.NODE_ENV === 'development' ?
|
||||
<div className="mb-2 flex w-full flex-col sm:flex-row">
|
||||
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
|
||||
Copy from Remote URL
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="mb-2">
|
||||
<button onClick={remoteCopy}>Copy</button>
|
||||
{remoteCopyStatus && <p>{remoteCopyStatus}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: ""}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -173,18 +173,14 @@ export default function EditableHour({
|
||||
{
|
||||
|
||||
hour.measurements?.map(m =>
|
||||
m.metricType === "timeseries" ?
|
||||
(<Icon name={titleCase(m.icon)} size={24}
|
||||
color={hour.foreground}
|
||||
tooltip={`${m.metricName}: ${m.value} ${m.unit}`}
|
||||
/>)
|
||||
: Array.from({ length: m.value }).map((_, index) =>
|
||||
|
||||
Array.from({ length: m.value }).map((_, index) =>
|
||||
<div key={`${m.id}-${index}`} className="hover:cursor-no-drop" onClick={(e) => {
|
||||
decrementCount({ metricId: m.metricId, hourId: hour.id });
|
||||
}}>
|
||||
<Icon name={titleCase(m.icon)} size={24}
|
||||
color={hour.foreground}
|
||||
tooltip={`${m.metricName} x${m.value}`}
|
||||
tooltip={m.metricName}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -198,11 +194,7 @@ export default function EditableHour({
|
||||
}
|
||||
|
||||
<HourMeasurementsDialog hour={hour} metrics={metrics} reload={reload}>
|
||||
{
|
||||
hour.measurements.length > 0
|
||||
? <Pencil size={16} className="opacity-50 hover:cursor-pointer" />
|
||||
: <Plus size={16} className="opacity-50 hover:cursor-pointer" />
|
||||
}
|
||||
<Plus size={16} className="opacity-50 hover:cursor-pointer" />
|
||||
</HourMeasurementsDialog>
|
||||
</div>
|
||||
: ""}
|
||||
|
||||
@ -12,17 +12,11 @@ import { ZHour } from "@lifetracker/shared/types/days";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { titleCase } from "title-case";
|
||||
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Trash, X } from "lucide-react";
|
||||
import { useDecrementCount, useIncrementCount, useSetValue, } from "@lifetracker/shared-react/hooks/measurements";
|
||||
import { X } from "lucide-react";
|
||||
import { useDecrementCount, useIncrementCount, } from "@lifetracker/shared-react/hooks/measurements";
|
||||
import { Separator } from "@radix-ui/react-dropdown-menu";
|
||||
import { EditableText } from "../EditableText";
|
||||
|
||||
interface CreateMeasurementSchema {
|
||||
type: string;
|
||||
dayId: string;
|
||||
hourId: string;
|
||||
metricId: string;
|
||||
}
|
||||
type CreateMeasurementSchema = z.infer<typeof zMeasurementSchema>;
|
||||
|
||||
export default function HourMeasurementsDialog({
|
||||
hour: initialHour,
|
||||
@ -37,51 +31,6 @@ export default function HourMeasurementsDialog({
|
||||
}) {
|
||||
const [hour, setHour] = useState(initialHour);
|
||||
const [isOpen, onOpenChange] = useState(false);
|
||||
const [pendingMeasurement, setPendingMeasurement] = useState(false);
|
||||
const pendingRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
pendingRef.current?.focus();
|
||||
}, [pendingMeasurement]);
|
||||
|
||||
const addMeasurement = (measurement: CreateMeasurementSchema) => {
|
||||
const { type, ...rest } = measurement;
|
||||
// Use a different function based on type of measurement.
|
||||
// If it's a timeseries, open a sub dialog to get the value
|
||||
// Use a switch statement to determine which function to call
|
||||
|
||||
// Javascript case/switch:
|
||||
switch (type) {
|
||||
case "timeseries":
|
||||
setPendingMeasurement(rest);
|
||||
pendingRef.current?.focus();
|
||||
break;
|
||||
default:
|
||||
increment(rest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const { mutate: setValue } = useSetValue({
|
||||
onSuccess: (res) => {
|
||||
const oldMeasurement = hour.measurements.find(m => m.metricId === res.metricId);
|
||||
const newHour = {
|
||||
...hour,
|
||||
measurements: oldMeasurement ? hour.measurements.map(m => m.metricId === res.metricId ? res : m) : [...hour.measurements, res],
|
||||
};
|
||||
setHour(newHour);
|
||||
reload(newHour);
|
||||
toast({
|
||||
description: "Measurement added!",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
description: error.message,
|
||||
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: increment } = useIncrementCount({
|
||||
onSuccess: (res) => {
|
||||
@ -92,6 +41,7 @@ export default function HourMeasurementsDialog({
|
||||
};
|
||||
setHour(newHour);
|
||||
reload(newHour);
|
||||
console.log("New hour's deets", newHour.measurements);
|
||||
toast({
|
||||
description: "Measurement added!",
|
||||
});
|
||||
@ -149,19 +99,6 @@ export default function HourMeasurementsDialog({
|
||||
currentMeasurements = hour.measurements.map(measurement => measurement.metricName);
|
||||
}, [hour]);
|
||||
|
||||
function groupMetricsByType(list: ZMetric[]): Record<string, ZMetric[]> {
|
||||
return list.reduce<Record<string, ZMetric[]>>((accumulator, item) => {
|
||||
// Initialize the array for this type if it doesn't exist yet
|
||||
if (!accumulator[item.type]) {
|
||||
accumulator[item.type] = [];
|
||||
}
|
||||
accumulator[item.type].push(item);
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// const metricsByType = groupMetricsByType(metrics);
|
||||
const metricsByType = groupMetricsByType(metrics.filter(m => { return !currentMeasurements.includes(m.name) }));
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
@ -171,35 +108,12 @@ export default function HourMeasurementsDialog({
|
||||
<DialogTitle>Metrics for {hour.date} at {hour.datetime}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Separator />
|
||||
{(hour.measurements && hour.measurements.length > 0) || pendingMeasurement ?
|
||||
{hour.measurements && hour.measurements.length > 0 ?
|
||||
(<>
|
||||
{/* <div className="font-bold">Measurements</div> */}
|
||||
<div className="font-bold">Measurements</div>
|
||||
<div className="mx-4 mb-4">
|
||||
{hour.measurements.map(measurement => {
|
||||
const metric = metrics.find(m => m.id === measurement.metricId);
|
||||
if (metric!.type === "timeseries") {
|
||||
return (
|
||||
<div key={measurement.id} className="flex items-center justify-between">
|
||||
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
||||
<div>{metric.name}</div></div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<EditableText originalText={measurement.value + " " + metric?.unit} onSave={(value) => {
|
||||
setValue({
|
||||
metricId: metric.id,
|
||||
hourId: hour.id,
|
||||
dayId: hour.dayId,
|
||||
value: parseFloat(value),
|
||||
});
|
||||
}} />
|
||||
<Trash size={16} color="red" onClick={() => {
|
||||
decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||
}} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div key={measurement.id} className="flex items-center justify-between">
|
||||
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
||||
@ -215,60 +129,51 @@ export default function HourMeasurementsDialog({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{pendingMeasurement && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="gap-4 flex"><Icon name={titleCase(pendingMeasurement.icon)} size={24} />
|
||||
<div>{metrics.find(m => m.id === pendingMeasurement.metricId)?.name}</div></div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div>
|
||||
<input
|
||||
ref={pendingRef}
|
||||
type="text" className="border border-gray-300 rounded-md p-1 mr-1 w-16 text-right"
|
||||
value={pendingMeasurement.value}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setValue({
|
||||
...pendingMeasurement,
|
||||
value: parseFloat(pendingRef.current.value),
|
||||
});
|
||||
setPendingMeasurement(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{pendingMeasurement.unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>)
|
||||
: <>
|
||||
</>
|
||||
}
|
||||
|
||||
{Object.keys(metricsByType).map(type => (
|
||||
<>
|
||||
<div className="font-bold border-b border-white">{titleCase(type)}</div>
|
||||
<div className="grid" style={{ gridTemplateColumns: "repeat(4, 1fr)", rowGap: "1em" }}>
|
||||
{metricsByType[type].map(metric => (
|
||||
<div className="font-bold">Add Measurement</div>
|
||||
<div className="grid" style={{ gridTemplateColumns: "repeat(6, 1fr)" }}>
|
||||
{metrics.map(metric => (
|
||||
// If metric.name is in currentMeasurements, don't show it
|
||||
currentMeasurements.includes(metric.name) ? null :
|
||||
<button style={{}} key={metric.id} className="flex flex-col items-center justify-center hover:opacity-50" onClick={() => {
|
||||
addMeasurement({ ...metric, metricId: metric.id!, hourId: hour.id!, dayId: hour.dayId });
|
||||
<button style={{ aspectRatio: "1/1" }} key={metric.id} className="flex flex-col items-center justify-center hover:opacity-50" onClick={() => {
|
||||
increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||
// onOpenChange(false);
|
||||
}}>
|
||||
<Icon name={titleCase(metric.icon)} size={32} />
|
||||
<span className="text-sm">{metric.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div></>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // Show a dropdown menu with an option for each metric. When the user selects a metric, increment the count for that metric in the hour.
|
||||
// // The dropdown should show the metric name and icon.
|
||||
// // Reset the dropdown to the default value after the user selects a metric.
|
||||
|
||||
// <Select
|
||||
// className="w-full h-2"
|
||||
// unstyled={true}
|
||||
// isSearchable={false}
|
||||
// placeholder="Add"
|
||||
// options={metrics.map(metric =>
|
||||
// ({ value: metric.id, label: metric.name, icon: metric.icon }))}
|
||||
// onChange={(selectedOption) => incrementCount(selectedOption.value)}
|
||||
// classNames={{
|
||||
// menu: (state) =>
|
||||
// "bg-card px-4 py-2 text-white rounded-md shadow-lg",
|
||||
// }}
|
||||
// />
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@ -52,8 +52,8 @@ export default function AddMetricDialog({
|
||||
const form = useForm<CreateMetricSchema>({
|
||||
resolver: zodResolver(zMetricSchema),
|
||||
defaultValues: {
|
||||
type: "count",
|
||||
...initialMetric,
|
||||
type: "count"
|
||||
},
|
||||
});
|
||||
const handleSuccess = (message: string) => {
|
||||
@ -117,12 +117,7 @@ export default function AddMetricDialog({
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{initialMetric
|
||||
? "Update " + initialMetric.name
|
||||
: "New " + titleCase(form.watch('type')) + " Metric"
|
||||
}
|
||||
</DialogTitle>
|
||||
<DialogTitle>Track a New {titleCase(form.watch("type"))} Metric</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit((val) => mutate(val))}>
|
||||
@ -147,30 +142,6 @@ export default function AddMetricDialog({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select onValueChange={field.onChange} defaultValue={initialMetric?.type ?? "count"}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a type of metric to track" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="drug">Drug</SelectItem>
|
||||
<SelectItem value="diet">Diet</SelectItem>
|
||||
<SelectItem value="workout">Workout</SelectItem>
|
||||
<SelectItem value="sex">Sex</SelectItem>
|
||||
<SelectItem value="count">Count</SelectItem>
|
||||
<SelectItem value="timeseries">Timeseries</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="unit"
|
||||
@ -182,12 +153,35 @@ export default function AddMetricDialog({
|
||||
placeholder="Unit"
|
||||
{...field}
|
||||
className="w-full rounded border p-2"
|
||||
disabled={(form.watch("type") === "count")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Select field with "timeseries" and "count" */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select onValueChange={field.onChange} defaultValue={"count"}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a type of metric to track" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="timeseries">Timeseries</SelectItem>
|
||||
<SelectItem value="count">Count</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<FormField
|
||||
|
||||
@ -54,12 +54,20 @@ export default function MetricsView() {
|
||||
<TableCell className="py-1">{m.type}</TableCell>
|
||||
<TableCell className="py-1">{m.description}</TableCell>
|
||||
<TableCell className="flex gap-1 py-1">
|
||||
<ActionButtonWithTooltip
|
||||
tooltip="Delete category"
|
||||
variant="outline"
|
||||
onClick={() => deleteMetric({ id: m.id! })}
|
||||
loading={false}
|
||||
>
|
||||
<Trash size={16} color="red" />
|
||||
</ActionButtonWithTooltip>
|
||||
<AddMetricDialog initialMetric={m} >
|
||||
<ButtonWithTooltip
|
||||
tooltip="Edit"
|
||||
variant="outline"
|
||||
>
|
||||
<Pencil size={16} color="white" />
|
||||
<Pencil size={16} color="red" />
|
||||
</ButtonWithTooltip>
|
||||
</AddMetricDialog>
|
||||
</TableCell>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import MobileSidebarItem from "@/components/shared/sidebar/MobileSidebarItem";
|
||||
import MobileSidebarItem from "@/components/shared/sidebar/ModileSidebarItem";
|
||||
import HoarderLogoIcon from "@/public/icons/logo-icon.svg";
|
||||
import { CheckCheck, ClipboardList, GaugeCircleIcon, Home, HomeIcon, Search, Tag } from "lucide-react";
|
||||
|
||||
@ -11,7 +11,7 @@ export default async function MobileSidebar() {
|
||||
path="/dashboard/day/today"
|
||||
/>
|
||||
<MobileSidebarItem logo={<Tag />} path="/dashboard/categories" />
|
||||
<MobileSidebarItem logo={<CheckCheck />} path="/dashboard/metrics" />
|
||||
<MobileSidebarItem logo={<CheckCheck />} path="/dashboard/habits" />
|
||||
<MobileSidebarItem logo={<GaugeCircleIcon />} path="/analytics" />
|
||||
</ul>
|
||||
</aside>
|
||||
@ -1,18 +1,5 @@
|
||||
import { api } from "../trpc";
|
||||
|
||||
|
||||
export function useSetValue(
|
||||
...opts: Parameters<typeof api.measurements.setValue.useMutation>
|
||||
) {
|
||||
const apiUtils = api.useUtils();
|
||||
return api.measurements.setValue.useMutation({
|
||||
...opts[0],
|
||||
onSuccess: (res, req, meta) => {
|
||||
return opts[0]?.onSuccess?.(res, req, meta);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useIncrementCount(
|
||||
...opts: Parameters<typeof api.measurements.incrementCount.useMutation>
|
||||
) {
|
||||
|
||||
@ -4,8 +4,6 @@ export const zMeasurementSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
metricId: z.string(),
|
||||
metricName: z.string().optional(),
|
||||
metricType: z.string().optional(),
|
||||
unit: z.string().nullish(),
|
||||
hourId: z.string().optional(),
|
||||
dayId: z.string().optional(),
|
||||
value: z.coerce.number(),
|
||||
@ -18,7 +16,7 @@ export const zMetricSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
unit: z.string().nullish(),
|
||||
type: z.enum(["timeseries", "count", "drug", "diet", "workout", "sex"]),
|
||||
type: z.enum(["timeseries", "count"]),
|
||||
icon: z.string(),
|
||||
measurements: z.array(zMeasurementSchema).optional(),
|
||||
});
|
||||
|
||||
@ -64,10 +64,8 @@ export async function hourJoinsQuery(
|
||||
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))
|
||||
|
||||
@ -24,47 +24,6 @@ export const measurementsAppRouter = router({
|
||||
console.log(dbMeasurements.length);
|
||||
return dbMeasurements;
|
||||
}),
|
||||
setValue: authedProcedure
|
||||
.input(z.object({ metricId: z.string(), 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]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Metric not found",
|
||||
});
|
||||
}
|
||||
// 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.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[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: input.value.toString(),
|
||||
userId: ctx.user.id,
|
||||
}).returning();
|
||||
return {
|
||||
...newMeasurement[0],
|
||||
icon: metric[0].icon,
|
||||
metricName: metric[0].name,
|
||||
};
|
||||
}
|
||||
}),
|
||||
incrementCount: authedProcedure
|
||||
.input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string() }))
|
||||
.output(zMeasurementSchema)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user