Skip to content

Commit

Permalink
[Feat]: Improve entry selection logic (#3484)
Browse files Browse the repository at this point in the history
* feat(timesheet): Improve entry selection logic

* refactor(timesheet): Improve search and filtering functionality

* fix:coderabbitai
  • Loading branch information
Innocent-Akim authored Dec 26, 2024
1 parent 13e8403 commit e6a9b33
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
className='w-full font-medium dark:text-white'
options={activeTeam?.members || []}
onChange={(value) => {
console.log(value)
updateFormState('employeeId', value)
}}
renderOption={(option) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
closeModal={closeModal}
isOpen={isOpen}
showCloseIcon
title={'Edit Task'}
title={t('common.EDIT_TASK')}
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-40 md:min-w-[32rem] justify-start h-[auto]"
titleClass="font-bold flex justify-start w-full">
<form onSubmit={handleUpdateSubmit} className="flex flex-col w-full">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,27 @@ export interface IRejectSelectedModalProps {
onReject: (reason: string) => void;
minReasonLength?: number;
maxReasonLength?: number;
selectTimesheetId?: string[];
}
/**
* A modal for rejecting selected timesheet entries.
*
* @param isOpen - If true, show the modal. Otherwise, hide the modal.
* @param closeModal - A function to close the modal.
* @param maxReasonLength - The maximum length of the rejection reason.
* @param onReject - A function to call when the user rejects the selected entries.
* @param minReasonLength - The minimum length of the rejection reason.
* @param selectTimesheetId - The IDs of the timesheet entries to be rejected.
*
* @returns A modal component.
*/
export function RejectSelectedModal({
isOpen,
closeModal,
maxReasonLength,
onReject,
minReasonLength
minReasonLength,
selectTimesheetId
}: IRejectSelectedModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [reason, setReason] = useState('');
Expand Down
22 changes: 3 additions & 19 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,34 +52,18 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
to: endOfMonth(new Date()),
});

const { timesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({
const { timesheet: filterDataTimesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({
startDate: dateRange.from!,
endDate: dateRange.to!,
timesheetViewMode: timesheetNavigator
timesheetViewMode: timesheetNavigator,
inputSearch: search
});

React.useEffect(() => {
getOrganizationProjects();
}, [getOrganizationProjects])

const lowerCaseSearch = useMemo(() => search?.toLowerCase() ?? '', [search]);
const filterDataTimesheet = useMemo(() => {
const filteredTimesheet =
timesheet
.filter((v) =>
v.tasks.some(
(task) =>
task.task?.title?.toLowerCase()?.includes(lowerCaseSearch) ||
task.employee?.fullName?.toLowerCase()?.includes(lowerCaseSearch) ||
task.project?.name?.toLowerCase()?.includes(lowerCaseSearch)
)
);

return filteredTimesheet;
}, [
timesheet,
lowerCaseSearch,
]);
const {
isOpen: isManualTimeModalOpen,
openModal: openManualTimeModal,
Expand Down
28 changes: 19 additions & 9 deletions apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export function useTimelogFilterOptions() {
];
return user?.role.name ? allowedRoles.includes(user.role.name as RoleNameEnum) : false;
};
const normalizeText = (text: string | undefined | null): string => {
if (!text) return '';
return text
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim();
};

const generateTimeOptions = (interval = 15) => {
const totalSlots = (24 * 60) / interval; // Total intervals in a day
Expand All @@ -47,16 +55,17 @@ export function useTimelogFilterOptions() {
const handleSelectRowByStatusAndDate = (logs: TimesheetLog[], isChecked: boolean) => {
setSelectTimesheetId((prev) => {
const logIds = logs.map((item) => item.id);

if (isChecked) {
return [...new Set([...prev, ...logIds])];
} else {
return prev.filter((id) => !logIds.includes(id));
if (!isChecked) {
const allSelected = logIds.every(id => prev.includes(id));
if (allSelected) {
return prev.filter((id) => !logIds.includes(id));
} else {
return [...new Set([...prev, ...logIds])];
}
}
return [...new Set([...prev, ...logIds])];
});
}


};

React.useEffect(() => {
return () => setSelectTimesheetId([]);
Expand Down Expand Up @@ -84,6 +93,7 @@ export function useTimelogFilterOptions() {
generateTimeOptions,
setPuTimesheetStatus,
puTimesheetStatus,
isUserAllowedToAccess
isUserAllowedToAccess,
normalizeText
};
}
88 changes: 76 additions & 12 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface TimesheetParams {
startDate?: Date | string;
endDate?: Date | string;
timesheetViewMode?: 'ListView' | 'CalendarView'
inputSearch?: string
}

export interface GroupedTimesheet {
Expand Down Expand Up @@ -88,14 +89,46 @@ const groupByMonth = createGroupingFunction(date =>
`${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`
);

/**
* @function useTimesheet
*
* @description
* Fetches timesheet logs based on the provided date range and filters.
*
* @param {TimesheetParams} params
* @prop {Date} startDate - Start date of the period to fetch.
* @prop {Date} endDate - End date of the period to fetch.
* @prop {string} timesheetViewMode - "ListView" or "CalendarView"
* @prop {string} inputSearch - Search string to filter the timesheet logs.
*
* @returns
* @prop {boolean} loadingTimesheet - Whether the timesheet is being fetched.
* @prop {TimesheetLog[]} timesheet - The list of timesheet logs, grouped by day.
* @prop {function} getTaskTimesheet - Callable to fetch timesheet logs.
* @prop {boolean} loadingDeleteTimesheet - Whether a timesheet is being deleted.
* @prop {function} deleteTaskTimesheet - Callable to delete timesheet logs.
* @prop {function} getStatusTimesheet - Callable to group timesheet logs by status.
* @prop {TimesheetStatus} timesheetGroupByDays - The current filter for grouping timesheet logs.
* @prop {object} statusTimesheet - Timesheet logs grouped by status.
* @prop {function} updateTimesheetStatus - Callable to update the status of timesheet logs.
* @prop {boolean} loadingUpdateTimesheetStatus - Whether timesheet logs are being updated.
* @prop {boolean} puTimesheetStatus - Whether timesheet logs are updatable.
* @prop {function} createTimesheet - Callable to create a new timesheet log.
* @prop {boolean} loadingCreateTimesheet - Whether a timesheet log is being created.
* @prop {function} updateTimesheet - Callable to update a timesheet log.
* @prop {boolean} loadingUpdateTimesheet - Whether a timesheet log is being updated.
* @prop {function} groupByDate - Callable to group timesheet logs by date.
* @prop {boolean} isManage - Whether the user is authorized to manage the timesheet.
*/
export function useTimesheet({
startDate,
endDate,
timesheetViewMode
timesheetViewMode,
inputSearch
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess } = useTimelogFilterOptions();
const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText } = 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 @@ -266,23 +299,53 @@ export function useTimesheet({
}, [user, queryDeleteTimesheet, setTimesheet]);


const filterDataTimesheet = useMemo(() => {
if (!timesheet || !inputSearch) {
return timesheet;
}
const searchTerms = normalizeText(inputSearch).split(/\s+/).filter(Boolean);
if (searchTerms.length === 0) {
return timesheet;
}
return timesheet.filter((task) => {
const searchableContent = {
title: normalizeText(task.task?.title),
employee: normalizeText(task.employee?.fullName),
project: normalizeText(task.project?.name)
};
return searchTerms.every(term =>
Object.values(searchableContent).some(content =>
content.includes(term)
)
);
});
}, [timesheet, inputSearch]);

const timesheetElementGroup = useMemo(() => {
if (!timesheet) {
return [];
}

if (timesheetViewMode === 'ListView') {
if (timesheetGroupByDays === 'Daily') {
return groupByDate(timesheet);
switch (timesheetGroupByDays) {
case 'Daily':
return groupByDate(filterDataTimesheet);
case 'Weekly':
return groupByWeek(filterDataTimesheet);
case 'Monthly':
return groupByMonth(filterDataTimesheet);
default:
return groupByDate(filterDataTimesheet);
}
if (timesheetGroupByDays === 'Weekly') {
return groupByWeek(timesheet);
}
return groupByMonth(timesheet);
}
return groupByDate(timesheet);

return groupByDate(filterDataTimesheet);
}, [timesheetGroupByDays, timesheetViewMode, timesheet]);


useEffect(() => {
getTaskTimesheet({ startDate, endDate });
}, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays]);
}, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, inputSearch]);

return {
loadingTimesheet,
Expand All @@ -292,7 +355,7 @@ export function useTimesheet({
deleteTaskTimesheet,
getStatusTimesheet,
timesheetGroupByDays,
statusTimesheet: getStatusTimesheet(timesheet.flat()),
statusTimesheet: getStatusTimesheet(filterDataTimesheet.flat()),
updateTimesheetStatus,
loadingUpdateTimesheetStatus,
puTimesheetStatus,
Expand All @@ -301,6 +364,7 @@ export function useTimesheet({
updateTimesheet,
loadingUpdateTimesheet,
groupByDate,
isManage
isManage,
normalizeText
};
}
20 changes: 16 additions & 4 deletions apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
countID={selectTimesheetId.length}
/>
<RejectSelectedModal
selectTimesheetId={selectTimesheetId}
onReject={() => {
// Pending implementation
}}
Expand Down Expand Up @@ -186,11 +187,16 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
<AccordionContent className="flex flex-col w-full">
<HeaderRow
handleSelectRowByStatusAndDate={
() => handleSelectRowByStatusAndDate(rows, selectTimesheetId.length === 0)}
() => handleSelectRowByStatusAndDate(
rows,
!rows.every(row => selectTimesheetId.includes(row.id))
)
}
data={rows}
status={status}
onSort={handleSort}
date={plan.date}
selectedIds={selectTimesheetId}
/>
{rows.map((task) => (
<div
Expand Down Expand Up @@ -520,13 +526,17 @@ const HeaderRow = ({
status,
onSort,
data,
handleSelectRowByStatusAndDate, date
handleSelectRowByStatusAndDate, date,
selectedIds

}: {
status: string;
onSort: (key: string, order: SortOrder) => void,
data: TimesheetLog[],
handleSelectRowByStatusAndDate: (status: string, date: string) => void,
date?: string
date?: string,
selectedIds: string[]

}) => {

const { bg, bgOpacity } = statusColor(status);
Expand All @@ -536,6 +546,7 @@ const HeaderRow = ({
Employee: null,
Status: null,
});
const isAllSelected = data.length > 0 && data.every(row => selectedIds.includes(row.id));

const handleSort = (key: string) => {
const newOrder = sortState[key] === "ASC" ? "DESC" : "ASC";
Expand All @@ -549,6 +560,7 @@ const HeaderRow = ({
className="flex items-center text-[#71717A] font-medium border-b border-t dark:border-gray-600 space-x-4 p-1 h-[60px] w-full"
>
<Checkbox
checked={isAllSelected}
onCheckedChange={() => date && handleSelectRowByStatusAndDate(status, date)}
className="w-5 h-5"
disabled={!date}
Expand Down Expand Up @@ -581,7 +593,7 @@ const HeaderRow = ({
currentSort={sortState["Status"]}
/>
</div>
<div className="space-x-2">
<div className="ml-auto">
<span>Time</span>
</div>
</div>
Expand Down

0 comments on commit e6a9b33

Please sign in to comment.