From 2d58bfb6aa2c0cbf2b6c62593f3ad057cb32b4ef Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Tue, 3 Dec 2024 08:21:53 +0200 Subject: [PATCH] feat: Add MonthlyCalendarDataView component with dynamic calendar generation and timesheet grouping --- .../[memberId]/components/CalendarView.tsx | 134 +++++++++++++++--- .../components/MonthlyTimesheetCalendar.tsx | 126 ++++++++++++++++ apps/web/lib/features/task/task-displays.tsx | 1 + 3 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx index 584cf19df..b6d825ac1 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx @@ -1,22 +1,31 @@ import { GroupedTimesheet, useTimesheet } from "@/app/hooks/features/useTimesheet"; -import { clsxm } from "@/app/utils"; import { statusColor } from "@/lib/components"; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from "@/lib/features"; -import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion"; -import { Accordion } from "@radix-ui/react-accordion"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion"; import { TranslationHooks, useTranslations } from "next-intl"; import React from "react"; import { EmployeeAvatar } from "./CompactTimesheetComponent"; import { formatDate } from "@/app/helpers"; -import { ClockIcon } from "lucide-react"; +import { ClockIcon, CodeSquareIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; +import MonthlyTimesheetCalendar from "./MonthlyTimesheetCalendar"; +import { useTimelogFilterOptions } from "@/app/hooks"; + export function CalendarView({ data, loading }: { data?: GroupedTimesheet[], loading: boolean }) { const t = useTranslations(); + const { timesheetGroupByDays } = useTimelogFilterOptions(); + // "Daily" | "Weekly" | "Monthly" return ( -
+
{data ? ( data.length > 0 ? ( - + <> + {timesheetGroupByDays === 'Monthly' ? + : + timesheetGroupByDays === 'Daily' ? + : null} + ) : (

{t('pages.timesheet.NO_ENTRIES_FOUND')}

@@ -40,7 +49,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati {data?.map((plan, index) => { return
0 && status && + className="p-1 rounded" > + )}>
-
+
{status === 'DENIED' ? 'REJECTED' : status} @@ -97,10 +104,9 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati borderLeftColor: statusColor(status).border }} - className={clsxm( + className={cn( 'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 h-[100px]', - )} - > + )}>
) } + + +const MonthlyCalendarDataView = ({ data }: { data: GroupedTimesheet[] }) => { + const { getStatusTimesheet } = useTimesheet({}); + return ( + { + return <> + {plan ? ( + + {Object.entries(getStatusTimesheet(plan.tasks)).map(([status, rows]) => ( + rows.length > 0 && status && + +
+
+
+
+ + {status === 'DENIED' ? 'REJECTED' : status} + + ({rows.length}) +
+
+
+ + +
+
+
+ + {rows.map((task) => ( +
+
+
+ + {task.employee.fullName} +
+ +
+ +
+ {task.project && task.project.name} +
+
+ ))} +
+
+ ))} +
+ ) : ( +
+ + No Data +
+ )} + + }} /> + ) +} diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx new file mode 100644 index 000000000..9a6e57cb6 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/MonthlyTimesheetCalendar.tsx @@ -0,0 +1,126 @@ +import React, { useMemo, useState, useCallback } from "react"; +import { format, addMonths, eachDayOfInterval, startOfMonth, endOfMonth, addDays, Locale } from "date-fns"; +import { GroupedTimesheet } from "@/app/hooks/features/useTimesheet"; +import { fr, enGB } from 'date-fns/locale'; +import { cn } from "@/lib/utils"; +import { TotalDurationByDate } from "@/lib/features"; +import { formatDate } from "@/app/helpers"; + +type MonthlyCalendarDataViewProps = { + data?: GroupedTimesheet[]; + onDateClick?: (date: Date) => void; + renderDayContent?: (date: Date, plan?: GroupedTimesheet) => React.ReactNode; + locale?: Locale; + daysLabels?: string[]; + noDataText?: string; + classNames?: { + container?: string; + header?: string; + grid?: string; + day?: string; + noData?: string; + }; +}; + +const defaultDaysLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +const generateFullCalendar = (currentMonth: Date) => { + const monthStart = startOfMonth(currentMonth); + const monthEnd = endOfMonth(currentMonth); + const startDate = addDays(monthStart, -monthStart.getDay()); + const endDate = addDays(monthEnd, 6 - monthEnd.getDay()); + return eachDayOfInterval({ start: startDate, end: endDate }); +}; + +const MonthlyTimesheetCalendar: React.FC = ({ + data = [], + onDateClick, + renderDayContent, + locale = enGB, + daysLabels = defaultDaysLabels, + noDataText = "No Data", + classNames = {} +}) => { + const [currentMonth, setCurrentMonth] = useState(new Date()); + const calendarDates = useMemo(() => generateFullCalendar(currentMonth), [currentMonth]); + const groupedData = useMemo( + () => new Map(data.map((plan) => [format(new Date(plan.date), "yyyy-MM-dd"), plan])), + [data] + ); + + const handlePreviousMonth = useCallback(() => setCurrentMonth((prev) => addMonths(prev, -1)), []); + const handleNextMonth = useCallback(() => setCurrentMonth((prev) => addMonths(prev, 1)), []); + + return ( +
+ {/* Header */} +
+ +

+ {format(currentMonth, "MMMM yyyy", { locale: locale })} +

+ +
+ + {/* Grid */} +
+ {daysLabels.map((day) => ( +
{day}
+ ))} +
+
+ {calendarDates.map((date) => { + const formattedDate = format(date, "yyyy-MM-dd"); + const plan = groupedData.get(formattedDate); + return ( +
onDateClick?.(date)} + > +
+ + {format(date, "dd MMM yyyy")} + +
+ Total{" : "} + {plan && } +
+
+ {renderDayContent ? ( + renderDayContent(date, plan) + ) : plan ? ( +
{JSON.stringify(plan)}
+ ) : ( +
+ {noDataText} +
+ )} +
+ ); + })} +
+
+ ); +}; + +export default MonthlyTimesheetCalendar; diff --git a/apps/web/lib/features/task/task-displays.tsx b/apps/web/lib/features/task/task-displays.tsx index c87453592..bb92d08ff 100644 --- a/apps/web/lib/features/task/task-displays.tsx +++ b/apps/web/lib/features/task/task-displays.tsx @@ -105,6 +105,7 @@ TotalTimeDisplay.displayName = 'TotalTimeDisplay'; export const TotalDurationByDate = React.memo( ({ timesheetLog, createdAt, className }: { timesheetLog: TimesheetLog[]; createdAt: Date | string, className?: string }) => { + console.log("========================>", createdAt) const targetDateISO = new Date(createdAt).toISOString(); const filteredLogs = timesheetLog.filter( (item) => formatDate(item.timesheet.createdAt) === formatDate(targetDateISO));