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
Merged
167 changes: 147 additions & 20 deletions apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,47 @@
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 { EmployeeAvatar, ProjectLogo } 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";
import WeeklyTimesheetCalendar from "./WeeklyTimesheetCalendar";
interface BaseCalendarDataViewProps {
data: GroupedTimesheet[];
daysLabels?: string[];
CalendarComponent: typeof MonthlyTimesheetCalendar | typeof WeeklyTimesheetCalendar;
}

export function CalendarView({ data, loading }: { data?: GroupedTimesheet[], loading: boolean }) {
const t = useTranslations();
const { timesheetGroupByDays } = useTimelogFilterOptions();
const defaultDaysLabels = [
t("common.DAYS.sun"),
t("common.DAYS.mon"),
t("common.DAYS.tue"),
t("common.DAYS.wed"),
t("common.DAYS.thu"),
t("common.DAYS.fri"),
t("common.DAYS.sat")
];
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} daysLabels={defaultDaysLabels} />
) : timesheetGroupByDays === 'Weekly' ? (
<WeeklyCalendarDataView data={data} daysLabels={defaultDaysLabels} />
) : (
<CalendarDataView data={data} t={t} />
)}
</>
) : (
<div className="flex items-center justify-center h-full min-h-[280px]">
<p>{t('pages.timesheet.NO_ENTRIES_FOUND')}</p>
Expand All @@ -40,7 +65,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 +88,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 +120,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,17 +137,18 @@ 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}
dash
taskNumberClassName="text-sm"
/>
<div>
<div className="flex items-center gap-2">
{task.project && <ProjectLogo imageUrl={task.project.imageUrl as string} />}
<span className="flex-1">{task.project && task.project.name}</span>
</div>
</div>
Expand All @@ -142,3 +165,107 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
</div>
)
}

const BaseCalendarDataView = ({ data, daysLabels, CalendarComponent }: BaseCalendarDataViewProps) => {
const { getStatusTimesheet } = useTimesheet({});
return (
<CalendarComponent
data={data}
// locale={ }
daysLabels={daysLabels}
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-[110px] !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 className="flex items-center gap-x-2">
{task.project && <ProjectLogo imageUrl={task.project.imageUrl as string} />}
<span className="flex-1 font-medium">{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>
)}
</>
}}
/>
);
};

const MonthlyCalendarDataView = (props: { data: GroupedTimesheet[], daysLabels?: string[] }) => (
<BaseCalendarDataView {...props} CalendarComponent={MonthlyTimesheetCalendar} />
);

const WeeklyCalendarDataView = (props: { data: GroupedTimesheet[], daysLabels?: string[] }) => (
<BaseCalendarDataView {...props} CalendarComponent={WeeklyTimesheetCalendar} />
);
Original file line number Diff line number Diff line change
@@ -1,27 +1,5 @@
import React from "react";

export const EmployeeAvatar = ({ imageUrl }: { imageUrl: string }) => {
const [isLoading, setIsLoading] = React.useState(true);

return (
<div className="relative w-6 h-6">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-200 rounded-full">
<LoadingSpinner className="w-4 h-4" />
</div>
)}
<img
className="w-6 h-6 rounded-full"
src={imageUrl}
alt="Employee"
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
/>
</div>
);
};


const LoadingSpinner = ({ className }: { className?: string }) => (
<svg
className={`animate-spin text-gray-500 ${className}`}
Expand All @@ -44,3 +22,33 @@ const LoadingSpinner = ({ className }: { className?: string }) => (
></path>
</svg>
);

const ImageWithLoader = ({ imageUrl, alt, className = "w-6 h-6 rounded-full" }:
{ imageUrl: string; alt: string; className?: string }) => {
const [isLoading, setIsLoading] = React.useState(true);
return (
<div className="relative w-6 h-6">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-200 rounded-full">
<LoadingSpinner className="w-4 h-4" />
</div>
)}
<img
className={className}
src={imageUrl}
alt={alt}
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
/>
</div>
);
};

export const EmployeeAvatar = ({ imageUrl }: { imageUrl: string }) => (
<ImageWithLoader imageUrl={imageUrl} alt="Employee" />
);


export const ProjectLogo = ({ imageUrl }: { imageUrl: string }) => (
<ImageWithLoader imageUrl={imageUrl} alt="Project Logo" />
);
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
valueKey: 'id',
displayKey: 'name',
element: 'Project',
defaultValue: dataTimesheet.project.name
defaultValue: 'name'
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

},
];
Expand Down
Loading
Loading