Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: Add MonthlyCalendarDataView and WeeklyCalendarDataView for displaying timesheet #3388

Merged
merged 8 commits into from
Dec 4, 2024
134 changes: 116 additions & 18 deletions apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">
<div className="grow h-full bg-[#FFFFFF] dark:bg-dark--theme">
{data ? (
data.length > 0 ? (
<CalendarDataView data={data} t={t} />
<>
{timesheetGroupByDays === 'Monthly' ?
<MonthlyCalendarDataView data={data} /> :
timesheetGroupByDays === 'Daily' ?
<CalendarDataView data={data} t={t} /> : null}
</>
) : (
<div className="flex items-center justify-center h-full min-h-[280px]">
<p>{t('pages.timesheet.NO_ENTRIES_FOUND')}</p>
Expand All @@ -40,7 +49,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
{data?.map((plan, index) => {
return <div key={index}>
<div
className={clsxm(
className={cn(
'h-[40px] flex justify-between items-center w-full',
'bg-[#ffffffcc] dark:bg-dark--theme rounded-md border-1',
'border-gray-400 px-5 text-[#71717A] font-medium'
Expand All @@ -63,18 +72,16 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
rows.length > 0 && status && <AccordionItem
key={status}
value={status === 'DENIED' ? 'REJECTED' : status}
className="p-1 rounded"
>
className="p-1 rounded" >
<AccordionTrigger
type="button"
className={clsxm(
className={cn(
'flex flex-row-reverse justify-end items-center w-full h-[30px] rounded-sm gap-x-2 hover:no-underline px-2',
statusColor(status).text
)}
>
)}>
<div className="flex items-center justify-between space-x-1 w-full">
<div className="flex items-center w-full gap-2">
<div className={clsxm('p-2 rounded', statusColor(status).bg)}></div>
<div className={cn('p-2 rounded', statusColor(status).bg)}></div>
<div className="flex items-center gap-x-1">
<span className="text-base font-normal text-gray-400 uppercase text-[12px]">
{status === 'DENIED' ? 'REJECTED' : status}
Expand All @@ -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]',
)}
>
)}>
<div className="flex px-3 justify-between items-center w-full">
<div className="flex items-center gap-x-1">
<EmployeeAvatar
Expand All @@ -115,10 +121,10 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
</div>
<TaskNameInfoDisplay
task={task.task}
className={clsxm(
className={cn(
'shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent'
)}
taskTitleClassName={clsxm(
taskTitleClassName={cn(
'text-sm text-ellipsis overflow-hidden !text-[#293241] dark:!text-white '
)}
showSize={true}
Expand All @@ -142,3 +148,95 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
</div>
)
}


const MonthlyCalendarDataView = ({ data }: { data: GroupedTimesheet[] }) => {
const { getStatusTimesheet } = useTimesheet({});
return (
<MonthlyTimesheetCalendar data={data}
renderDayContent={(date, plan) => {
return <>
{plan ? (
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
<Accordion type="single" collapsible className="w-full">
{Object.entries(getStatusTimesheet(plan.tasks)).map(([status, rows]) => (
rows.length > 0 && status && <AccordionItem
key={status}
value={status === 'DENIED' ? 'REJECTED' : status}
className="p-1 rounded" >
<AccordionTrigger
type="button"
className={cn(
'flex flex-row-reverse justify-end items-center w-full !h-[20px] rounded-sm gap-x-2 hover:no-underline',
statusColor(status).text
)}>
<div className="flex items-center justify-between space-x-1 w-full">
<div className="flex items-center w-full gap-2">
<div className={cn('p-2 rounded', statusColor(status).bg)}></div>
<div className="flex items-center gap-x-1">
<span className="text-base font-normal text-gray-400 uppercase text-[12px]">
{status === 'DENIED' ? 'REJECTED' : status}
</span>
<span className="text-gray-400 text-[12px]">({rows.length})</span>
</div>
</div>
<div className="flex items-center space-x-2">
<ClockIcon className=' text-[12px] h-3 w-3' />
<TotalTimeDisplay timesheetLog={rows} />
</div>
</div>
</AccordionTrigger>
<AccordionContent className="flex flex-col w-full gap-y-2 overflow-auto">
{rows.map((task) => (
<div
key={task.id}
style={{
backgroundColor: statusColor(status).bgOpacity,
borderLeftColor: statusColor(status).border

}}
className={cn(
'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 h-[100px] !w-full',
)}>
<div className="flex px-3 justify-between items-center w-full">
<div className="flex items-center gap-x-1">
<EmployeeAvatar
imageUrl={task.employee.user.imageUrl ?? ''}
/>
<span className=" font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
</div>
<DisplayTimeForTimesheet
duration={task.timesheet.duration}

/>
</div>
<TaskNameInfoDisplay
task={task.task}
className={cn(
'shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent'
)}
taskTitleClassName={cn(
'text-sm !text-ellipsis !overflow-hidden !truncate !text-[#293241] dark:!text-white '
)}
showSize={true}
dash
taskNumberClassName="text-sm"
/>
<div>
<span className="flex-1">{task.project && task.project.name}</span>
</div>
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<div className="text-gray-400 text-sm flex items-center justify-center min-h-[150px] sm:w-[250px] md:w-[300px] lg:w-[350px] max-w-full gap-2">
<CodeSquareIcon />
<span>No Data</span>
</div>
)}
</>
}} />
)
}
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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<MonthlyCalendarDataViewProps> = ({
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]
);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

const handlePreviousMonth = useCallback(() => setCurrentMonth((prev) => addMonths(prev, -1)), []);
const handleNextMonth = useCallback(() => setCurrentMonth((prev) => addMonths(prev, 1)), []);

return (
<div className={classNames.container || "p-4 w-full"}>
{/* Header */}
<div className={classNames.header || "flex items-center justify-between mb-4"}>
<button
onClick={handlePreviousMonth}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 dark:bg-primary-light hover:dark:bg-primary-light"
>
Previous
</button>
<h2 className="text-xl font-bold">
{format(currentMonth, "MMMM yyyy", { locale: locale })}
</h2>
<button
onClick={handleNextMonth}
className="px-4 py-2 bg-gray-200 dark:bg-primary-light rounded hover:bg-gray-300 hover:dark:bg-primary-light"
>
Next
</button>
</div>

{/* Grid */}
<div className={classNames.grid || "grid grid-cols-7 text-center font-semibold text-gray-600"}>
{daysLabels.map((day) => (
<div key={day}>{day}</div>
))}
</div>
<div className="grid grid-cols-7 mt-2 w-full">
{calendarDates.map((date) => {
const formattedDate = format(date, "yyyy-MM-dd");
const plan = groupedData.get(formattedDate);
return (
<div
key={formattedDate}
className={cn(
classNames.day,
"border flex flex-col gap-2 relative shadow-sm rounded min-h-[150px] sm:w-[250px] md:w-[300px] lg:w-[350px] max-w-full", {
"bg-gray-100 dark:bg-gray-900": date.getMonth() !== currentMonth.getMonth(),
}
)}
onClick={() => onDateClick?.(date)}
>
<div className="px-2 flex items-center justify-between">
<span className="block text-gray-500 text-sm font-medium">
{format(date, "dd MMM yyyy")}
</span>
<div className="flex items-center gap-x-1 text-gray-500 text-sm font-medium">
<span className="text-[#868687]">Total{" : "}</span>
{plan && <TotalDurationByDate
timesheetLog={plan.tasks}
createdAt={formatDate(plan.date)}
className="text-black dark:text-gray-500 text-sm"
/>}
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
{renderDayContent ? (
renderDayContent(date, plan)
) : plan ? (
<div>{JSON.stringify(plan)}</div>
) : (
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
<div className={classNames.noData || "text-gray-400 text-sm"}>
{noDataText}
</div>
)}
</div>
);
})}
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
);
};

export default MonthlyTimesheetCalendar;
1 change: 1 addition & 0 deletions apps/web/lib/features/task/task-displays.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
const targetDateISO = new Date(createdAt).toISOString();
const filteredLogs = timesheetLog.filter(
(item) => formatDate(item.timesheet.createdAt) === formatDate(targetDateISO));
Expand Down
Loading