lifetracker/apps/web/components/dashboard/hours/EditableHour.tsx

216 lines
8.3 KiB
TypeScript

"use client";
import { usePathname, useRouter } from "next/navigation";
import { toast } from "@/components/ui/use-toast";
import { cn } from "@/lib/utils";
import React, { useState, useEffect, useRef } from 'react';
import { useUpdateHour } from "@lifetracker/shared-react/hooks/days";
import { EditableText } from "@/components/dashboard/EditableText";
import { format } from "date-fns";
import { TZDate } from "@date-fns/tz";
import { ZHour } from "@lifetracker/shared/types/days";
import { MessageCircle, Pencil, Plus } from "lucide-react";
import { ButtonWithTooltip } from "@/components/ui/button";
import { EditableHourCode } from "./EditableHourCode";
import { EditableHourComment } from "./EditableHourComment";
import spacetime from 'spacetime';
import { eq, is } from "drizzle-orm";
import HourMeasurementsDialog from "@/components/dashboard/hours/HourMeasurementsDialog";
import { ActionButtonWithTooltip } from "@/components/ui/action-button";
import { ZMetric } from "@lifetracker/shared/types/metrics";
import { Badge } from "@/components/ui/badge";
import { Icon } from "@/components/ui/icon";
import { titleCase } from "title-case";
import { useDecrementCount } from "@lifetracker/shared-react/hooks/measurements";
import { useTimezone } from "@/lib/userLocalSettings/client";
export default function EditableHour({
hour: initialHour,
i,
j,
hourGroup,
metrics,
isConsecutiveHour = false,
className,
}: {
hour: ZHour,
i: number,
j: number,
hourGroup: ZHour[],
metrics: ZMetric[] | undefined,
isConsecutiveHour?: boolean,
className?: string;
}) {
const [hour, setHour] = useState(initialHour);
const { mutate: updateHour, isPending } = useUpdateHour({
onSuccess: (res, req, meta) => {
const { categoryCode: oldCode, comment: oldComment } = hour;
const newHour = {
categoryCode: parseInt(req.code!),
comment: oldComment,
...res,
};
setHour(newHour);
// Only show toast if client screen is larger than mobile
if (window.innerWidth > 640) {
toast({
description: "Hour updated!",
});
}
},
});
const { mutate: decrementCount } = useDecrementCount({
onSuccess: (res, req) => {
const oldMeasurementIndex = hour.measurements.findIndex(m => m.metricId === req.metricId);
let newMeasurements;
if (oldMeasurementIndex !== -1) {
if (res === undefined) {
// Remove the measurement if res is undefined
newMeasurements = [
...hour.measurements.slice(0, oldMeasurementIndex),
...hour.measurements.slice(oldMeasurementIndex + 1)
];
} else {
// Update the measurement
newMeasurements = [
...hour.measurements.slice(0, oldMeasurementIndex),
res,
...hour.measurements.slice(oldMeasurementIndex + 1)
];
}
} else {
// Add the new measurement
newMeasurements = [...hour.measurements, res];
}
const newHour = {
...hour,
measurements: newMeasurements,
};
setHour(newHour);
toast({
description: res === undefined ? "Measurement removed!" : "Measurement updated!",
});
}
});
const tzOffset = spacetime.now(useTimezone()).offset() / 60;
const localDateTime = (h: ZHour): string => {
return spacetime(h.date).add(h.time + tzOffset, "hour").format('{hour} {ampm}');
}
hour.datetime = localDateTime(hour);
useEffect(() => {
// console.log(hour.categoryDesc);
}, [hour]);
function reload(newHour: ZHour) {
setHour(newHour);
}
return (
<div
data-hourid={hour.id}
className={cn(
"p-4 grid justify-between",
)}
style={{
background: hour.background!, color: hour.foreground!, fontFamily: "inherit",
// gridTemplateColumns: window.innerWidth > 640 ? "50px 100px 3fr 2fr" : "50px 100px 1fr", // Known issue: This won't work if the screen is resized, only on reload
gridTemplateColumns: "50px 100px 3fr 2fr"
}}
>
<span className="text-right hover:cursor-default">
{hourGroup.length > 1
? (
<div className="flex flex-col items-center">
<span>{hour.datetime}</span>
<span>|</span>
<span>{localDateTime(hourGroup[hourGroup.length - 1])}</span>
</div>
)
: <div className="flex flex-col items-center">{hour.datetime}</div>
}
</span>
<div className="flex justify-center">
<EditableHourCode
className={"w-8 border-b"}
originalText={hour.categoryCode}
hour={hour}
onSubmit={updateHour}
i={i}
/>
</div>
<span className="block">
{hour.categoryCode != undefined ?
<EditableHourComment
originalText={hour.comment ?? hour.categoryName}
hour={hour}
onSubmit={updateHour}
i={i}
/>
: ""}
</span>
<span className="hidden">
<div className="w-full text-left edit-hour-comment"
style={{
background: hour.background ?? "inherit", color: hour.foreground ?? "inherit", fontFamily: "inherit",
}}>
{hour.categoryName}
</div>
</span>
<div className="flex items-center justify-end">
{hour.categoryCode != undefined ?
<div className="flex items-center gap-2">
{
hour.measurements?.map(m =>
m.metricType === "timeseries" ?
(<Icon name={titleCase(m.icon)} size={24}
color={hour.foreground}
tooltip={`${m.metricName}: ${m.value} ${m.unit}`}
key={m.id}
/>)
: 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}`}
/>
</div>
)
)}
{(hour.measurements.length > 0) ?
(<span className="mx-2 opacity-50">
{/* {JSON.stringify(hour.measurements.length)} */}
|
</span>)
: ""
}
<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" />
}
</HourMeasurementsDialog>
</div>
: ""}
</div>
</div>
);
}