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]: Timesheet Calendar View UI #3369

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,142 @@
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 { TranslationHooks, useTranslations } from "next-intl";
import React from "react";
import { EmployeeAvatar } from "./CompactTimesheetComponent";
import { formatDate } from "@/app/helpers";
import { ClockIcon } from "lucide-react";

export function CalendarView() {
export function CalendarView({ data }: { data?: GroupedTimesheet[] }) {
const t = useTranslations();
return (
<div className='grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme'>
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">
{data ? (
data.length > 0 ? (
<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>
</div>
)
) : (
<div className="flex items-center justify-center h-full">
<p>{t('pages.timesheet.LOADING')}</p>
</div>
)}
</div>
);
}

const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: TranslationHooks }) => {
const { getStatusTimesheet } = useTimesheet({});

return (
<div className="w-full dark:bg-dark--theme">
<div className="rounded-md">
{data?.map((plan, index) => {
return <div key={index}>
<div
className={clsxm(
'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'
)}>
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
<div className='flex gap-x-3'>
<span>{formatDate(plan.date)}</span>
</div>
<div className="flex items-center gap-x-1">
<span className="text-[#868687]">Total{" : "}</span>

<TotalDurationByDate
timesheetLog={plan.tasks}
createdAt={formatDate(plan.date)}
className="text-black dark:text-gray-500 text-sm"
/>
</div>
</div>
<Accordion type="single" collapsible>
{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={clsxm(
'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="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-[14px]">({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">
{rows.map((task) => (
<div
key={task.id}
style={{
backgroundColor: statusColor(status).bgOpacity,
borderLeftColor: statusColor(status).border

}}
className={clsxm(
'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
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={clsxm(
'shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent'
)}
taskTitleClassName={clsxm(
'text-sm text-ellipsis overflow-hidden !text-[#293241] dark:!text-white '
)}
showSize={true}
dash
taskNumberClassName="text-sm"
/>
<span className="flex-1">{task.project && task.project.name}</span>
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
}

)}
</div>

</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react";

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

Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="relative w-6 h-6">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-200 rounded-full">
<svg
className="w-4 h-4 animate-spin text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
></path>
</svg>
</div>
)}
<img
className="w-6 h-6 rounded-full"
src={imageUrl}
alt="Employee"
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
/>
</div>
);
};
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './TimeSheetFilterPopover'
export * from './TimesheetAction';
export * from './RejectSelectedModal';
export * from './EditTaskModal';
export * from './CompactTimesheetComponent'
2 changes: 1 addition & 1 deletion apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
{timesheetNavigator === 'ListView' ? (
<TimesheetView data={filterDataTimesheet} />
) : (
<CalendarView />
<CalendarView data={filterDataTimesheet} />
)}
</div>
</Container>
Expand Down
7 changes: 7 additions & 0 deletions apps/web/lib/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
export type IVariant = 'primary' | 'outline' | 'ghost' | 'light' | 'dark';
type StatusColorScheme = {
bg: string;
border: string,
text: string;
bgOpacity: string;
};

const STATUS_COLORS: Record<string, StatusColorScheme> = {
PENDING: {
bg: 'bg-[#FBB650]',
border: 'rgb(251, 182, 80)',
text: 'text-[#FBB650]',
bgOpacity: 'rgba(251, 182, 80, 0.1)',
},
APPROVED: {
bg: 'bg-[#30B366]',
border: 'rgba(48, 179, 102)',
text: 'text-[#30B366]',
bgOpacity: 'rgba(48, 179, 102, 0.1)',
},
DENIED: {
bg: 'bg-[#dc2626]',
border: 'rgba(220, 38, 38)',
text: 'text-[#dc2626]',
bgOpacity: 'rgba(220, 38, 38, 0.1)',
},
DRAFT: {
bg: 'bg-gray-300',
border: 'rgba(220, 220, 220)',
text: 'text-gray-500',
bgOpacity: 'rgba(220, 220, 220, 0.1)',
},
'IN REVIEW': {
bg: 'bg-blue-500',
border: 'rgba(59, 130, 246)',
text: 'text-blue-500',
bgOpacity: 'rgba(59, 130, 246, 0.1)',
},
DEFAULT: {
bg: 'bg-gray-100',
border: 'rgba(243, 244, 246)',
text: 'text-gray-400',
bgOpacity: 'rgba(243, 244, 246, 0.1)',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
RejectSelectedModal,
StatusAction,
StatusType,
EmployeeAvatar,
getTimesheetButtons,
statusTable
} from '@/app/[locale]/timesheet/[memberId]/components';
Expand Down Expand Up @@ -324,10 +325,8 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
</div>
<span className="flex-1">{task.project && task.project.name}</span>
<div className="flex items-center flex-1 gap-x-2">
<img
className="w-8 h-8 rounded-full"
src={task.employee.user.imageUrl!}
alt=""
<EmployeeAvatar
imageUrl={task.employee.user.imageUrl!}
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
/>
<span className="flex-1 font-medium">{task.employee.fullName}</span>
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/lib/features/task/task-displays.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const DisplayTimeForTimesheet = ({ duration }: { duration: number }) => {
return (
<div className='flex items-center font-medium gap-x-1'>
<ClockIcon className='text-green-400 text-[14px] h-4 w-4' />
<div className='flex items-center'>
<div className='flex items-center text-[#282048] dark:text-[#9b8ae1]'>
{formatTime(hours, minute)}
</div>
</div>
Expand All @@ -104,15 +104,15 @@ TotalTimeDisplay.displayName = 'TotalTimeDisplay';


export const TotalDurationByDate = React.memo(
({ timesheetLog, createdAt }: { timesheetLog: TimesheetLog[]; createdAt: Date | string }) => {
({ timesheetLog, createdAt, className }: { timesheetLog: TimesheetLog[]; createdAt: Date | string, className?: string }) => {
const targetDateISO = new Date(createdAt).toISOString();
const filteredLogs = timesheetLog.filter(
(item) => formatDate(item.timesheet.createdAt) === formatDate(targetDateISO));
const totalDurationInSeconds = filteredLogs.reduce(
(total, log) => total + (log.timesheet?.duration || 0), 0);
const { h: hours, m: minutes } = secondsToTime(totalDurationInSeconds);
return (
<div className="flex items-center text-[#868688]">
<div className={clsxm("flex items-center text-[#868688]", className)}>
{formatTime(hours, minutes)}
</div>
);
Expand Down