Skip to content

Commit

Permalink
[Feat]: Implement dynamic date range picker for timesheet filtering (#…
Browse files Browse the repository at this point in the history
…3213)

* feat: Implement dynamic date range picker for timesheet filtering

* fix: Resolved

* improve: timesheet date filter
  • Loading branch information
Innocent-Akim authored Oct 31, 2024
1 parent daa15ab commit df0452e
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 37 deletions.
66 changes: 29 additions & 37 deletions apps/web/app/[locale]/timesheet/components/TimesheetFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,39 @@
import { FilterWithStatus } from './FilterWithStatus';
import { FilterTaskActionMenu, FrequencySelect } from '.';
import { FrequencySelect, TimesheetFilterDate } from '.';
import { Button } from 'lib/components';
import { TimeSheetFilterPopover } from './time-sheet-filter-popover';
import { Cross2Icon } from '@radix-ui/react-icons';
import { SettingFilterIcon } from '@/assets/svg';

export function TimesheetFilter() {
return (
<>

<div className="flex flex-col md:flex-row w-full gap-4">
<div className="flex-1">
<FilterWithStatus
activeStatus="All Tasks"
onToggle={(label) => {
// TODO: Implement filter toggle handler
}}
/>
</div>
<div className="flex-1 flex justify-end">
<div className='flex gap-2'>
<FrequencySelect />
<div className='flex items-center border border-gray-100 rounded-md h-10 '>
<FilterTaskActionMenu />
<button
aria-label="Clear filters"
onClick={() => {
// TODO: Implement clear filters logic
}}
className='border-l h-10 w-10 font-normal flex items-center justify-center text-gray-400 hover:bg-gray-50'>
<Cross2Icon />
</button>
</div>
<TimeSheetFilterPopover />
<Button
onClick={() => null}
variant='outline'
className='bg-primary/5 dark:bg-primary-light h-10 w-[2.5rem] font-medium'>
Add Time
</Button>
</div>
<div className="grid grid-cols-3 w-full">
<div className="col-span-1">
<FilterWithStatus
activeStatus="Rejected"
onToggle={(label) => {
console.log(label)
}}
/>
</div>
<div className="col-span-1"></div>
<div className="col-span-1">
<div className='flex gap-2'>
<FrequencySelect />
<button
onClick={() => null}
className='flex items-center justify-center h-10 rounded-lg bg-white dark:bg-dark--theme-light border dark:border-gray-700 hover:bg-white p-3 gap-2' >
<SettingFilterIcon className="text-gray-700 dark:text-white w-3.5" strokeWidth="1.8" />
<span className="text-gray-700 dark:text-white">Filter</span>
</button>
<TimesheetFilterDate />
<Button
onClick={() => null}
variant='outline'
className='bg-primary/5 dark:bg-primary-light h-10 w-[2.5rem] font-medium'>
Add Time
</Button>
</div>
</div>
</>
</div>

)
}
197 changes: 197 additions & 0 deletions apps/web/app/[locale]/timesheet/components/TimesheetFilterDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { cn } from "@/lib/utils"
import { DatePicker } from "@components/ui/DatePicker"
import { Button } from "@components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/ui/popover"
import { CalendarIcon } from "@radix-ui/react-icons"
import { format } from "date-fns"
import React from "react"
import { MdKeyboardArrowRight } from "react-icons/md"
import { PiCalendarDotsThin } from "react-icons/pi"

interface DatePickerInputProps {
date: Date | null;
label: string;
}
interface TimesheetFilterDateProps {
onChange?: (range: { from: Date | null; to: Date | null }) => void;
initialRange?: { from: Date | null; to: Date | null };
minDate?: Date;
maxDate?: Date;
}

