Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions src/api/redmine/hooks/useRedmineProjectTimeEntryActivities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redmineProjectQuery } from "@/api/redmine/queries/projects";
import { redmineTimeEntryActivitiesQuery } from "@/api/redmine/queries/timeEntryActivities";
import { useQuery } from "@tanstack/react-query";
import { TProject, TTimeEntryActivity } from "@/api/redmine/types";
import { useQueries, useQuery, UseQueryResult } from "@tanstack/react-query";
import { useRedmineApi } from "../../../provider/RedmineApiProvider";

export const useRedmineProjectTimeEntryActivities = (projectId: number) => {
Expand All @@ -9,17 +10,51 @@ export const useRedmineProjectTimeEntryActivities = (projectId: number) => {
const projectQuery = useQuery(redmineProjectQuery(redmineApi, projectId));
const systemTimeEntryActivitiesQuery = useQuery(redmineTimeEntryActivitiesQuery(redmineApi));

const projectTimeEntryActivities = projectQuery.data?.time_entry_activities;
const activities = projectTimeEntryActivities
? projectTimeEntryActivities?.map((activity) => ({
...systemTimeEntryActivitiesQuery.data?.find((systemActivity) => systemActivity.id === activity.id),
...activity,
}))
: systemTimeEntryActivitiesQuery.data?.filter((activity) => activity.active !== false);
const activities = resolveProjectTimeEntryActivities(projectQuery.data, systemTimeEntryActivitiesQuery.data);

return {
activities,
defaultActivity: activities?.find((entry) => entry.is_default),
isPending: projectQuery.isPending || systemTimeEntryActivitiesQuery.isPending,
};
};

export const useRedmineMultipleProjectTimeEntryActivities = (projectIds: number[]) => {
const redmineApi = useRedmineApi();

const projectsQueries = useQueries({
queries: projectIds.map((id) => redmineProjectQuery(redmineApi, id)),
combine: combineMultipleProjects,
});
const systemTimeEntryActivitiesQuery = useQuery(redmineTimeEntryActivitiesQuery(redmineApi));

const projectTimeEntryActivitiesMap = Object.fromEntries(
projectIds.map((projectId) => {
const activities = resolveProjectTimeEntryActivities(projectsQueries.data[projectId], systemTimeEntryActivitiesQuery.data);
return [projectId, { activities, defaultActivity: activities?.find((activity) => activity.is_default) }];
})
);

return {
projectTimeEntryActivitiesMap,
};
};

const combineMultipleProjects = (results: UseQueryResult<TProject, Error>[]) => ({
data: results.reduce<Record<number, TProject>>((result, query) => {
if (query.data) {
result[query.data.id] = query.data;
}
return result;
}, {}),
});

const resolveProjectTimeEntryActivities = (project: TProject | undefined, systemActivities: TTimeEntryActivity[] | undefined) => {
const projectTimeEntryActivities = project?.time_entry_activities;
return projectTimeEntryActivities
? projectTimeEntryActivities?.map((activity) => ({
...systemActivities?.find((systemActivity) => systemActivity.id === activity.id),
...activity,
}))
: systemActivities?.filter((activity) => activity.active !== false);
};
55 changes: 40 additions & 15 deletions src/components/time-entry/TimeEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TimeEntryContextMenu } from "@/components/time-entry/TimeEntryContextMenu";
import { clsx } from "clsx";
import { Fragment } from "react";
import { TTimeEntry } from "../../api/redmine/types";
import useFormatHours from "../../hooks/useFormatHours";
Expand All @@ -7,27 +8,42 @@ import TimeEntryTooltip from "./TimeEntryTooltip";

type PropTypes = {
entries: TTimeEntry[];
previewHours?: number;
maxDayHours?: number;
preview?: {
hours: number;
name?: string;
}[];
size?: "sm" | "md" | "lg";
withContextMenu?: boolean;
};

