From 50a1e981cfbe66f7e5f6bd3a1e845e8f0122bb23 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Sat, 7 Dec 2024 09:55:03 +0200 Subject: [PATCH] feat: manage permissions and improve time entries view --- .../[memberId]/components/CalendarView.tsx | 4 +- .../[memberId]/components/TimesheetFilter.tsx | 31 ++++++++----- .../components/TimesheetFilterDate.tsx | 13 ++++-- .../[memberId]/components/TimesheetView.tsx | 5 ++- .../[locale]/timesheet/[memberId]/page.tsx | 21 +++++---- .../hooks/features/useTimelogFilterOptions.ts | 14 +++++- apps/web/app/hooks/features/useTimesheet.ts | 14 +++--- .../calendar/table-time-sheet.tsx | 43 ++++++++++++------- apps/web/lib/features/task/task-displays.tsx | 20 ++++++--- apps/web/lib/settings/member-table.tsx | 2 +- 10 files changed, 113 insertions(+), 54 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx index 360aced88e..2b94ede43a 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx @@ -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 = [ diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilter.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilter.tsx index 71d9c6fb0c..11831e90ef 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilter.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilter.tsx @@ -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, @@ -14,10 +15,13 @@ interface ITimesheetFilter { onChangeStatus?: (status: FilterStatus) => void; filterStatus?: FilterStatus, data?: Record + 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 ( <> { @@ -35,15 +39,20 @@ export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, fi
- - - - + {isManage && ( + <> + + + + + + ) + }
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx index 5c50e0631f..83e1f8a91f 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetFilterDate.tsx @@ -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; @@ -138,7 +140,7 @@ export function TimesheetFilterDate({
))} diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx index 18606f7199..be0640fdfb 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx @@ -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) { @@ -26,7 +27,7 @@ export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; lo return (
- +
); } diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index ade889e9bf..a8adbfc947 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -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'; @@ -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(''); const [filterStatus, setFilterStatus] = useLocalStorageState('timesheet-filter-status', 'All Tasks'); const [timesheetNavigator, setTimesheetNavigator] = useLocalStorageState( @@ -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 }); @@ -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; @@ -150,13 +150,13 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb icon={} classNameIcon="bg-[#3D5A80] shadow-[#3d5a809c] " /> - } classNameIcon="bg-[#30B366] shadow-[#30b3678f]" - /> + />)}
@@ -190,6 +190,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
{timesheetNavigator === 'ListView' ? ( ) : ( diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index a79fcb1ad3..8dea19f449 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -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); @@ -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) => { @@ -64,6 +75,7 @@ export function useTimelogFilterOptions() { setTimesheetGroupByDays, generateTimeOptions, setPuTimesheetStatus, - puTimesheetStatus + puTimesheetStatus, + isUserAllowedToAccess }; } diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 9add0836d5..ed41259914 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -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) @@ -297,6 +300,7 @@ export function useTimesheet({ loadingCreateTimesheet, updateTimesheet, loadingUpdateTimesheet, - groupByDate + groupByDate, + isManage }; } diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index 117d90d436..081977e112 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -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[] = [ { @@ -154,12 +154,11 @@ export const columns: ColumnDef[] = [ } ]; -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 = () => { @@ -300,7 +299,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
- {getTimesheetButtons(status as StatusType, t, true, handleButtonClick)} + {isManage && getTimesheetButtons(status as StatusType, t, true, handleButtonClick)}
@@ -362,8 +361,12 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { - + ))} @@ -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 ( <> @@ -485,14 +490,22 @@ const TaskActionMenu = ({ dataTimesheet }: { dataTimesheet: TimesheetLog }) => { - - {t('common.EDIT')} - + {canEdit && ( + + {t('common.EDIT')} + + ) + } - - - {t('common.DELETE')} - + {isManage && ( + <> + + + {t('common.DELETE')} + + + ) + } diff --git a/apps/web/lib/features/task/task-displays.tsx b/apps/web/lib/features/task/task-displays.tsx index 16fcf816ac..c39f6da589 100644 --- a/apps/web/lib/features/task/task-displays.tsx +++ b/apps/web/lib/features/task/task-displays.tsx @@ -5,6 +5,7 @@ import { TaskIssueStatus } from './task-issue'; import { formatDate, secondsToTime } from '@/app/helpers'; import { ClockIcon } from "@radix-ui/react-icons" import React from 'react'; +import { CalendarArrowDown, CalendarClock, UserPlusIcon } from 'lucide-react'; type Props = { task: Nullable; @@ -73,23 +74,32 @@ const formatTime = (hours: number, minutes: number) => ( ); -export const DisplayTimeForTimesheet = ({ duration }: { duration: number }) => { +export const DisplayTimeForTimesheet = ({ duration, logType }: { duration: number, logType?: 'TRACKED' | 'MANUAL' | 'IDLE' | undefined }) => { if (duration < 0) { console.warn('Negative duration provided to DisplayTimeForTimesheet'); duration = 0; } const { h: hours, m: minute } = secondsToTime(duration || 0); + + const iconClasses = 'text-[14px] h-4 w-4'; + const icons = { + MANUAL: , + TRACKED: , + IDLE: , + }; + const resolvedLogType: keyof typeof icons = logType ?? 'TRACKED'; return ( -
- -
+
+ {icons[resolvedLogType]} +
{formatTime(hours, minute)}
- ) + ); } + export const TotalTimeDisplay = React.memo(({ timesheetLog }: { timesheetLog: TimesheetLog[] }) => { const totalDuration = Array.isArray(timesheetLog) ? timesheetLog.reduce((acc, curr) => acc + (curr.timesheet?.duration || 0), 0) diff --git a/apps/web/lib/settings/member-table.tsx b/apps/web/lib/settings/member-table.tsx index 497148d72d..f186d14c73 100644 --- a/apps/web/lib/settings/member-table.tsx +++ b/apps/web/lib/settings/member-table.tsx @@ -21,7 +21,7 @@ export const MemberTable = ({ members }: { members: OT_Member[] }) => { const t = useTranslations(); const { total, onPageChange, itemsPerPage, itemOffset, endOffset, setItemsPerPage, currentItems } = usePagination(members); - const { activeTeam, updateOrganizationTeam,} = useOrganizationTeams(); + const { activeTeam, updateOrganizationTeam, } = useOrganizationTeams(); const { updateAvatar } = useSettings(); const activeTeamRef = useSyncRef(activeTeam);