Skip to content

Commit

Permalink
[Feat]: Add Component for Timesheet Creation and Optimize Task Button (
Browse files Browse the repository at this point in the history
…#3359)

* feat: add component for timesheet creation and optimize task button

* feat(timesheet): add grouping by week and month functionality

* fix: simplify and improve readability of conditional rendering in timesheet display

* feat: timesheet creation task buttons

* fix:coderabbitai
  • Loading branch information
Innocent-Akim authored Nov 22, 2024
1 parent bfde5c7 commit 8f6352d
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 65 deletions.
157 changes: 157 additions & 0 deletions apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useTeamTasks } from '@/app/hooks';
import { ITaskIssue } from '@/app/interfaces';
import { clsxm } from '@/app/utils';
import { Modal } from '@/lib/components'
import { CustomSelect, TaskStatus, taskIssues } from '@/lib/features';
import { Item, ManageOrMemberComponent, getNestedValue } from '@/lib/features/manual-time/manage-member-component';
import { useTranslations } from 'next-intl';
import React from 'react'
import { ToggleButton } from './EditTaskModal';
export interface IAddTaskModalProps {
isOpen: boolean;
closeModal: () => void;
}
export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
const t = useTranslations();
const { activeTeam } = useTeamTasks();
const [notes, setNotes] = React.useState('');
const [task, setTasks] = React.useState('')
const [isBillable, setIsBillable] = React.useState<boolean>(true);

const projectItemsLists = {
Project: activeTeam?.projects ?? [],
};

const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => {
// Handle value changes
};
const selectedValues = {
Project: null,
};
const handleChange = (field: string, selectedItem: Item | null) => {
// Handle field changes
};

const fields = [
{
label: t('sidebar.PROJECTS'),
placeholder: 'Select a project',
isRequired: true,
valueKey: 'id',
displayKey: 'name',
element: 'Project'
},
];