const TimeEntry = ({ entries, previewHours, maxDayHours = 24, withContextMenu = false }: PropTypes) => {
const TimeEntry = ({ entries, preview, size = "sm", withContextMenu = false }: PropTypes) => {
const formatHours = useFormatHours();

const sumHours = entries.reduce((sum, entry) => sum + entry.hours, 0);
const sumPreviewHours = preview?.reduce((sum, p) => sum + p.hours, 0);
const maxHours = Math.max(8, sumHours + (sumPreviewHours ?? 0));

return (
<div role="row" className="flex items-center gap-x-0.5">
<div
role="row"
className={clsx("flex items-center", {
"gap-x-0.5": size === "sm",
"gap-x-1": size === "md" || size === "lg",
})}
>
{entries.map((entry) => {
const entryElement = (
<TimeEntryTooltip entry={entry}>
<div
role="cell"
data-type="time-entry"
className="h-4 rounded-sm bg-primary"
className={clsx("min-w-1 bg-primary", {
"h-4 rounded-sm": size === "sm",
"h-6 rounded-md": size === "md",
"h-8 rounded-lg": size === "lg",
})}
style={{
width: `${(entry.hours / maxDayHours) * 100}%`,
width: `${(entry.hours / maxHours) * 100}%`,
}}
/>
</TimeEntryTooltip>
Expand All @@ -44,28 +60,37 @@ const TimeEntry = ({ entries, previewHours, maxDayHours = 24, withContextMenu =
</Fragment>
);
})}
{!!previewHours && (
<Tooltip>
{preview?.map((p, index) => (
<Tooltip key={index}>
<TooltipTrigger
delay={300}
render={
<div
className="h-3.5 rounded-sm bg-primary/60"
className={clsx("min-w-1 bg-primary/60", {
"h-3.5 rounded-sm": size === "sm",
"h-5.5 rounded-md": size === "md",
"h-7 rounded-lg": size === "lg",
})}
style={{
width: `${(previewHours / maxDayHours) * 100}%`,
width: `${(p.hours / maxHours) * 100}%`,
}}
/>
}
/>
<TooltipContent>
<p className="text-sm font-semibold">{formatHours(previewHours)}</p>
<TooltipContent className="flex max-w-[17rem] flex-col items-start gap-y-3 truncate">
<p className="text-sm font-semibold">{formatHours(p.hours)}</p>
{p.name && <p className="truncate text-xs font-normal">{p.name}</p>}
</TooltipContent>
</Tooltip>
)}
))}
<div
className="h-3 rounded-sm bg-muted"
className={clsx("bg-muted", {
"h-3 rounded-sm": size === "sm",
"h-5 rounded-md": size === "md",
"h-7 rounded-lg": size === "lg",
})}
style={{
width: `${((maxDayHours - sumHours - (previewHours ?? 0)) / maxDayHours) * 100}%`,
width: `${((maxHours - sumHours - (sumPreviewHours ?? 0)) / maxHours) * 100}%`,
}}
/>
</div>
Expand Down
7 changes: 1 addition & 6 deletions src/components/time-entry/TimeEntryOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ export const TimeEntryOverview = ({ from, to, entries }: PropTypes) => {
(date: Date) => groupedByDate.get(formatISO(date, { representation: "date" })) ?? ({ date, entries: [], hours: 0 } satisfies GroupedTimeEntries)
);

const maxDayHours = Math.max(
groupedByDate.values().reduce((max, { hours }) => Math.max(max, hours), 0),
8
);

const totalHours = entries.reduce((sum, entry) => sum + entry.hours, 0);

const isMoreThanOneWeek = !isSameWeek(from, to, { weekStartsOn: 1 });
Expand Down Expand Up @@ -87,7 +82,7 @@ export const TimeEntryOverview = ({ from, to, entries }: PropTypes) => {
</span>
<span className="w-17 truncate text-end text-xs font-semibold text-muted-foreground">{isDisabled ? "–" : formatHours(roundHours(hours))}</span>
<div className="grow">
<TimeEntry entries={entries} maxDayHours={maxDayHours} withContextMenu />
<TimeEntry entries={entries} withContextMenu />
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/time-entry/TimeEntryPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const TimeEntryPreview = ({ date, previewHours, className }: PropTypes) => {
<div className={cn("flex items-center gap-x-1", className)}>
<h3 className="max-w-20 truncate text-sm font-semibold">{formatHours(roundHours(sumHours))}</h3>
<div className="grow">
<TimeEntry entries={timeEntries} previewHours={previewHours} maxDayHours={sumHours > 12 ? sumHours : 12} />
<TimeEntry entries={timeEntries} preview={[{ hours: previewHours }]} />
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/timer/ProjectTimersGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ export const ProjectTimersGroup = ({ projectGroup, className, ...props }: Projec
);
};

const TimerProject = ({ project, type }: { project?: TReference; type: ProjectTimersGroupType["type"] }) => {
export const TimerProject = ({ project, type, forceNoSticky }: { project?: TReference; type: ProjectTimersGroupType["type"]; forceNoSticky?: boolean }) => {
const { settings } = useSettings();

return (
<div
className={clsx("flex items-center gap-x-1 py-1", {
"sticky top-0 z-5 bg-background shadow shadow-background": settings.style.stickyScroll,
"sticky top-0 z-5 bg-background shadow shadow-background": settings.style.stickyScroll && !forceNoSticky,
})}
>
<SquareChartGanttIcon className="size-3.5 shrink-0" />
Expand Down
Loading
Loading