Skip to content

Commit

Permalink
[Feat]:Calendar timesheet selection (#3498)
Browse files Browse the repository at this point in the history
* feat: add SelectedTimesheet component for managing task selection in calendar view timesheet

* feat: add SelectedTimesheet component for managing task selection in calendar view timesheet
  • Loading branch information
Innocent-Akim authored Jan 6, 2025
1 parent bfb5c25 commit 04ecaf8
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { cn } from "@/lib/utils";
import MonthlyTimesheetCalendar from "./MonthlyTimesheetCalendar";
import { useTimelogFilterOptions } from "@/app/hooks";
import WeeklyTimesheetCalendar from "./WeeklyTimesheetCalendar";
import { IUser } from "@/app/interfaces";
import { IUser, TimesheetLog } from "@/app/interfaces";
import TimesheetSkeleton from "@components/shared/skeleton/TimesheetSkeleton";
import { Checkbox } from "@components/ui/checkbox";
interface BaseCalendarDataViewProps {
t: TranslationHooks
data: GroupedTimesheet[];
Expand Down Expand Up @@ -51,7 +52,6 @@ export function CalendarView({ data, loading, user }: { data?: GroupedTimesheet[
</div>
);
}

return (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">
{(() => {
Expand All @@ -70,7 +70,7 @@ export function CalendarView({ data, loading, user }: { data?: GroupedTimesheet[
}

const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: TranslationHooks }) => {
const { getStatusTimesheet } = useTimesheet({});
const { getStatusTimesheet, handleSelectRowTimesheet, selectTimesheetId } = useTimesheet({});

return (
<div className="w-full dark:bg-dark--theme">
Expand Down Expand Up @@ -134,9 +134,9 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati

}}
className={cn(
'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 ',
'group/item border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4',
)}>
<div className="flex px-3 justify-between items-center w-full">
<div className="flex pl-3 justify-between items-center w-full">
<div className="flex items-center gap-x-1">
<EmployeeAvatar
imageUrl={task.employee.user.imageUrl ?? ''}
Expand All @@ -146,7 +146,6 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
</div>
<DisplayTimeForTimesheet
timesheetLog={task}

/>
</div>
<TaskNameInfoDisplay
Expand All @@ -161,16 +160,23 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
dash
taskNumberClassName="text-sm"
/>
<div className="flex flex-row items-center py-0 gap-2 flex-none order-2 self-stretch flex-grow-0">
{task.project?.imageUrl && (
<ProjectLogo
className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]"
imageUrl={task.project.imageUrl}
/>
)}
<span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1">
{task.project?.name ?? 'No Project'}
</span>
<div className="flex pr-3 justify-between items-center w-full">
<div className="flex flex-row items-center py-0 gap-2 flex-none self-stretch flex-grow-0">
{task.project?.imageUrl && (
<ProjectLogo
className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]"
imageUrl={task.project.imageUrl}
/>
)}
<span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1">
{task.project?.name ?? 'No Project'}
</span>
</div>
<CheckBoxTimesheet
handleSelectRowTimesheet={handleSelectRowTimesheet}
timesheet={task}
selectTimesheetId={selectTimesheetId}
/>
</div>
</div>
))}
Expand All @@ -188,7 +194,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
}

const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCalendarDataViewProps) => {
const { getStatusTimesheet } = useTimesheet({});
const { getStatusTimesheet, handleSelectRowTimesheet, selectTimesheetId } = useTimesheet({});
return (
<CalendarComponent
t={t}
Expand Down Expand Up @@ -236,7 +242,7 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa

}}
className={cn(
'border-l-4 rounded-l space-x-4 box-border flex flex-col items-start py-2.5 gap-2 w-[258px] rounded-tr-md rounded-br-md flex-none order-1 self-stretch flex-grow',
'group/item border-l-4 rounded-l space-x-4 box-border flex flex-col items-start py-2.5 gap-2 w-[258px] rounded-tr-md rounded-br-md flex-none order-1 self-stretch flex-grow',
)}>
<div className="flex pl-3 justify-between items-center w-full">
<div className="flex items-center gap-x-1">
Expand All @@ -263,16 +269,23 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa
dash
taskNumberClassName="text-sm"
/>
<div className="flex flex-row items-center py-0 gap-2 flex-none order-2 self-stretch flex-grow-0">
{task.project?.imageUrl && (
<ProjectLogo
className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]"
imageUrl={task.project.imageUrl}
/>
)}
<span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1">
{task.project?.name ?? 'No Project'}
</span>
<div className="flex justify-between items-center w-full">
<div className="flex flex-row items-center py-0 gap-2 flex-none self-stretch flex-grow-0">
{task.project?.imageUrl && (
<ProjectLogo
className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]"
imageUrl={task.project.imageUrl}
/>
)}
<span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1">
{task.project?.name ?? 'No Project'}
</span>
</div>
<CheckBoxTimesheet
handleSelectRowTimesheet={handleSelectRowTimesheet}
timesheet={task}
selectTimesheetId={selectTimesheetId}
/>
</div>
</div>
))}
Expand All @@ -299,3 +312,14 @@ const MonthlyCalendarDataView = (props: { data: GroupedTimesheet[], t: Translati
const WeeklyCalendarDataView = (props: { data: GroupedTimesheet[], t: TranslationHooks, daysLabels?: string[] }) => (
<BaseCalendarDataView {...props} CalendarComponent={WeeklyTimesheetCalendar} />
);


export const CheckBoxTimesheet = ({ selectTimesheetId, timesheet, handleSelectRowTimesheet }: { selectTimesheetId: TimesheetLog[], timesheet: TimesheetLog, handleSelectRowTimesheet: (items: TimesheetLog) => void }) => {
return <Checkbox
className={cn(
"group/edit invisible w-5 h-5 select-auto group-hover/item:visible",
selectTimesheetId.includes(timesheet) && 'visible')}
onCheckedChange={() => handleSelectRowTimesheet(timesheet)}
checked={selectTimesheetId.includes(timesheet)}
/>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ID, TimesheetLog, TimesheetStatus } from "@/app/interfaces";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
import { useCallback } from "react";

type ActionButtonProps = {
label: string;
Expand Down Expand Up @@ -70,3 +72,77 @@ export const SelectionBar = ({
</div>
)
}


interface SelectedTimesheetProps {
selectTimesheetId: TimesheetLog[];
updateTimesheetStatus: ({ status, ids }: { status: TimesheetStatus, ids: ID[] | ID }) => Promise<void>;
deleteTaskTimesheet: ({ logIds }: { logIds: string[] }) => Promise<void>;
setSelectTimesheetId: React.Dispatch<React.SetStateAction<TimesheetLog[]>>;
fullWidth: boolean;
}


/**
* SelectedTimesheet
*
* A component that renders a selection bar to handle tasks in the timesheet.
* It provides buttons to approve, reject, delete and clear the selected tasks.
*
* @param selectTimesheetId - The selected timesheet logs.
* @param updateTimesheetStatus - A function to update the status of the selected timesheet logs.
* @param deleteTaskTimesheet - A function to delete the selected timesheet logs.
* @param setSelectTimesheetId - A function to set the selected timesheet logs.
* @param fullWidth - A boolean to indicate if the component should be rendered in full width.
* @returns {React.ReactElement} - The rendered timesheet component.
*/
export const SelectedTimesheet: React.FC<SelectedTimesheetProps> = ({ selectTimesheetId, updateTimesheetStatus, deleteTaskTimesheet, setSelectTimesheetId, fullWidth }) => {
const handleApprove = useCallback(async () => {
try {
updateTimesheetStatus({
status: 'APPROVED',
ids: selectTimesheetId.map((select) => select.timesheet.id).filter((id) => id !== undefined)
}).then(() => {
setSelectTimesheetId([]);
});
} catch (error) {
console.error(error);
}
}, [selectTimesheetId, updateTimesheetStatus]);

const handleReject = useCallback(async () => {
try {
updateTimesheetStatus({
status: 'DENIED',
ids: selectTimesheetId.map((select) => select.timesheet.id).filter((id) => id !== undefined)
}).then(() => {
setSelectTimesheetId([]);
});
} catch (error) {
console.error(error);
}
}, [selectTimesheetId, updateTimesheetStatus]);

const handleDelete = useCallback(async () => {
try {
deleteTaskTimesheet({
logIds: selectTimesheetId?.map((select) => select.timesheet.id).filter((id) => id !== undefined)
}).then(() => {
setSelectTimesheetId([]);
});
} catch (error) {
console.error(error);
}
}, [selectTimesheetId, deleteTaskTimesheet, setSelectTimesheetId]);

return (
<SelectionBar
selectedCount={selectTimesheetId.length}
onApprove={handleApprove}
onReject={handleReject}
onDelete={handleDelete}
onClearSelection={() => setSelectTimesheetId([])}
fullWidth={fullWidth}
/>
)
}
23 changes: 20 additions & 3 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { fullWidthState } from '@app/stores/fullWidth';
import { useAtomValue } from 'jotai';

import { ArrowLeftIcon } from 'assets/svg';
import { CalendarView, CalendarViewIcon, FilterStatus, ListViewIcon, MemberWorkIcon, MenHoursIcon, PendingTaskIcon, TimesheetCard, TimesheetFilter, TimesheetView } from './components';
import { CalendarView, CalendarViewIcon, FilterStatus, ListViewIcon, MemberWorkIcon, MenHoursIcon, PendingTaskIcon, SelectedTimesheet, TimesheetCard, TimesheetFilter, TimesheetView } from './components';
import { GoSearch } from 'react-icons/go';

import { differenceBetweenHours, getGreeting, secondsToTime } from '@/app/helpers';
Expand Down Expand Up @@ -55,7 +55,16 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
to: endOfMonth(new Date()),
});

const { timesheet: filterDataTimesheet, statusTimesheet, loadingTimesheet, isManage, timesheetGroupByDays } = useTimesheet({
const {
timesheet: filterDataTimesheet,
statusTimesheet, loadingTimesheet,
isManage,
timesheetGroupByDays,
selectTimesheetId,
setSelectTimesheetId,
updateTimesheetStatus,
deleteTaskTimesheet
} = useTimesheet({
startDate: dateRange.from!,
endDate: dateRange.to!,
timesheetViewMode: timesheetNavigator,
Expand Down Expand Up @@ -281,8 +290,16 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
totalGroups={totalGroups}
/>
)}
</div>

</div>
{selectTimesheetId.length > 0 && <SelectedTimesheet
deleteTaskTimesheet={deleteTaskTimesheet}
fullWidth={fullWidth}
selectTimesheetId={selectTimesheetId}
setSelectTimesheetId={setSelectTimesheetId}
updateTimesheetStatus={updateTimesheetStatus}
/>
}
</Container>
</div>
</MainLayout>
Expand Down
6 changes: 4 additions & 2 deletions apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IUser, RoleNameEnum, TimesheetLog } from '@/app/interfaces';
import { timesheetDeleteState, timesheetGroupByDayState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState, timesheetUpdateStatus } from '@/app/stores';
import { timesheetDeleteState, timesheetGroupByDayState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState, timesheetUpdateStatus, selectTimesheetIdState } from '@/app/stores';
import { useAtom } from 'jotai';
import React from 'react';

Expand All @@ -13,7 +13,9 @@ export function useTimelogFilterOptions() {
const [timesheetGroupByDays, setTimesheetGroupByDays] = useAtom(timesheetGroupByDayState);
const [puTimesheetStatus, setPuTimesheetStatus] = useAtom(timesheetUpdateStatus)
const [selectedItems, setSelectedItems] = React.useState<{ status: string; date: string }[]>([]);
const [selectTimesheetId, setSelectTimesheetId] = React.useState<TimesheetLog[]>([])
// const [selectTimesheetId, setSelectTimesheetId] = React.useState<TimesheetLog[]>([])
const [selectTimesheetId, setSelectTimesheetId] = useAtom(selectTimesheetIdState)


const employee = employeeState;
const project = projectState;
Expand Down
7 changes: 5 additions & 2 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export function useTimesheet({
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText, setSelectTimesheetId } = useTimelogFilterOptions();
const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText, setSelectTimesheetId, selectTimesheetId, handleSelectRowByStatusAndDate, handleSelectRowTimesheet } = useTimelogFilterOptions();
const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi);
const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi)
Expand Down Expand Up @@ -384,6 +384,9 @@ export function useTimesheet({
groupByDate,
isManage,
normalizeText,
setSelectTimesheetId
setSelectTimesheetId,
selectTimesheetId,
handleSelectRowByStatusAndDate,
handleSelectRowTimesheet
};
}
1 change: 1 addition & 0 deletions apps/web/app/stores/time-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export const timesheetDeleteState = atom<string[]>([]);
export const timesheetGroupByDayState = atom<TimesheetFilterByDays>('Daily')
export const timesheetUpdateStatus = atom<UpdateTimesheetStatus[]>([])
export const timesheetUpdateState = atom<TimesheetLog | null>(null)
export const selectTimesheetIdState = atom<TimesheetLog[]>([])

0 comments on commit 04ecaf8

Please sign in to comment.