Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: Add Timelog FilterOptions #3333

Merged
merged 5 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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)}
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
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;
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
}

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>([]);
55 changes: 32 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,41 @@ 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[]>(
JSON.parse(typeof window !== 'undefined'
&& window.localStorage.getItem(localStorageKey) as any) || []
);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
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(() => {
const savedItems = localStorage.getItem(localStorageKey);
if (savedItems) {
setSelectedItems(typeof window !== 'undefined' && JSON.parse(savedItems));
} else if (defaultValue) {
const initialItems = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
setSelectedItems(initialItems);
if (onValueChange) onValueChange(multiSelect ? initialItems : initialItems[0]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

// Save selected items to localStorage whenever they change
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem(localStorageKey, JSON.stringify(selectedItems));
if (onValueChange) {
onValueChange(multiSelect ? selectedItems : selectedItems[0] || null);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedItems]);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

const onClick = (item: T) => {
let newSelectedItems: T[];
if (multiSelect) {
Expand All @@ -49,25 +78,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 +102,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
2 changes: 1 addition & 1 deletion apps/web/lib/features/task/task-issue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export function TaskIssueStatusTimesheet({
}: { task: Nullable<ITimeSheet>; showIssueLabels?: boolean } & IClassName) {
return (
<TaskStatus
{...taskIssues[task?.issueType || 'Task']}
{...taskIssues[task?.task.issueType || 'Task']}
showIssueLabels={showIssueLabels}
issueType="issue"
className={clsxm('rounded-md px-2 text-white', className)}
Expand Down
Loading