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]: Manage Permissions and Improve Time entries view #3403

Merged
merged 7 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 }) {
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
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!,
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
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
Loading