Skip to content

Commit

Permalink
[Feat]: Manage Permissions and Improve Time entries view (#3403)
Browse files Browse the repository at this point in the history
* fix: Kanban | Cards are broken when toogle FullWidth Mode

* fix: cspell

* feat: manage permissions and improve time entries view

* fix:coderabbitai

* feat(timesheet): calculate and display total duration

* improve:Enhanced user permissions management in the Timesheet components

* improve: update status style for tasks in timesheet
  • Loading branch information
Innocent-Akim authored Dec 8, 2024
1 parent 7cf00f0 commit ca52c4d
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 98 deletions.
18 changes: 11 additions & 7 deletions apps/web/app/[locale]/kanban/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useAtomValue } from 'jotai';
import { fullWidthState } from '@app/stores/fullWidth';
import { CircleIcon } from 'lucide-react';
import { XMarkIcon } from '@heroicons/react/20/solid';
import { cn } from '@/lib/utils';

const Kanban = () => {
const {
Expand Down Expand Up @@ -161,11 +162,10 @@ const Kanban = () => {
<div
key={tab.name}
onClick={() => setActiveTab(tab.value)}
className={`cursor-pointer pt-2.5 px-5 pb-[30px] text-base font-semibold ${
activeTab === tab.value
? 'border-b-[#3826A6] text-[#3826A6] dark:text-white dark:border-b-white'
: 'border-b-white dark:border-b-[#191A20] dark:text-white text-[#282048]'
}`}
className={`cursor-pointer pt-2.5 px-5 pb-[30px] text-base font-semibold ${activeTab === tab.value
? 'border-b-[#3826A6] text-[#3826A6] dark:text-white dark:border-b-white'
: 'border-b-white dark:border-b-[#191A20] dark:text-white text-[#282048]'
}`}
style={{
borderBottomWidth: '3px',
borderBottomStyle: 'solid'
Expand Down Expand Up @@ -259,18 +259,22 @@ const Kanban = () => {
}
>
{/** TODO:fetch teamtask based on days */}


<div className="pt-10">
{activeTab &&
(Object.keys(data).length > 0 ? (
<KanbanView isLoading={isLoading} kanbanBoardTasks={data} />
<Container fullWidth={fullWidth} className={cn("!pt-0 px-5")}>
<KanbanView isLoading={isLoading} kanbanBoardTasks={data} />
</Container>
) : (
// add filter for today, yesterday and tomorrow
<div className="flex flex-col flex-1 w-full h-full">
<KanbanBoardSkeleton />
</div>
))}
</div>
</MainLayout>
</MainLayout >
<InviteFormModal open={isOpen && !!user?.isEmailVerified} closeModal={closeModal} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
{...taskIssues[option as ITaskIssue]}
showIssueLabels={false}
issueType="issue"
className={clsxm('rounded-md px-2 text-white bg-primary')}
className={clsxm('rounded-sm h-auto !px-[0.3312rem] py-[0.2875rem] text-white bg-primary')}
/>
{option}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import { cn } from "@/lib/utils";
import MonthlyTimesheetCalendar from "./MonthlyTimesheetCalendar";
import { useTimelogFilterOptions } from "@/app/hooks";
import WeeklyTimesheetCalendar from "./WeeklyTimesheetCalendar";
import { IUser } from "@/app/interfaces";
interface BaseCalendarDataViewProps {
t: TranslationHooks
data: GroupedTimesheet[];
daysLabels?: string[];
CalendarComponent: typeof MonthlyTimesheetCalendar | typeof WeeklyTimesheetCalendar;
user?: IUser | undefined
}

export function CalendarView({ data, loading }: { data?: GroupedTimesheet[], loading: boolean }) {
export function CalendarView({ data, loading, user }: { data?: GroupedTimesheet[], loading: boolean, user?: IUser | undefined }) {
const t = useTranslations();
const { timesheetGroupByDays } = useTimelogFilterOptions();
const defaultDaysLabels = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { FrequencySelect, TimeSheetFilterPopover, TimesheetFilterDate, Timesheet
import { Button } from 'lib/components';
import { TranslationHooks } from 'next-intl';
import { AddTaskModal } from './AddTaskModal';
import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { IUser, TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { useTimelogFilterOptions } from '@/app/hooks';

interface ITimesheetFilter {
isOpen: boolean,
Expand All @@ -14,10 +15,13 @@ interface ITimesheetFilter {
onChangeStatus?: (status: FilterStatus) => void;
filterStatus?: FilterStatus,
data?: Record<TimesheetStatus, TimesheetLog[]>
user?: IUser | undefined

}

export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, filterStatus, onChangeStatus, data }: ITimesheetFilter,) {
export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, filterStatus, onChangeStatus, data, user }: ITimesheetFilter,) {
const { isUserAllowedToAccess } = useTimelogFilterOptions();
const isManage = isUserAllowedToAccess(user);
return (
<>
{
Expand All @@ -37,13 +41,18 @@ export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, fi
<div className="flex gap-2">
<FrequencySelect />
<TimesheetFilterDate t={t} {...initDate} />
<TimeSheetFilterPopover />
<Button
onClick={openModal}
variant="outline"
className="bg-primary/5 dark:bg-primary-light dark:border-transparent !h-[2.2rem] font-medium">
{t('common.ADD_TIME')}
</Button>
{isManage && (
<>
<TimeSheetFilterPopover />
<Button
onClick={openModal}
variant="outline"
className="bg-primary/5 dark:bg-primary-light dark:border-transparent !h-[2.2rem] font-medium">
{t('common.ADD_TIME')}
</Button>
</>
)
}
</div>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { MdKeyboardArrowRight } from 'react-icons/md';
import { PiCalendarDotsThin } from 'react-icons/pi';
import React, { Dispatch, useEffect, useState, SetStateAction, useCallback, useMemo, memo } from 'react';
import moment from 'moment';
import { ChevronDown } from 'lucide-react';


interface DatePickerInputProps {
date: Date | null;
Expand Down Expand Up @@ -138,7 +140,7 @@ export function TimesheetFilterDate({
<div className="flex items-end justify-end w-full">
<Button
variant={'outline'}
className={actionButtonClass}
className={cn(actionButtonClass, 'hover:text-primary')}
onClick={() => {
setDateRange(initialRange ?? { from: new Date(), to: new Date() });
setIsVisible(false);
Expand All @@ -148,7 +150,7 @@ export function TimesheetFilterDate({
</Button>
<Button
variant={'outline'}
className={actionButtonClass}
className={cn(actionButtonClass, 'hover:text-primary')}
onClick={() => {
onChange?.(dateRange);
setIsVisible(false);
Expand All @@ -172,14 +174,17 @@ export function TimesheetFilterDate({
key={index}
variant="outline"
className={clsxm(
'h-6 flex items-center justify-between border-none text-[12px] text-gray-700 dark:bg-dark--theme-light hover:bg-primary hover:text-white hover:dark:bg-primary-light'
'h-7 group flex items-center justify-between border-none text-[13px] text-gray-700 dark:bg-dark--theme-light hover:bg-primary hover:text-white hover:dark:bg-primary-light'
)}
onClick={() => {
label === t('common.FILTER_CUSTOM_RANGE') && setIsVisible((prev) => !prev);
handlePresetClick(label);
}}
>
<span> {label}</span>
<div className='flex items-center gap-x-2'>
<ChevronDown />
<span> {label}</span>
</div>
{label === t('common.FILTER_CUSTOM_RANGE') && <MdKeyboardArrowRight />}
</Button>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { GroupedTimesheet } from '@/app/hooks/features/useTimesheet';
import { IUser } from '@/app/interfaces';
import TimesheetSkeleton from '@components/shared/skeleton/TimesheetSkeleton';
import { DataTableTimeSheet } from 'lib/features/integrations/calendar';
import { useTranslations } from 'next-intl';

export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; loading?: boolean }) {
export function TimesheetView({ data, loading, user }: { data?: GroupedTimesheet[]; loading?: boolean, user?: IUser | undefined }) {
const t = useTranslations();

if (loading || !data) {
Expand All @@ -26,7 +27,7 @@ export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; lo

return (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">
<DataTableTimeSheet data={data} />
<DataTableTimeSheet data={data} user={user} />
</div>
);
}
37 changes: 24 additions & 13 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import { CalendarDaysIcon, Clock, User2 } from 'lucide-react';
import { GrTask } from 'react-icons/gr';
import { GoSearch } from 'react-icons/go';

import { getGreeting } from '@/app/helpers';
import { getGreeting, secondsToTime } from '@/app/helpers';
import { useTimesheet } from '@/app/hooks/features/useTimesheet';
import { endOfDay, startOfDay } from 'date-fns';
import { startOfWeek, endOfWeek } from 'date-fns';
import TimesheetDetailModal from './components/TimesheetDetailModal';

type TimesheetViewMode = 'ListView' | 'CalendarView';
Expand All @@ -37,6 +37,7 @@ type ViewToggleButtonProps = {
const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memberId: string } }) {
const t = useTranslations();
const { user } = useAuthenticateUser();
const { isTrackingEnabled, activeTeam } = useOrganizationTeams();
const [search, setSearch] = useState<string>('');
const [filterStatus, setFilterStatus] = useLocalStorageState<FilterStatus>('timesheet-filter-status', 'All Tasks');
const [timesheetNavigator, setTimesheetNavigator] = useLocalStorageState<TimesheetViewMode>(
Expand All @@ -45,12 +46,12 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
);

const [dateRange, setDateRange] = React.useState<{ from: Date | null; to: Date | null }>({
from: startOfDay(new Date()),
to: endOfDay(new Date())
from: startOfWeek(new Date(), { weekStartsOn: 1 }),
to: endOfWeek(new Date(), { weekStartsOn: 1 }),
});
const { timesheet, statusTimesheet, loadingTimesheet } = useTimesheet({
startDate: dateRange.from ?? '',
endDate: dateRange.to ?? '',
const { timesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({
startDate: dateRange.from!,
endDate: dateRange.to!,
timesheetViewMode: timesheetNavigator
});

Expand Down Expand Up @@ -88,10 +89,13 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb

const username = user?.name || user?.firstName || user?.lastName || user?.username;


const totalDuration = Object.values(statusTimesheet)
.flat()
.map(entry => entry.timesheet.duration)
.reduce((total, current) => total + current, 0);
const { h: hours, m: minute } = secondsToTime(totalDuration || 0);

const fullWidth = useAtomValue(fullWidthState);
const { isTrackingEnabled, activeTeam } = useOrganizationTeams();

const paramsUrl = useParams<{ locale: string }>();
const currentLocale = paramsUrl ? paramsUrl.locale : null;
Expand Down Expand Up @@ -144,19 +148,23 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
onClick={() => openTimesheetDetail()}
/>
<TimesheetCard
hours="63:00h"
hours={`${hours}:${minute}`}
title="Men Hours"
date={`${moment(dateRange.from).format('YYYY-MM-DD')} - ${moment(dateRange.to).format('YYYY-MM-DD')}`}
icon={<Clock className="font-bold" />}
classNameIcon="bg-[#3D5A80] shadow-[#3d5a809c] "
/>
<TimesheetCard
count={8}
{isManage && (<TimesheetCard
count={Object.values(statusTimesheet)
.flat()
.map(entry => entry.employee.id)
.filter((id, index, array) => array.indexOf(id) === index)
.length}
title="Members Worked"
description="People worked since last time"
icon={<User2 className="font-bold" />}
classNameIcon="bg-[#30B366] shadow-[#30b3678f]"
/>
/>)}
</div>
<div className="flex justify-between w-full overflow-hidden">
<div className="flex w-full">
Expand Down Expand Up @@ -190,6 +198,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
</div>
</div>
<TimesheetFilter
user={user}
data={statusTimesheet}
onChangeStatus={setFilterStatus}
filterStatus={filterStatus}
Expand All @@ -214,11 +223,13 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
<div className="border border-gray-200 rounded-lg dark:border-gray-800">
{timesheetNavigator === 'ListView' ? (
<TimesheetView
user={user}
data={filterDataTimesheet}
loading={loadingTimesheet}
/>
) : (
<CalendarView
user={user}
data={filterDataTimesheet}
loading={loadingTimesheet}
/>
Expand Down
14 changes: 13 additions & 1 deletion apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { IUser, RoleNameEnum } from '@/app/interfaces';
import { timesheetDeleteState, timesheetGroupByDayState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState, timesheetUpdateStatus } from '@/app/stores';
import { useAtom } from 'jotai';
import React from 'react';

export function useTimelogFilterOptions() {

const [employeeState, setEmployeeState] = useAtom(timesheetFilterEmployeeState);
const [projectState, setProjectState] = useAtom(timesheetFilterProjectState);
const [statusState, setStatusState] = useAtom(timesheetFilterStatusState);
Expand All @@ -17,6 +19,15 @@ export function useTimelogFilterOptions() {
const project = projectState;
const task = taskState

const isUserAllowedToAccess = (user: IUser | null | undefined): boolean => {
const allowedRoles: RoleNameEnum[] = [
RoleNameEnum.SUPER_ADMIN,
RoleNameEnum.MANAGER,
RoleNameEnum.ADMIN,
];
return user?.role.name ? allowedRoles.includes(user.role.name as RoleNameEnum) : false;
};

const generateTimeOptions = (interval = 15) => {
const totalSlots = (24 * 60) / interval; // Total intervals in a day
return Array.from({ length: totalSlots }, (_, i) => {
Expand Down Expand Up @@ -67,6 +78,7 @@ export function useTimelogFilterOptions() {
setTimesheetGroupByDays,
generateTimeOptions,
setPuTimesheetStatus,
puTimesheetStatus
puTimesheetStatus,
isUserAllowedToAccess
};
}
14 changes: 9 additions & 5 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,26 +95,29 @@ export function useTimesheet({
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus } = useTimelogFilterOptions();
const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess } = useTimelogFilterOptions();
const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi);
const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi)
const { loading: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi);
const { loading: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(updateTimesheetFromAPi);

const isManage = user && isUserAllowedToAccess(user);

const getTaskTimesheet = useCallback(
({ startDate, endDate }: TimesheetParams) => {
if (!user) return;

const from = moment(startDate).format('YYYY-MM-DD');
const to = moment(endDate).format('YYYY-MM-DD')
const to = moment(endDate).format('YYYY-MM-DD');
queryTimesheet({
startDate: from,
endDate: to,
organizationId: user.employee?.organizationId,
tenantId: user.tenantId ?? '',
timeZone: user.timeZone?.split('(')[0].trim(),
employeeIds: employee?.map((member) => member.employee.id).filter((id) => id !== undefined),
employeeIds: isManage
? employee?.map(({ employee: { id } }) => id).filter(Boolean)
: [user.employee.id],
projectIds: project?.map((project) => project.id).filter((id) => id !== undefined),
taskIds: task?.map((task) => task.id).filter((id) => id !== undefined),
status: statusState?.map((status) => status.value).filter((value) => value !== undefined)
Expand Down Expand Up @@ -297,6 +300,7 @@ export function useTimesheet({
loadingCreateTimesheet,
updateTimesheet,
loadingUpdateTimesheet,
groupByDate
groupByDate,
isManage
};
}
Loading

0 comments on commit ca52c4d

Please sign in to comment.