Skip to content

Commit

Permalink
[Feat]: Visual Distinction of Days with Time Entries in Date Range Fi…
Browse files Browse the repository at this point in the history
…lter (#3499)

* feat: Visual Distinction of Days with Time Entries in Date Range Filter

* fix: Refactor suggestion
  • Loading branch information
Innocent-Akim authored Jan 8, 2025
1 parent 65c4a41 commit 2da1192
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, fi

<div className="flex gap-2">
<FrequencySelect />
<TimesheetFilterDate t={t} {...initDate} />
<TimesheetFilterDate t={t} {...initDate} data={Object.values(data || {}).flat()} />
{isManage && (
<>
<TimeSheetFilterPopover />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ 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 { format, isAfter, isToday, startOfToday } from 'date-fns';
import { TranslationHooks } from 'next-intl';
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';
import { TimesheetLog } from '@/app/interfaces';


interface DatePickerInputProps {
Expand All @@ -25,19 +26,33 @@ export interface TimesheetFilterDateProps {
minDate?: Date;
maxDate?: Date;
t: TranslationHooks;
data?: TimesheetLog[]
}

export function TimesheetFilterDate({
onChange,
initialRange,
minDate,
maxDate,
data,
t
}: Readonly<TimesheetFilterDateProps>) {
const [dateRange, setDateRange] = React.useState<{ from: Date | null; to: Date | null }>({
from: initialRange?.from ?? new Date(),
to: initialRange?.to ?? new Date()
});
const today = startOfToday()

const adjustedInitialRange = React.useMemo(() => {
if (!initialRange) {
return {
from: today,
to: today,
}
}
return {
from: initialRange.from,
to: initialRange.to && isAfter(initialRange.to, today) ? today : initialRange.to,
}
}, [initialRange, today])

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

const [isVisible, setIsVisible] = useState(false);

Expand All @@ -49,6 +64,7 @@ export function TimesheetFilterDate({
onChange?.({ ...dateRange, from: fromDate });
};


const handleToChange = (toDate: Date | null) => {
if (dateRange.from && toDate && toDate < dateRange.from) {
return;
Expand Down Expand Up @@ -129,12 +145,13 @@ export function TimesheetFilterDate({
{isVisible && (
<div className="flex flex-col justify-between gap-2 p-2 translate-x-0">
<div className="flex flex-col gap-2">
<DatePickerFilter label="From" date={dateRange.from} setDate={handleFromChange} />
<DatePickerFilter label="From" date={dateRange.from} setDate={handleFromChange} timesheet={data} />
<DatePickerFilter
label="To"
date={dateRange.to}
setDate={handleToChange}
minDate={dateRange.from}
timesheet={data}
/>
</div>
<div className="flex items-end justify-end w-full">
Expand Down Expand Up @@ -215,14 +232,17 @@ export function DatePickerFilter({
date,
setDate,
minDate,
maxDate
maxDate,
timesheet,
}: {
label: string;
date: Date | null;
setDate: (date: Date | null) => void;
minDate?: Date | null;
maxDate?: Date | null;
timesheet?: TimesheetLog[],
}) {

const isDateDisabled = React.useCallback(
(date: Date) => {
if (minDate && date < minDate) return true;
Expand All @@ -232,41 +252,127 @@ export function DatePickerFilter({
[minDate, maxDate]
);

const datesWithEntries = React.useMemo(() => {
return new Set(timesheet?.map((entry) => format(new Date(entry.createdAt), "yyyy-MM-dd")))
}, [timesheet])

const entriesByDate = React.useMemo(() => {
const map = new Map<string, TimesheetLog[]>();
timesheet?.forEach(entry => {
const dateKey = format(new Date(entry.createdAt), "yyyy-MM-dd");
if (!map.has(dateKey)) {
map.set(dateKey, []);
}
map.get(dateKey)?.push(entry);
});
return map;
}, [timesheet]);

const getEntriesForDate = (date: Date) => {
const dateKey = format(date, "yyyy-MM-dd");
return entriesByDate.get(dateKey) || [];
};
const hasTimeEntry = (date: Date) => {
return datesWithEntries.has(format(date, "yyyy-MM-dd"))
}


const handleSelect = (day: Date) => {
if (day && !isDateDisabled(day)) {
setDate(day);
}
};

return (
<div>
<DatePicker
captionLayout="dropdown"
buttonVariant={'link'}
className="bg-white rounded-lg dark:bg-dark--theme-light dark:text-gray-200 "
buttonVariant="link"
classNames={{
day: "h-9 w-9 text-center rounded-md relative",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "rounded-none",
day_hidden: "invisible",
}}
buttonClassName={
'decoration-transparent flex items-center w-full h-[2.2em] bg-white dark:text-gray-200 dark:bg-dark--theme-light border-gray-300 justify-start text-left font-normal text-black h-[2.2rem] border dark:border-slate-600 rounded-md hover:border-primary'
"decoration-transparent flex items-center w-full h-[2.2em] bg-white dark:text-gray-200 dark:bg-dark--theme-light border-gray-300 justify-start text-left font-normal text-black h-[2.2rem] border dark:border-slate-600 rounded-md hover:border-primary"
}
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);
}
onSelect={(date) => date && handleSelect(date)}
modifiers={{
hasEntry: (date) => hasTimeEntry(date),
today: (day) => isToday(day),
}}
modifiersClassNames={{
booked: clsxm(
'relative after:absolute after:bottom-0 after:left-1/2 after:-translate-x-1/2 after:w-1.5 after:h-1.5 after:bg-primary after:rounded-full'
),
selected: clsxm('bg-primary after:hidden text-white !rounded-full'),
pastDay: clsxm(
'relative after:absolute after:bottom-0 after:left-1/2 after:-translate-x-1/2 after:w-1.5 after:h-1.5 after:bg-yellow-600 after:rounded-full'
),
today: clsxm('border-2 !border-yellow-700 rounded')
selected: clsxm("bg-primary after:hidden text-white !rounded-full"),
today: clsxm("border-2 !border-yellow-700 rounded"),
}}
disabled={[
...(minDate ? [{ before: minDate }] : []),
...(maxDate ? [{ after: maxDate }] : []),
{
before: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
},
]}
components={{
Day: ({ date: dayDate, ...props }) => {
const isSelected = date?.getTime() === dayDate.getTime();

const isDayDisabled = isDateDisabled(dayDate);
return (
<button
disabled={isDayDisabled}
{...props}
className={cn(`h-9 w-9 rounded ${isSelected ? "bg-primary dark:bg-primary-light text-primary-foreground dark:text-white" : ""}`,

)}
onClick={() => handleSelect(dayDate)}>
<div className="relative w-full h-full flex items-center justify-center">
{dayDate.getDate()}
{getEntriesForDate(dayDate).length > 0 && (
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 flex gap-0.5">
<DayIndicators entries={getEntriesForDate(dayDate)} />
</span>
)}
</div>
</button>
);
},
}}
disabled={[...(minDate ? [{ before: minDate }] : []), ...(maxDate ? [{ after: maxDate }] : [])]}
/>
</div>
);
}
const DayIndicators = ({ entries }: { entries: TimesheetLog[] }) => {
if (entries.length === 1) {
return <span
className="h-1 w-1 rounded-full bg-green-500 dark:bg-primary-light"
role="status"
aria-label="1 time entry for this day"
/>;
}
return (
<div
className="flex items-center gap-0.5"
role="status"
aria-label={`${entries.length} time entries for this day`}
>
{[...Array(3)].map((_, index) => (
<span key={index} className="h-1 w-1 rounded-full bg-green-500 dark:bg-primary-light" />
))}
</div>
);
};


interface ICalendarProps<T extends { date: string | Date }> {
setSelectedPlan: Dispatch<SetStateAction<Date>>;
Expand Down

0 comments on commit 2da1192

Please sign in to comment.