Skip to content

Commit

Permalink
[Feat]: Add Timelog FilterOptions (#3333)
Browse files Browse the repository at this point in the history
* feat: add useTimelogFilterOptions hook for managing time log filter states

* fix:coderabbitai

* fix:build

* feat:save multi select filter in localstorage clear on cancel

* fix:coderabbitai
  • Loading branch information
Innocent-Akim authored Nov 14, 2024
1 parent 257cb9e commit 9a94213
Show file tree
Hide file tree
Showing 12 changed files with 128 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import { useOrganizationTeams, useTeamTasks } from "@app/hooks";
import { Button } from "@components/ui/button";
import { statusOptions } from "@app/constants";
import { MultiSelect } from "lib/components/custom-select";
import React, { useEffect } from "react";
import {
Popover,
PopoverContent,
Expand All @@ -11,16 +11,17 @@ import {
import { SettingFilterIcon } from "@/assets/svg";
import { useTranslations } from "next-intl";
import { clsxm } from "@/app/utils";
import { useTimelogFilterOptions } from "@/app/hooks";



export function TimeSheetFilterPopover() {
export const TimeSheetFilterPopover = React.memo(function TimeSheetFilterPopover() {
const [shouldRemoveItems, setShouldRemoveItems] = React.useState(false);
const { activeTeam } = useOrganizationTeams();
const { tasks } = useTeamTasks();
const t = useTranslations();
const { setEmployeeState, setProjectState, setStatusState, setTaskState, employee, project, statusState, task } = useTimelogFilterOptions();

useEffect(() => {
React.useEffect(() => {
if (shouldRemoveItems) {
setShouldRemoveItems(false);
}
Expand All @@ -46,42 +47,45 @@ export function TimeSheetFilterPopover() {
<div className="">
<label className="flex justify-between text-gray-600 mb-1 text-sm">
<span className="text-[12px]">{t('manualTime.EMPLOYEE')}</span>
<span className={clsxm("text-primary/10")}>Clear</span>
<span className={clsxm("text-primary/10", employee?.length > 0 && "text-primary dark:text-primary-light")}>Clear</span>
</label>
<MultiSelect
localStorageKey="timesheet-select-filter-employee"
removeItems={shouldRemoveItems}
items={activeTeam?.members ?? []}
itemToString={(members) => (members ? members.employee.fullName : '')}
itemId={(item) => item.id}
onValueChange={(selectedItems) => console.log(selectedItems)}
onValueChange={(selectedItems) => setEmployeeState(selectedItems as any)}
multiSelect={true}
triggerClassName="dark:border-gray-700"
/>
</div>
<div className="">
<label className="flex justify-between text-gray-600 mb-1 text-sm">
<span className="text-[12px]">{t('sidebar.PROJECTS')}</span>
<span className={clsxm("text-primary/10")}>Clear</span>
<span className={clsxm("text-primary/10", project?.length > 0 && "text-primary dark:text-primary-light")}>Clear</span>
</label>
<MultiSelect
localStorageKey="timesheet-select-filter-projects"
removeItems={shouldRemoveItems}
items={activeTeam?.projects ?? []}
itemToString={(project) => (activeTeam?.projects ? project.name! : '')}
itemId={(item) => item.id}
onValueChange={(selectedItems) => console.log(selectedItems)}
onValueChange={(selectedItems) => setProjectState(selectedItems as any)}
multiSelect={true}
triggerClassName="dark:border-gray-700"
/>
</div>
<div className="">
<label className="flex justify-between text-gray-600 mb-1 text-sm">
<span className="text-[12px]">{t('hotkeys.TASK')}</span>
<span className={clsxm("text-primary/10")}>Clear</span>
<span className={clsxm("text-primary/10", task?.length > 0 && "text-primary dark:text-primary-light")}>Clear</span>
</label>
<MultiSelect
localStorageKey="timesheet-select-filter-task"
removeItems={shouldRemoveItems}
items={tasks}
onValueChange={(task) => task}
onValueChange={(selectedItems) => setTaskState(selectedItems as any)}
itemId={(task) => (task ? task.id : '')}
itemToString={(task) => (task ? task.title : '')}
multiSelect={true}
Expand All @@ -91,14 +95,15 @@ export function TimeSheetFilterPopover() {
<div className="">
<label className="flex justify-between text-gray-600 mb-1 text-sm">
<span className="text-[12px]">{t('common.STATUS')}</span>
<span className={clsxm("text-primary/10")}>Clear</span>
<span className={clsxm("text-primary/10", statusState && "text-primary dark:text-primary-light")}>Clear</span>
</label>
<MultiSelect
localStorageKey="timesheet-select-filter-status"
removeItems={shouldRemoveItems}
items={statusOptions}
itemToString={(status) => (status ? status.value : '')}
itemId={(item) => item.value}
onValueChange={(selectedItems) => console.log(selectedItems)}
onValueChange={(selectedItems) => setStatusState(selectedItems)}
multiSelect={true}
triggerClassName="dark:border-gray-700"
/>
Expand All @@ -121,4 +126,4 @@ export function TimeSheetFilterPopover() {
</Popover>
</>
)
}
})
25 changes: 25 additions & 0 deletions apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores';
import { useAtom } from 'jotai';

export function useTimelogFilterOptions() {
const [employeeState, setEmployeeState] = useAtom(timesheetFilterEmployeeState);
const [projectState, setProjectState] = useAtom(timesheetFilterProjectState);
const [statusState, setStatusState] = useAtom(timesheetFilterStatusState);
const [taskState, setTaskState] = useAtom(timesheetFilterTaskState);

const employee = employeeState;
const project = projectState;
const task = taskState


return {
statusState,
employee,
project,
task,
setEmployeeState,
setProjectState,
setTaskState,
setStatusState
};
}
17 changes: 13 additions & 4 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { useCallback, useEffect } from 'react';
import { getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log';
import moment from 'moment';
import { ITimeSheet } from '@/app/interfaces';
import { useTimelogFilterOptions } from './useTimelogFilterOptions';

interface TimesheetParams {
startDate: Date | string;
endDate: Date | string;
startDate?: Date | string;
endDate?: Date | string;
}

export interface GroupedTimesheet {
Expand Down Expand Up @@ -45,7 +46,7 @@ export function useTimesheet({
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);

const { employee, project } = useTimelogFilterOptions();
const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);

const getTaskTimesheet = useCallback(
Expand All @@ -59,13 +60,21 @@ export function useTimesheet({
organizationId: user.employee?.organizationId,
tenantId: user.tenantId ?? '',
timeZone: user.timeZone?.split('(')[0].trim(),
employeeIds: employee?.map((member) => member.employee.id).filter((id) => id !== undefined),
projectIds: project?.map((project) => project.id).filter((id) => id !== undefined)
}).then((response) => {
setTimesheet(response.data);
}).catch((error) => {
console.error('Error fetching timesheet:', error);
});
},
[user, queryTimesheet, setTimesheet]
[
user,
queryTimesheet,
setTimesheet,
employee,
project
]
);
useEffect(() => {
getTaskTimesheet({ startDate, endDate });
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export * from './features/useTeamTasks';
export * from './features/useTimer';
export * from './features/useUser';
export * from './features/useUserProfilePage';
export * from './features/useTimelogFilterOptions';
export * from './useCollaborative';

//export user personal setting
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/interfaces/timer/ITimerLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ interface Project {
interface Task {
id: string;
title: string;
issueType?: ITaskIssue | null;
estimate: number | null;
taskStatus: string | null;
taskNumber: string;
}

Expand Down Expand Up @@ -71,5 +73,4 @@ export interface ITimeSheet {
employee: Employee;
duration: number;
isEdited: boolean;
issueType?: ITaskIssue;
}
16 changes: 13 additions & 3 deletions apps/web/app/services/client/api/timer/timer-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ export async function getTaskTimesheetLogsApi({
tenantId,
startDate,
endDate,
timeZone
timeZone,
projectIds = [],
employeeIds = []
}: {
organizationId: string,
tenantId: string,
startDate: string | Date,
endDate: string | Date,
timeZone?: string
timeZone?: string,
projectIds?: string[],
employeeIds?: string[]
}) {

if (!organizationId || !tenantId || !startDate || !endDate) {
Expand All @@ -37,7 +41,6 @@ export async function getTaskTimesheetLogsApi({
if (isNaN(new Date(start).getTime()) || isNaN(new Date(end).getTime())) {
throw new Error('Invalid date format provided');
}

const params = new URLSearchParams({
'activityLevel[start]': '0',
'activityLevel[end]': '100',
Expand All @@ -53,6 +56,13 @@ export async function getTaskTimesheetLogsApi({
'relations[4]': 'task.taskStatus'
});

projectIds.forEach((id, index) => {
params.append(`projectIds[${index}]`, id);
});

employeeIds.forEach((id, index) => {
params.append(`employeeIds[${index}]`, id);
});
const endpoint = `/timesheet/time-log?${params.toString()}`;
return get<ITimeSheet[]>(endpoint, { tenantId });
}
1 change: 1 addition & 0 deletions apps/web/app/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './integration-tenant';
export * from './integration-github';
export * from './integration-types';
export * from './integration';
export * from './time-logs'
13 changes: 12 additions & 1 deletion apps/web/app/stores/time-logs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { ITimerLogsDailyReport } from '@app/interfaces/timer/ITimerLogs';
import { atom } from 'jotai';
import { ITimeSheet } from '../interfaces';
import { IProject, ITeamTask, ITimeSheet, OT_Member } from '../interfaces';

interface IFilterOption {
value: string;
label: string;
}

export const timerLogsDailyReportState = atom<ITimerLogsDailyReport[]>([]);

export const timesheetRapportState = atom<ITimeSheet[]>([])

export const timesheetFilterEmployeeState = atom<OT_Member[]>([]);
export const timesheetFilterProjectState = atom<IProject[]>([]);
export const timesheetFilterTaskState = atom<ITeamTask[]>([]);

export const timesheetFilterStatusState = atom<IFilterOption | IFilterOption[] | null>([]);
63 changes: 40 additions & 23 deletions apps/web/lib/components/custom-select/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ interface MultiSelectProps<T> {
renderItem?: (item: T, onClick: () => void, isSelected: boolean) => JSX.Element;
defaultValue?: T | T[];
multiSelect?: boolean;
removeItems?: boolean
removeItems?: boolean;
localStorageKey?: string;
}

export function MultiSelect<T>({
Expand All @@ -29,13 +30,49 @@ export function MultiSelect<T>({
renderItem,
defaultValue,
multiSelect = false,
removeItems
removeItems,
localStorageKey = "select-items-selected",
}: MultiSelectProps<T>) {
const [selectedItems, setSelectedItems] = useState<T[]>(Array.isArray(defaultValue) ? defaultValue : defaultValue ? [defaultValue] : []);
const [selectedItems, setSelectedItems] = useState<T[]>(() => {
if (typeof window === 'undefined') return [];
try {
const saved = localStorage.getItem(localStorageKey);
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
});
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [popoverWidth, setPopoverWidth] = useState<number | null>(null);
const triggerRef = useRef<HTMLButtonElement>(null);

// Load selected items from localStorage on component mount
useEffect(() => {
if (defaultValue) {
const initialItems = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
setSelectedItems(initialItems);
}
}, [defaultValue, setSelectedItems]);

useEffect(() => {
if (onValueChange) {
onValueChange(multiSelect ? selectedItems : selectedItems[0] || null);
}
}, [selectedItems, multiSelect, onValueChange]);

// Save selected items to localStorage whenever they change
// Handle persistence
useEffect(() => {
if (typeof window !== 'undefined') {
try {
localStorage.setItem(localStorageKey, JSON.stringify(selectedItems));
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedItems, localStorageKey]);

const onClick = (item: T) => {
let newSelectedItems: T[];
if (multiSelect) {
Expand All @@ -49,25 +86,15 @@ export function MultiSelect<T>({
setPopoverOpen(false);
}
setSelectedItems(newSelectedItems);
if (onValueChange) {
onValueChange(multiSelect ? newSelectedItems : newSelectedItems[0]);
}
};


const removeItem = (item: T) => {
const newSelectedItems = selectedItems.filter((selectedItem) => itemId(selectedItem) !== itemId(item));
setSelectedItems(newSelectedItems);
if (onValueChange) {
onValueChange(multiSelect ? newSelectedItems : newSelectedItems.length > 0 ? newSelectedItems[0] : null);
}
};

const removeAllItems = () => {
setSelectedItems([]);
if (onValueChange) {
onValueChange(null);
}
};

useEffect(() => {
Expand All @@ -83,16 +110,6 @@ export function MultiSelect<T>({
}, [removeItems, removeAllItems]) // deepscan-disable-line


useEffect(() => {
const initialItems = Array.isArray(defaultValue) ? defaultValue : defaultValue ? [defaultValue] : [];
setSelectedItems(initialItems);
if (onValueChange) {
onValueChange(multiSelect ? initialItems : initialItems[0] || null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValue]);


useEffect(() => {
if (triggerRef.current) {
setPopoverWidth(triggerRef.current.offsetWidth);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
<span className="text-[#868688]">24:30h</span>
</Badge>
</div>
<div className="flex items-center gap-2 p-x-1">
<div className={clsxm("flex items-center gap-2 p-x-1")}>
{getTimesheetButtons(status as StatusType, t, handleButtonClick)}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/features/task/task-block-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default function TaskBlockCard(props: TaskItemProps) {
previousValue + currentValue.duration,
0
)) ||
0
0
);

return (
Expand Down
Loading

0 comments on commit 9a94213

Please sign in to comment.