Skip to content

Commit

Permalink
[Feat]: Add sortable headers to Timesheet component with dynamic sort…
Browse files Browse the repository at this point in the history
…ing (#3367)

* Add sortable headers to Timesheet component with dynamic sorting logic for columns

* fix:deepscan

* fix: coderabbitai

* fix: coderabbitai
  • Loading branch information
Innocent-Akim authored Nov 27, 2024
1 parent 528362c commit 897ad04
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BackdropLoader } from "@/lib/components";
import { useEffect, useState } from "react";

export function TimesheetLoader({ show = false }: { show?: boolean }) {
const [dots, setDots] = useState("");

useEffect(() => {
if (!show) {
setDots(""); // Reset the dots when loader is hidden
return;
}

const interval = setInterval(() => {
setDots((prev) => (prev.length < 3 ? prev + "." : ""));
}, 1000); // Update dots every second

return () => clearInterval(interval); // Cleanup interval on unmount or when `show` changes
}, [show]);

return (
<BackdropLoader show={show} title={`Loading${dots}`} />
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,28 @@ import { GroupedTimesheet } from '@/app/hooks/features/useTimesheet';
import { DataTableTimeSheet } from 'lib/features/integrations/calendar';
import { useTranslations } from 'next-intl';

export function TimesheetView({ data }: { data?: GroupedTimesheet[] }) {
export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; loading?: boolean }) {
const t = useTranslations();

if (loading || !data) {
return (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme flex items-center justify-center">
<p>{t('pages.timesheet.LOADING')}</p>
</div>
);
}

if (data.length === 0) {
return (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme flex flex-col items-center justify-center h-full min-h-[280px]">
<p>{t('pages.timesheet.NO_ENTRIES_FOUND')}</p>
</div>
);
}

return (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">
{data ? (
data.length > 0 ? (
<DataTableTimeSheet data={data} />
) : (
<div className="flex items-center justify-center h-full min-h-[280px]">
<p>{t('pages.timesheet.NO_ENTRIES_FOUND')}</p>
</div>
)
) : (
<div className="flex items-center justify-center h-full">
<p>{t('pages.timesheet.LOADING')}</p>
</div>
)}
<DataTableTimeSheet data={data} />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './TimeSheetFilterPopover'
export * from './TimesheetAction';
export * from './RejectSelectedModal';
export * from './EditTaskModal';
export * from './TimesheetLoader'
5 changes: 3 additions & 2 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
from: startOfDay(new Date()),
to: endOfDay(new Date())
});
const { timesheet, statusTimesheet } = useTimesheet({
const { timesheet, statusTimesheet, loadingTimesheet } = useTimesheet({
startDate: dateRange.from ?? '',
endDate: dateRange.to ?? ''
});
Expand Down Expand Up @@ -195,7 +195,8 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
{/* <DropdownMenuDemo /> */}
<div className="border border-gray-200 rounded-lg dark:border-gray-800">
{timesheetNavigator === 'ListView' ? (
<TimesheetView data={filterDataTimesheet} />
<TimesheetView data={filterDataTimesheet}
loading={loadingTimesheet} />
) : (
<CalendarView />
)}
Expand Down
14 changes: 14 additions & 0 deletions apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function useTimelogFilterOptions() {
const [selectTimesheet, setSelectTimesheet] = useAtom(timesheetDeleteState);
const [timesheetGroupByDays, setTimesheetGroupByDays] = useAtom(timesheetGroupByDayState);
const [puTimesheetStatus, setPuTimesheetStatus] = useAtom(timesheetUpdateStatus)
const [selectedItems, setSelectedItems] = React.useState<{ status: string; date: string }[]>([]);

const employee = employeeState;
const project = projectState;
Expand All @@ -26,6 +27,17 @@ export function useTimelogFilterOptions() {
const handleSelectRowTimesheet = (items: string) => {
setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items])
}

const handleSelectRowByStatusAndDate = (status: string, date: string) => {
setSelectedItems((prev) =>
prev.some((item) => item.status === status && item.date === date)
? prev.filter((item) => !(item.status === status && item.date === date))
: [...prev, { status, date }]
);
}



React.useEffect(() => {
return () => setSelectTimesheet([]);
}, []);
Expand All @@ -40,6 +52,8 @@ export function useTimelogFilterOptions() {
setTaskState,
setStatusState,
handleSelectRowTimesheet,
handleSelectRowByStatusAndDate,
selectedItems,
selectTimesheet,
setSelectTimesheet,
timesheetGroupByDays,
Expand Down
144 changes: 131 additions & 13 deletions apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ import {
MdKeyboardDoubleArrowLeft,
MdKeyboardDoubleArrowRight,
MdKeyboardArrowLeft,
MdKeyboardArrowRight
MdKeyboardArrowRight,
MdKeyboardArrowUp,
MdKeyboardArrowDown
} from 'react-icons/md';
import { ConfirmStatusChange, StatusBadge, statusOptions, dataSourceTimeSheet, TimeSheet } from '.';
import { useModal, useTimelogFilterOptions } from '@app/hooks';
Expand Down Expand Up @@ -153,7 +155,7 @@ export const columns: ColumnDef<TimeSheet>[] = [
export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
const { isOpen, openModal, closeModal } = useModal();
const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet } = useTimesheet({});
const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet, timesheetGroupByDays } = useTimelogFilterOptions();
const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet, timesheetGroupByDays, handleSelectRowByStatusAndDate } = useTimelogFilterOptions();
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const handleConfirm = () => {
try {
Expand Down Expand Up @@ -195,7 +197,9 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
rowSelection
}
});

const handleSort = (key: string, order: SortOrder) => {
console.log(`Sorting ${key} in ${order} order`);
};
const handleButtonClick = (action: StatusAction) => {
switch (action) {
case 'Approved':
Expand All @@ -211,7 +215,6 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
console.error(`Unsupported action: ${action}`);
}
};

return (
<div className="w-full dark:bg-dark--theme">
<AlertDialogConfirmation
Expand Down Expand Up @@ -255,11 +258,11 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
/>
</div>
<Accordion type="single" collapsible>
{Object.entries(getStatusTimesheet(plan.tasks)).map(([status, rows]) => (
<AccordionItem
{Object.entries(getStatusTimesheet(plan.tasks)).map(([status, rows]) => {
return rows.length > 0 && status && <AccordionItem
key={status}
value={status === 'DENIED' ? 'REJECTED' : status}
className="p-1 rounded"
className={clsxm("p-1 rounded")}
>
<AccordionTrigger
style={{ backgroundColor: statusColor(status).bgOpacity }}
Expand All @@ -276,7 +279,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
<span className="text-base font-normal text-gray-400 uppercase">
{status === 'DENIED' ? 'REJECTED' : status}
</span>
<span className="text-gray-400 text-[14px]">({rows?.length})</span>
<span className="text-gray-400 text-[14px]">({rows.length})</span>
</div>
<Badge
variant={'outline'}
Expand All @@ -292,7 +295,15 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
</div>
</AccordionTrigger>
<AccordionContent className="flex flex-col w-full">
{rows?.map((task) => (
<HeaderRow
handleSelectRowByStatusAndDate={
() => handleSelectRowByStatusAndDate(status, plan.date)}
data={rows}
status={status}
onSort={handleSort}
date={plan.date}
/>
{rows.map((task) => (
<div
key={task.id}
style={{
Expand Down Expand Up @@ -335,7 +346,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
<Badge
className={`${getBadgeColor(task.timesheet.status as TimesheetStatus)} rounded-md py-1 px-2 text-center font-medium text-black`}
>
{task.timesheet.status}
{task.timesheet.status === 'DENIED' ? 'REJECTED' : task.timesheet.status}
</Badge>
</div>
<DisplayTimeForTimesheet
Expand All @@ -346,11 +357,11 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
))}
</AccordionContent>
</AccordionItem>
))}
}
)}
</Accordion>
</div>
}

)}
</div>
<div className="flex items-center justify-end p-4 space-x-2">
Expand Down Expand Up @@ -388,7 +399,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
</Button>
</div>
</div>
</div>
</div >
);
}

Expand Down Expand Up @@ -555,3 +566,110 @@ const getBadgeColor = (timesheetStatus: TimesheetStatus | null) => {
return 'bg-gray-100';
}
};


type SortOrder = "ASC" | "DESC";

const HeaderColumn = ({
label,
onSort,
currentSort,
}: {
label: string;
onSort: () => void;
currentSort: SortOrder | null;
}) => (
<div className="flex gap-x-2" role="columnheader">
<span>{label}</span>
<button
onClick={onSort}
aria-label={`Sort ${label} column ${currentSort ? `currently ${currentSort.toLowerCase()}` : ''}`}
className="flex flex-col items-start leading-none gap-0"
>
<MdKeyboardArrowUp
style={{
height: 10,
color: "#71717A",
}}
/>
<MdKeyboardArrowDown
style={{
height: 10,
color: "#71717A",
}}
/>
</button>
</div>
);

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

const { bg, bgOpacity } = statusColor(status);
const [sortState, setSortState] = React.useState<{ [key: string]: SortOrder | null }>({
Task: null,
Project: null,
Employee: null,
Status: null,
});

const handleSort = (key: string) => {
const newOrder = sortState[key] === "ASC" ? "DESC" : "ASC";
setSortState({ ...sortState, [key]: newOrder });
onSort(key, newOrder);
};

return (
<div
style={{ backgroundColor: bgOpacity, borderBottomColor: bg }}
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
onCheckedChange={() => date && handleSelectRowByStatusAndDate(status, date)}
className="w-5 h-5"
disabled={!date}
/>
<div className="flex-[2]">
<HeaderColumn
label="Task"
onSort={() => handleSort("Task")}
currentSort={sortState["Task"]}
/>
</div>
<div className="flex-1">
<HeaderColumn
label="Project"
onSort={() => handleSort("Project")}
currentSort={sortState["Project"]}
/>
</div>
<div className="flex-1">
<HeaderColumn
label="Employee"
onSort={() => handleSort("Employee")}
currentSort={sortState["Employee"]}
/>
</div>
<div className="flex-auto">
<HeaderColumn
label="Status"
onSort={() => handleSort("Status")}
currentSort={sortState["Status"]}
/>
</div>
<div className="space-x-2">
<span>Time</span>
</div>
</div>
);
};

0 comments on commit 897ad04

Please sign in to comment.