return (
<Modal
isOpen={isOpen}
closeModal={closeModal}
title={'ADD TASK'}
showCloseIcon
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-40 md:min-w-[30rem] justify-start h-[auto]"
titleClass="font-bold flex justify-start w-full">
<div className="flex flex-col w-full gap-4 justify-start">
<div className=" w-full mr-[4%]">
<label className="block text-[#282048] font-medium mb-1">
Task
<span className="text-[#de5505e1] ml-1">*</span>
</label>
<input
aria-label="Task"
aria-describedby="start-time-error"
type="Task"
value={task}
onChange={(e) => setTasks(e.target?.value)}
className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md"
required
/>
</div>
<div className=" w-full mr-[4%] flex items-center">
<label className="block text-[#282048] mb-1 px-2">
{t('common.ISSUE_TYPE')}
<span className="text-[#de5505e1] ml-1">*</span>:
</label>
<CustomSelect
className='w-auto'
options={Object.keys(taskIssues).flatMap((items) => items)}
renderOption={(option) => (
<div className="flex items-center gap-x-2">
<TaskStatus
{...taskIssues[option as ITaskIssue]}
showIssueLabels={false}
issueType="issue"
className={clsxm('rounded-md px-2 text-white bg-primary')}
/>
{option}
</div>
)}
/>
</div>
<div className=" flex flex-col items-center w-full">
<label className="text-[#282048] font-medium mr-12 capitalize">
{t('pages.timesheet.BILLABLE.BILLABLE').toLowerCase()
}</label>
<div className="flex items-center gap-3">
<ToggleButton
isActive={isBillable}
onClick={() => setIsBillable(true)}
label={t('pages.timesheet.BILLABLE.YES')}
/>
<ToggleButton
isActive={!isBillable}
onClick={() => setIsBillable(false)}
label={t('pages.timesheet.BILLABLE.NO')}
/>
</div>
</div>
<div className="w-full flex flex-col">
<ManageOrMemberComponent
fields={fields}
itemsLists={projectItemsLists}
selectedValues={selectedValues}
onSelectedValuesChange={handleSelectedValuesChange}
handleChange={handleChange}
itemToString={(item, displayKey) => getNestedValue(item, displayKey) || ''}
itemToValue={(item, valueKey) => getNestedValue(item, valueKey) || ''}
/>
</div>
<div className="w-full flex flex-col">
<span className="text-[#282048] font-medium">Notes</span>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Insert notes here..."
className={clsxm(
"bg-transparent focus:border-transparent focus:ring-2 focus:ring-transparent",
"placeholder-gray-300 placeholder:font-normal resize-none p-2 grow w-full",
"border border-gray-200 dark:border-slate-600 dark:bg-dark--theme-light rounded-md h-40 bg-[#FBB6500D]",
notes.trim().length === 0 && "border-red-500"
)}
maxLength={120}
minLength={0}
aria-label="Insert notes here"
required
/>
<div className="text-sm text-[#282048] font-medium text-right">
{notes.length}/{120}
</div>
</div>
<div className="flex items-center gap-x-2 justify-end w-full">
<button
type="button"
className={clsxm("dark:text-primary h-[2.3rem] w-[5.5rem] border px-2 rounded-lg border-gray-300 dark:border-slate-600 font-normal dark:bg-dark--theme-light")}>
{t('common.CANCEL')}
</button>
<button
type="submit"
className={clsxm(
'bg-[#3826A6] h-[2.3rem] w-[5.5rem] justify-center font-normal flex items-center text-white px-2 rounded-lg',
)}>
{t('common.SAVE')}
</button>
</div>
</div>
</Modal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,47 @@
import React, { HTMLAttributes } from 'react';
import { Button } from 'lib/components';
import { clsxm } from '@app/utils';
import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';

export type FilterStatus = 'All Tasks' | 'Pending' | 'Approved' | 'Rejected';
export type FilterStatus = 'All Tasks' | 'Pending' | 'Approved' | 'In review' | 'Draft' | 'Rejected';
export function FilterWithStatus({
activeStatus,
onToggle,
className
className,
data
}: Readonly<{
activeStatus: FilterStatus;
data?: Record<TimesheetStatus, TimesheetLog[]>

onToggle: (status: FilterStatus) => void;
className?: HTMLAttributes<HTMLDivElement>;
}>) {
const buttonData: { label: FilterStatus; count: number; icon: React.ReactNode }[] = [
{ label: 'All Tasks', count: 46, icon: <i className="icon-all" /> },
{ label: 'Pending', count: 12, icon: <i className="icon-pending" /> },
{ label: 'Approved', count: 28, icon: <i className="icon-approved" /> },
{ label: 'Rejected', count: 6, icon: <i className="icon-rejected" /> }
];

const statusIcons: Record<FilterStatus, string> = {
'All Tasks': 'icon-all',
Pending: 'icon-pending',
Approved: 'icon-approved',
'In review': 'icon-rejected',
Draft: 'icon-approved',
Rejected: 'icon-rejected',
};

const buttonData = React.useMemo(() => {
const counts = {
'All Tasks': Object.values(data ?? {}).reduce((total, tasks) => total + (tasks?.length ?? 0), 0),
Pending: data?.PENDING?.length ?? 0,
Approved: data?.APPROVED?.length ?? 0,
'In review': data?.['IN REVIEW']?.length ?? 0,
Draft: data?.DRAFT?.length ?? 0,
Rejected: data?.DENIED?.length ?? 0,
};
return Object.entries(counts).map(([label, count]) => ({
label: label as FilterStatus,
count,
icon: <i className={statusIcons[label as FilterStatus]} />,
}));
}, [data]);


return (
<div
Expand All @@ -34,7 +58,7 @@ export function FilterWithStatus({
'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]',
activeStatus === label &&
'text-primary bg-white shadow-2xl dark:text-primary-light font-bold border'
'text-primary bg-white shadow-2xl dark:text-primary-light font-bold border'
)}
onClick={() => onToggle(label)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,32 @@ import { DatePicker } from "@components/ui/DatePicker";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { PiCalendarDotsThin } from "react-icons/pi";
import { useTimelogFilterOptions } from "@/app/hooks";
import { TimesheetFilterByDays } from "@/app/interfaces";

interface DatePickerInputProps {
date: Date | null;
label: string;
}

export function FrequencySelect() {
const [selectedValue, setSelectedValue] = React.useState<string | undefined>(undefined);

const { setTimesheetGroupByDays, timesheetGroupByDays } = useTimelogFilterOptions();
const handleSelectChange = (value: string) => {
setSelectedValue(value);
setTimesheetGroupByDays(value as TimesheetFilterByDays);
};

return (
<Select
value={selectedValue}
value={timesheetGroupByDays}
onValueChange={handleSelectChange}>
<SelectTrigger className="w-36 overflow-hidden h-[2.2rem] text-clip border border-gray-200 dark:border-gray-700 bg-white dark:bg-dark--theme-light focus:ring-2 focus:ring-transparent">
<SelectValue placeholder="Select a daily" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="Daily">Daily</SelectItem>
<SelectItem value="Weekly">Weekly</SelectItem>
<SelectItem value="Monthly">Monthly</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TimesheetStatus } from "@/app/interfaces";
import { TimesheetFilterByDays, TimesheetStatus } from "@/app/interfaces";
import { clsxm } from "@/app/utils";
import { TranslationHooks } from "next-intl";
import { ReactNode } from "react";
Expand Down Expand Up @@ -31,6 +31,7 @@ export type StatusType = "Pending" | "Approved" | "Denied";
export type StatusAction = "Deleted" | "Approved" | "Denied";



// eslint-disable-next-line @typescript-eslint/no-empty-function
export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, disabled: boolean, onClick: (action: StatusAction) => void) => {

Expand Down Expand Up @@ -69,3 +70,9 @@ export const statusTable: { label: TimesheetStatus; description: string }[] = [
{ label: "DRAFT", description: "The item is saved as draft" },
{ label: "DENIED", description: "The item has been rejected" },
];

export const DailyTable: { label: TimesheetFilterByDays; description: string }[] = [
{ label: "Daily", description: 'Group by Daily' },
{ label: "Weekly", description: 'Group by Weekly' },
{ label: "Monthly", description: 'Group by Monthly' },
];
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FilterStatus, FilterWithStatus } from './FilterWithStatus';
import { FrequencySelect, TimeSheetFilterPopover, TimesheetFilterDate, TimesheetFilterDateProps } from '.';
import { Button } from 'lib/components';
import { AddManualTimeModal } from '@/lib/features/manual-time/add-manual-time-modal';
import { TranslationHooks } from 'next-intl';
import { AddTaskModal } from './AddTaskModal';
import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';

interface ITimesheetFilter {
isOpen: boolean,
Expand All @@ -11,22 +12,23 @@ interface ITimesheetFilter {
t: TranslationHooks,
initDate?: Pick<TimesheetFilterDateProps, 'initialRange' | 'onChange' | 'maxDate' | 'minDate'>,
onChangeStatus?: (status: FilterStatus) => void;
filterStatus?: FilterStatus
filterStatus?: FilterStatus,
data?: Record<TimesheetStatus, TimesheetLog[]>

}

export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, filterStatus, onChangeStatus }: ITimesheetFilter,) {
export function TimesheetFilter({ closeModal, isOpen, openModal, t, initDate, filterStatus, onChangeStatus, data }: ITimesheetFilter,) {
return (
<>
{
isOpen && <AddManualTimeModal
isOpen && <AddTaskModal
closeModal={closeModal}
isOpen={isOpen}
params="AddManuelTime"
timeSheetStatus="ManagerTimesheet"
/>}
<div className="flex w-full justify-between items-center">
<div>
<FilterWithStatus
data={data}
activeStatus={filterStatus || "All Tasks"}
onToggle={(label) => onChangeStatus?.(label)}
/>
Expand Down
37 changes: 21 additions & 16 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,30 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
from: startOfDay(new Date()),
to: endOfDay(new Date())
});

const { timesheet } = useTimesheet({
const { timesheet, statusTimesheet } = useTimesheet({
startDate: dateRange.from ?? '',
endDate: dateRange.to ?? ''
});

const lowerCaseSearch = useMemo(() => search?.toLowerCase() ?? '', [search]);
const filterDataTimesheet = useMemo(
() =>
timesheet.filter((v) =>
v.tasks.some(
(task) =>
task.task?.title?.toLowerCase()?.includes(lowerCaseSearch) ||
task.employee?.fullName?.toLowerCase()?.includes(lowerCaseSearch) ||
task.project?.name?.toLowerCase()?.includes(lowerCaseSearch)
)
),
[timesheet, lowerCaseSearch]
);
const filterDataTimesheet = useMemo(() => {
const filteredTimesheet =
timesheet
.filter((v) =>
v.tasks.some(
(task) =>
task.task?.title?.toLowerCase()?.includes(lowerCaseSearch) ||
task.employee?.fullName?.toLowerCase()?.includes(lowerCaseSearch) ||
task.project?.name?.toLowerCase()?.includes(lowerCaseSearch)
)
);

return filteredTimesheet;
}, [
timesheet,
lowerCaseSearch,
]);


const {
isOpen: isManualTimeModalOpen,
Expand Down Expand Up @@ -166,8 +171,8 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
/>
</div>
</div>

<TimesheetFilter
data={statusTimesheet}
onChangeStatus={setFilterStatus}
filterStatus={filterStatus}
initDate={{
Expand Down Expand Up @@ -210,7 +215,7 @@ const ViewToggleButton: React.FC<ViewToggleButtonProps> = ({ mode, active, icon,
className={clsxm(
'text-[#7E7991] font-medium w-[191px] h-[40px] flex items-center gap-x-4 text-[14px] px-2 rounded',
active &&
'border-b-primary text-primary border-b-2 dark:text-primary-light dark:border-b-primary-light bg-[#F1F5F9] dark:bg-gray-800 font-bold'
'border-b-primary text-primary border-b-2 dark:text-primary-light dark:border-b-primary-light bg-[#F1F5F9] dark:bg-gray-800 font-bold'
)}
>
{icon}
Expand Down
Loading

0 comments on commit 8f6352d

Please sign in to comment.