export function TimesheetFilterDate({
onChange,
initialRange,
minDate,
maxDate
}: TimesheetFilterDateProps) {

const [dateRange, setDateRange] = React.useState<{ from: Date | null; to: Date | null }>({
from: initialRange?.from ?? new Date(),
to: initialRange?.to ?? new Date(),
});

const handleFromChange = (fromDate: Date | null) => {
if (maxDate && fromDate && fromDate > maxDate) {
return;
}
setDateRange((prev) => ({ ...prev, from: fromDate }));
onChange?.({ ...dateRange, from: fromDate });
};

const handleToChange = (toDate: Date | null) => {
if (dateRange.from && toDate && toDate < dateRange.from) {
return;
}
setDateRange((prev) => ({ ...prev, to: toDate }));
};

const handlePresetClick = (preset: string) => {
const today = new Date();
switch (preset) {
case 'Today':
setDateRange({ from: today, to: today });
break;
case 'Last 7 days':
setDateRange({
from: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7),
to: today
});
break;
case 'Last 30 days':
setDateRange({
from: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 30),
to: today
});
break;
case `This year (${today.getFullYear()})`:
setDateRange({
from: new Date(today.getFullYear(), 0, 1),
to: today
});
break;
case 'Custom Date Range':
setDateRange({ from: null, to: null });
break;
default:
break;
}
};


return (<>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
role="combobox"
aria-label="Select date range"
aria-expanded="false"
className={cn(
"w-[240px] justify-start text-left font-normal",
!dateRange.from && "text-muted-foreground"
)}>
<CalendarIcon />
{dateRange.from ? format(dateRange.from, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0 flex">
<div className="flex flex-col p-2 gap-2">
<DatePickerFilter
label="From"
date={dateRange.from}
setDate={handleFromChange}
/>
<DatePickerFilter
label="To"
date={dateRange.to}
setDate={handleToChange}
minDate={dateRange.from}
/>
</div>
<div className="flex flex-col p-2">
{["Today", "Last 7 days", "Last 30 days", `This year (${new Date().getFullYear()})`, "Custom Date Range"].map((label, index) => (
<Button
key={index}
variant="outline"
className="h-7 flex items-center justify-between border-none text-[12px] text-gray-700"
onClick={() => handlePresetClick(label)}>
<span> {label}</span>
{label === 'Custom Date Range' && <MdKeyboardArrowRight />}
</Button>
))}
</div>
</PopoverContent>
</Popover>
</>
)
}


const DatePickerInput: React.FC<DatePickerInputProps> = ({ date, label }) => (
<>
<Button
variant="outline"
className={cn(
"w-[150px] justify-start text-left font-normal bg-transparent hover:bg-transparent text-black h-8 border border-transparent dark:border-transparent",
!date && "text-muted-foreground"
)}
>
{date ? format(date, "LLL dd, y") : <span>{label}</span>}
</Button>
<PiCalendarDotsThin className="h-5 w-5 dark:text-gray-500" />
</>
);

export function DatePickerFilter({
label,
date,
setDate,
minDate,
maxDate
}: {
label: string;
date: Date | null;
setDate: (date: Date | null) => void;
minDate?: Date | null;
maxDate?: Date | null

}) {
const isDateDisabled = React.useCallback((date: Date) => {
if (minDate && date < minDate) return true;
if (maxDate && date > maxDate) return true;
return false;
}, [minDate, maxDate]);

return (
<div>
<DatePicker
buttonVariant={'link'}
className="dark:bg-dark--theme-light rounded-lg bg-white"
buttonClassName={'decoration-transparent flex items-center w-full bg-white dark:bg-dark--theme-light border-gray-300 justify-start text-left font-normal text-black h-10 border dark:border-slate-600 rounded-md'}
customInput={<DatePickerInput date={date} label={label} />}
mode="single"
numberOfMonths={1}
initialFocus
defaultMonth={date ?? new Date()}
selected={date ?? new Date()}
onSelect={(selectedDate) => {
if (selectedDate && !isDateDisabled(selectedDate)) {
setDate(selectedDate);
}
}}
modifiersClassNames={{
disabled: 'text-gray-300 cursor-not-allowed',
}}
disabled={[
...(minDate ? [{ before: minDate }] : []),
...(maxDate ? [{ after: maxDate }] : [])
]}
/>
</div>
);
}
1 change: 1 addition & 0 deletions apps/web/app/[locale]/timesheet/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './CalendarView';
export * from './TimesheetFilter';
export * from './FrequencySelect';
export * from './FilterWithStatus';
export * from './TimesheetFilterDate';

0 comments on commit df0452e

Please sign in to comment.