Skip to content

Commit

Permalink
[Feat]: Add Icons For Member work men hours pending task (#3460)
Browse files Browse the repository at this point in the history
* Add SVG icons for Member Work, Men Hours, Pending Task, List View, Calendar View, and Filter

* fix: some bugs on the timesheet

* fix: resolve TypeScript error for grouping timesheet details by employeeId

* fix: some bugs on the timesheet

* fix: deepscan

* fix: coderabbit

* fix:ticket total hours

* fix: deepscan

* fix:ticket total hours

* fix:ticket total hours

* feat: add modal for timesheet details of members work

* feat: image with Loader

* fix:code coderabbitai

* fix: css avatar employee

* fix: timesheet data overflow in Calendar View and View Details button functionality

* fix: coderabbitai
  • Loading branch information
Innocent-Akim authored Dec 23, 2024
1 parent 7f69d43 commit a53760c
Show file tree
Hide file tree
Showing 18 changed files with 722 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
<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-[14px]">
<span className="text-base font-medium text-[#71717A] uppercase !text-[14px]">
{status === 'DENIED' ? 'REJECTED' : status}
</span>
<span className="text-gray-400 text-[14px]">({rows.length})</span>
Expand Down Expand Up @@ -144,7 +144,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
<span className=" font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
</div>
<DisplayTimeForTimesheet
duration={task.timesheet.duration}
timesheetLog={task}

/>
</div>
Expand Down Expand Up @@ -218,7 +218,7 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa
</div>
</div>
</AccordionTrigger>
<AccordionContent className="flex flex-col w-full gap-y-2 overflow-auto">
<AccordionContent className="flex flex-col gap-y-2 overflow-auto items-start p-0 flex-none order-1 flex-grow-0">
{rows.map((task) => (
<div
key={task.id}
Expand All @@ -228,35 +228,43 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa

}}
className={cn(
'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 h-[110px] !w-full',
'border-l-4 rounded-l space-x-4 box-border flex flex-col items-start py-2.5 gap-2 w-[258px] rounded-tr-md rounded-br-md flex-none order-1 self-stretch flex-grow',
)}>
<div className="flex px-3 justify-between items-center w-full">
<div className="flex pl-3 justify-between items-center w-full">
<div className="flex items-center gap-x-1">
<EmployeeAvatar
className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-full"
imageUrl={task.employee.user.imageUrl ?? ''}
/>
<span className=" font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
<span className="font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
</div>
<DisplayTimeForTimesheet
duration={task.timesheet.duration}
timesheetLog={task}

/>
</div>
<TaskNameInfoDisplay
task={task.task}
className={cn(
'rounded-sm h-auto !px-[0.3312rem] py-[0.2875rem] shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent'
'rounded-sm h-auto !px-[0.3312rem] py-[0.2875rem] shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent '
)}
taskTitleClassName={cn(
'text-sm !text-ellipsis !overflow-hidden !truncate !text-[#293241] dark:!text-white '
'text-sm !text-ellipsis overflow-hidden !truncate !text-[#293241] dark:!text-white'
)}
showSize={true}
showSize={false}
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 className="flex flex-row items-center py-0 gap-2 flex-none order-2 self-stretch flex-grow-0">
{task.project?.imageUrl && (
<ProjectLogo
className="w-[28px] h-[28px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.15)] rounded-[8px]"
imageUrl={task.project.imageUrl}
/>
)}
<span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1">
{task.project?.name ?? 'No Project'}
</span>
</div>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ 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">
<div className="relative">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-200 rounded-full">
<LoadingSpinner className="w-5 h-5" />
Expand All @@ -44,11 +44,11 @@ const ImageWithLoader = ({ imageUrl, alt, className = "w-6 h-6 rounded-full" }:
);
};

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


export const ProjectLogo = ({ imageUrl }: { imageUrl: string }) => (
<ImageWithLoader imageUrl={imageUrl} alt="Project Logo" />
export const ProjectLogo = ({ imageUrl, className }: { imageUrl: string, className?: string }) => (
<ImageWithLoader className={className} imageUrl={imageUrl} alt="Project Logo" />
);
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ export function FilterWithStatus({
return (
<div
className={clsxm(
'flex flex-nowrap h-[2.2rem] items-center bg-[#e2e8f0aa] dark:bg-gray-800 rounded-xl ',
'flex flex-nowrap h-[36px] items-center bg-[#e2e8f0aa] dark:bg-gray-800 rounded-[8px] border-[1px] ',
className
)}>
{buttonData.map(({ label, count, icon }, index) => (
<Button
key={index}
className={clsxm(
'group flex items-center justify-start h-[2.2rem] rounded-xl w-full',
'dark:bg-gray-800 dark:border-primary-light bg-transparent text-[#71717A] w-[80px]',
'group flex items-center justify-start h-[36px] rounded-[8px] w-[111px]',
'dark:bg-gray-800 dark:border-primary-light bg-transparent text-[#71717A]',
activeStatus === label &&
'text-primary bg-white shadow-2xl dark:text-primary-light font-bold border'
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ type MonthlyCalendarDataViewProps = {

const defaultDaysLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

/**
* Generates an array of dates for a full month calendar.
*
* If the given month is February and it is a leap year, the month end date is set to the 29th.
* Otherwise, the month end date is set to the 28th.
*
* The start date is set to the first day of the month minus the day of the week of the first day of the month.
* The end date is set to the last day of the month plus 6 minus the day of the week of the last day of the month.
*
* @param currentMonth The current month to generate the full calendar for.
* @returns An array of dates for a full month calendar.
*/
const generateFullCalendar = (currentMonth: Date) => {
const monthStart = startOfMonth(currentMonth);
const monthEnd = (() => {
Expand All @@ -42,6 +54,24 @@ const generateFullCalendar = (currentMonth: Date) => {
};


/**
* A monthly calendar component for displaying timesheet data.
*
* The component is a grid of days in the month, with each day displaying the total duration of the tasks for that day.
* The component is also responsive and can be used in a variety of screen sizes.
*
* @param {MonthlyCalendarDataViewProps} props - The props for the component.
* @param {GroupedTimesheet[]} [props.data=[]] - The data to display in the calendar.
* @param {((date: Date) => void)} [props.onDateClick] - The function to call when a date is clicked.
* @param {((date: Date, plan?: GroupedTimesheet) => React.ReactNode)} [props.renderDayContent] - The function to call to render the content for each day.
* @param {Locale} [props.locale=enGB] - The locale to use for the dates.
* @param {string[]} [props.daysLabels=defaultDaysLabels] - The labels for the days of the week.
* @param {string} [props.noDataText="No Data"] - The text to display when there is no data for a day.
* @param {{ container?: string; header?: string; grid?: string; day?: string; noData?: string; }} [props.classNames={}] - The CSS class names to use for the component.
* @param {TranslationHooks} props.t - The translations to use for the component.
*
* @returns {React.ReactElement} The JSX element for the component.
*/
const MonthlyTimesheetCalendar: React.FC<MonthlyCalendarDataViewProps> = ({
data = [],
onDateClick,
Expand Down Expand Up @@ -123,7 +153,7 @@ const MonthlyTimesheetCalendar: React.FC<MonthlyCalendarDataViewProps> = ({
{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>
{/* <span className="text-[#868687]">Total{" : "}</span> */}
{plan && <TotalDurationByDate
timesheetLog={plan.tasks}
createdAt={formatDate(plan.date)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ export const TimeSheetFilterPopover = React.memo(function TimeSheetFilterPopover
return Object.values(statusTimesheet).reduce((sum, status) => sum + status.length, 0);
}, [statusTimesheet]);

const totalFilteredItems = React.useMemo(() => {
let total = 0;
if (employee?.length) total += employee.length;
if (project?.length) total += project.length;
if (task?.length) total += task.length;
if (statusState?.length) total += statusState.length;
return total;
}, [employee, project, task, statusState]);

const [filteredCount, setFilteredCount] = React.useState(0);

React.useEffect(() => {
if (timesheet && statusTimesheet) {
let filteredResults = timesheet;
if (employee?.length > 0) {
filteredResults = filteredResults.filter((item) =>
employee.some((emp) => emp.employeeId === item.tasks[0]?.employee.id)
);
}
if (project?.length > 0) {
filteredResults = filteredResults.filter((item) =>
project.some((proj) => proj.id === item.tasks[0]?.projectId)
);
}
if (task?.length > 0) {
filteredResults = filteredResults.filter((item) =>
task.some((t) => t.id === item.tasks[0]?.taskId)
);
}
if (statusState?.length > 0) {
filteredResults = filteredResults.filter((item) =>
statusState.some((status) => status.label === item.tasks[0]?.timesheet.status)
);
}
setFilteredCount(filteredResults.length);
}
}, [timesheet, employee, project, task, statusState, statusTimesheet]);

return (
<>
<Popover>
Expand All @@ -41,7 +79,7 @@ export const TimeSheetFilterPopover = React.memo(function TimeSheetFilterPopover
>
<SettingFilterIcon className="text-gray-700 dark:text-white w-3.5" strokeWidth="1.8" />
<span className="text-gray-700 dark:text-white">{t('common.FILTER')}</span>
{timesheet && timesheet.length > 0 && (
{filteredCount && totalFilteredItems > 0 && (
<span
role="status"
aria-label={`${totalItems} items filtered`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { TimesheetFilterByDays, TimesheetStatus } from "@/app/interfaces";
import { clsxm } from "@/app/utils";
import { TranslationHooks } from "next-intl";
import { ReactNode } from "react";
import { FaClipboardCheck } from "react-icons/fa6";
import { IoClose } from "react-icons/io5";
import { RiDeleteBin6Fill } from "react-icons/ri";
import { ApproveSelectedIcon, DeleteSelectedIcon, RejectSelectedIcon } from "./TimesheetIcons";
type ITimesheetButton = {
title?: string,
onClick?: () => void,
Expand Down Expand Up @@ -37,17 +35,17 @@ export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, dis

const buttonsConfig: Record<StatusType, { icon: JSX.Element; title: string; action: StatusAction }[]> = {
PENDING: [
{ icon: <FaClipboardCheck className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
{ icon: <ApproveSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <RejectSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" },
{ icon: <DeleteSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
],
APPROVED: [
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
{ icon: <RejectSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" },
{ icon: <DeleteSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
],
Denied: [
{ icon: <FaClipboardCheck className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
{ icon: <ApproveSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <DeleteSelectedIcon className="dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
]
};

Expand Down
Loading

0 comments on commit a53760c

Please sign in to comment.