Skip to content

Commit

Permalink
feat: manage permissions and improve time entries view
Browse files Browse the repository at this point in the history
  • Loading branch information
Innocent-Akim committed Dec 7, 2024
1 parent 88fc30c commit 50a1e98
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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 {
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 @@ -35,15 +39,20 @@ export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, fi
</div>

<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 && (
<>
<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>
</>
)
}
</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>
);
}
21 changes: 12 additions & 9 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { GoSearch } from 'react-icons/go';

import { getGreeting } 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 @@ -91,7 +92,6 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb


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

const paramsUrl = useParams<{ locale: string }>();
const currentLocale = paramsUrl ? paramsUrl.locale : null;
Expand Down Expand Up @@ -150,13 +150,13 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
icon={<Clock className="font-bold" />}
classNameIcon="bg-[#3D5A80] shadow-[#3d5a809c] "
/>
<TimesheetCard
{isManage && (<TimesheetCard
count={8}
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 +190,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 +215,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 @@ -16,6 +18,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 @@ -64,6 +75,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, selectTimesheet: logIds, timesheetGroupByDays, puTimesheetStatus } = useTimelogFilterOptions();
const { employee, project, task, statusState, selectTimesheet: logIds, 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
};
}
43 changes: 28 additions & 15 deletions apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import { useTranslations } from 'next-intl';
import { formatDate } from '@/app/helpers';
import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet';
import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays';
import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { IUser, TimesheetLog, TimesheetStatus } from '@/app/interfaces';

export const columns: ColumnDef<TimeSheet>[] = [
{
Expand Down Expand Up @@ -154,12 +154,11 @@ export const columns: ColumnDef<TimeSheet>[] = [
}
];

export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[], user?: IUser | undefined }) {
const { isOpen, openModal, closeModal } = useModal();


const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet, updateTimesheetStatus } = useTimesheet({});
const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet, timesheetGroupByDays, handleSelectRowByStatusAndDate } = useTimelogFilterOptions();
const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet, timesheetGroupByDays, handleSelectRowByStatusAndDate, isUserAllowedToAccess } = useTimelogFilterOptions();
const isManage = isUserAllowedToAccess(user);

const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const handleConfirm = () => {
Expand Down Expand Up @@ -300,7 +299,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
</Badge>
</div>
<div className={clsxm('flex items-center gap-2 p-x-1 capitalize')}>
{getTimesheetButtons(status as StatusType, t, true, handleButtonClick)}
{isManage && getTimesheetButtons(status as StatusType, t, true, handleButtonClick)}
</div>
</div>
</AccordionTrigger>
Expand Down Expand Up @@ -362,8 +361,12 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
</div>
<DisplayTimeForTimesheet
duration={task.timesheet.duration}
logType={task.logType}
/>
<TaskActionMenu dataTimesheet={task} />
<TaskActionMenu
dataTimesheet={task}
isManage={isManage}
user={user} />
</div>
))}
</AccordionContent>
Expand Down Expand Up @@ -466,9 +469,11 @@ export function SelectFilter({ selectedStatus }: { selectedStatus?: string }) {
);
}

const TaskActionMenu = ({ dataTimesheet }: { dataTimesheet: TimesheetLog }) => {
const TaskActionMenu = ({ dataTimesheet, isManage, user }: { dataTimesheet: TimesheetLog, isManage?: boolean, user?: IUser | undefined }) => {
const { isOpen: isEditTask, openModal: isOpenModalEditTask, closeModal: isCloseModalEditTask } = useModal();

const t = useTranslations();
const canEdit = isManage || user?.id === dataTimesheet.employee.user.id;

return (
<>
Expand All @@ -485,14 +490,22 @@ const TaskActionMenu = ({ dataTimesheet }: { dataTimesheet: TimesheetLog }) => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="cursor-pointer" onClick={isOpenModalEditTask}>
{t('common.EDIT')}
</DropdownMenuItem>
{canEdit && (
<DropdownMenuItem className="cursor-pointer" onClick={isOpenModalEditTask}>
{t('common.EDIT')}
</DropdownMenuItem>
)
}
<DropdownMenuSeparator />
<StatusTask timesheet={dataTimesheet} />
<DropdownMenuItem className="text-red-600 hover:!text-red-600 cursor-pointer">
{t('common.DELETE')}
</DropdownMenuItem>
{isManage && (
<>
<StatusTask timesheet={dataTimesheet} />
<DropdownMenuItem className="text-red-600 hover:!text-red-600 cursor-pointer">
{t('common.DELETE')}
</DropdownMenuItem>
</>
)
}
</DropdownMenuContent>
</DropdownMenu>
</>
Expand Down
Loading

0 comments on commit 50a1e98

Please sign in to comment.