Skip to content

Commit

Permalink
[Feature] Add manual time popup page (#2715)
Browse files Browse the repository at this point in the history
* [Feature]create a form to add time

* remove undefined object error

* Add key to options

* Check if null

* Replace the Add time button

* Place the button between filter and Update the deign of the modal

* Delete unused var

* Change unfecognised word

* Improve the design

* Add reusable time picker component

* Implement the custom time picker and add a gap of 10min

* Update custom date picker

* Update style and fix bug when selecting 0h

* Fix bug and update styling

* Install time picker

* Install time picker

* Remove TimeKeeper

---------

Co-authored-by: cedric karungu <[email protected]>
  • Loading branch information
GedeonTS and Cedric921 authored Jul 17, 2024
1 parent ef94bf0 commit ba9477c
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 10 deletions.
6 changes: 1 addition & 5 deletions apps/web/app/[locale]/profile/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { TaskFilter, Timer, TimerStatus, UserProfileTask, getTimerStatusValue, u
import { MainHeader, MainLayout } from 'lib/layout';
import Link from 'next/link';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useTranslations } from 'next-intl'
import stc from 'string-to-color';

import { useRecoilValue, useSetRecoilState } from 'recoil';
Expand Down Expand Up @@ -126,7 +126,6 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId
{/* User Profile Detail */}
<div className="flex flex-col items-center justify-between py-5 md:py-10 md:flex-row">
<UserProfileDetail member={profile.member} />

{profileIsAuthUser && isTrackingEnabled && (
<Timer
className={clsxm(
Expand All @@ -142,9 +141,6 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId
</MainHeader>
</ResizablePanel>
<ResizableHandle withHandle />

{/* Divider */}
{/* <div className="h-0.5 bg-[#FFFFFF14]"></div> */}
<ResizablePanel defaultSize={53} maxSize={95} className="!overflow-y-scroll custom-scrollbar">
{hook.tab == 'worked' && canSeeActivity && (
<Container fullWidth={fullWidth} className="py-8">
Expand Down
271 changes: 266 additions & 5 deletions apps/web/lib/features/task/task-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,35 @@ import {
useAuthenticateUser,
useDailyPlan,
useOrganizationTeams,
useOutsideClick
useOutsideClick,
useModal,
useTeamTasks
} from '@app/hooks';
import { IClassName, ITeamTask } from '@app/interfaces';
import { clsxm } from '@app/utils';
import { Transition } from '@headlessui/react';
import { Button, InputField, Tooltip, VerticalSeparator } from 'lib/components';
import { SearchNormalIcon } from 'assets/svg';
import intersection from 'lodash/intersection';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState, FormEvent } from 'react';
import { TaskUnOrAssignPopover } from './task-assign-popover';
import { TaskLabelsDropdown, TaskPropertiesDropdown, TaskSizesDropdown, TaskStatusDropdown } from './task-status';
import { useTranslations } from 'next-intl';
import { SettingFilterIcon } from 'assets/svg';
import { DailyPlanFilter } from './daily-plan/daily-plan-filter';
import { Modal, Divider } from 'lib/components';
import api from '@app/services/client/axios';
import { MdOutlineMoreTime } from "react-icons/md";
import { IoIosTimer } from "react-icons/io";
import { FiLoader } from "react-icons/fi";
import { DatePicker } from '@components/ui/DatePicker';
import { PencilSquareIcon } from '@heroicons/react/20/solid';
import { FaRegCalendarAlt } from "react-icons/fa";
import { useDateRange } from '@app/hooks/useDateRange';
import { TaskDatePickerWithRange } from './task-date-range';

export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan';

type ITabs = {
tab: ITab;
name: string;
Expand Down Expand Up @@ -263,6 +274,105 @@ export function TaskFilter({ className, hook, profile }: IClassName & Props) {
function InputFilters({ hook, profile }: Props) {
const t = useTranslations();
const [loading, setLoading] = useState(false);
const { tasks } = useTeamTasks();
const { activeTeam } = useOrganizationTeams();
const members = activeTeam?.members;

const [date, setDate] = useState<string>('');
const [isBillable, setIsBillable] = useState<boolean>(false);
const [startTime, setStartTime] = useState<string>('');
const [endTime, setEndTime] = useState<string>('');
const [team, setTeam] = useState<string>('');
const [task, setTask] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [reason, setReason] = useState<string>('');
const [timeDifference, setTimeDifference] = useState<string>('');
const [errorMsg, setError] = useState<string>('');
const [loading1, setLoading1] = useState<boolean>(false);

const { isOpen, openModal, closeModal } = useModal();

useEffect(() => {
const now = new Date();
const currentDate = now.toISOString().slice(0, 10);
const currentTime = now.toTimeString().slice(0, 5);

setDate(currentDate);
setStartTime(currentTime);
setEndTime(currentTime);
}, []);


const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

const timeObject = {
date,
isBillable,
startTime,
endTime,
team,
task,
description,
reason,
timeDifference
};

if (date && startTime && endTime && team && task) {
setLoading1(true);
setError('');
const postData = async () => {
try {
const response = await api.post('/add_time', timeObject);
if (response.data.message) {
setLoading1(false);
closeModal();
}

} catch (err) {
setError('Failed to post data');
setLoading1(false);
}
};

postData();
} else {
setError(`Please complete all required fields with a ${"*"}`)
}
};

const calculateTimeDifference = () => {

const [startHours, startMinutes] = startTime.split(':').map(Number);
const [endHours, endMinutes] = endTime.split(':').map(Number);

const startTotalMinutes = startHours * 60 + startMinutes;
const endTotalMinutes = endHours * 60 + endMinutes;

const diffMinutes = endTotalMinutes - startTotalMinutes;
if (diffMinutes < 0) {
return;
}

const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
setTimeDifference(`${hours} Hours ${minutes} Minutes`);
};

useEffect(() => {
calculateTimeDifference();
}, [endTime, startTime]);

useEffect(() => {
if (task == '') {
setTask(tasks[0]?.id);
}
if (team == '') {
members && setTeam(members[0].id);
}

}, [tasks, members]);

const osSpecificAssignTaskTooltipLabel = 'A';

return (
Expand Down Expand Up @@ -294,6 +404,12 @@ function InputFilters({ hook, profile }: Props) {
<SettingFilterIcon className="dark:text-white w-3.5" strokeWidth="1.8" />
<span>{t('common.FILTER')}</span>
</button>
<button
onClick={() => openModal()}
className="p-[10px] text-white rounded-[12px] min-w-[200px] border-[1px] text-[15px] flex items-center bg-[#3826A6]"
>
<MdOutlineMoreTime size={20} className="mr-[10px]" />{"Add manual time"}
</button>

{/* Assign task combobox */}
<TaskUnOrAssignPopover
Expand All @@ -313,14 +429,160 @@ function InputFilters({ hook, profile }: Props) {
loading={loading}
className={clsxm(
'dark:bg-gradient-to-tl dark:from-regal-rose dark:to-regal-blue h-full px-4 py-3 rounded-xl text-base',
'min-w-[11.25rem] h-[2.75rem]'
'min-w-[8.25rem] h-[2.75rem]'
)}
>
{t('common.ASSIGN_TASK')}
</Button>
</Tooltip>
</TaskUnOrAssignPopover>
</div>
<div>
<Modal
isOpen={isOpen}
closeModal={closeModal}
title={'Add time'}
className="bg-light--theme-light dark:bg-dark--theme-light py-5 rounded-xl w-full md:min-w-[20vw] md:max-w-fit h-[auto] justify-start"
titleClass="text-[16px] font-bold"
>
<Divider className="mt-4" />
<form onSubmit={handleSubmit} className="text-[13px] w-[90%]">
<div className="mb-4">
<label className="block text-gray-700 mb-1">Date<span className="text-[#de5505e1] ml-1">*</span></label>
<div className="w-full p-2 border border-gray-300 rounded-[10px]">
<DatePicker
buttonVariant={'link'}
buttonClassName={'py-3 decoration-transparent h-[0.875rem] w-full flex items-center'}
customInput={
<div
className={clsxm(
'not-italic cursor-pointer font-semibold text-[0.625rem] 3xl:text-xs w-full',
'leading-[140%] tracking-[-0.02em] text-[#282048] dark:text-white w-full'
)}
>
{
date ?
<div className="flex items-center w-full">
<FaRegCalendarAlt size={20} fill={"#13648fa9"} className="mx-[20px]" />
{date}
</div>
: (
<PencilSquareIcon className="dark:text-white text-dark w-4 h-4" />
)}
</div>
}
selected={new Date()}
onSelect={(dateI) => {
dateI && setDate(dateI.toDateString());
}}
mode={'single'}
/>
</div>
</div>

<div className="mb-4 flex items-center">
<label className="block text-gray-700 mr-2">Billable</label>
<div
className={`w-12 h-6 flex items-center bg-[#3726a662] rounded-full p-1 cursor-pointer `}
onClick={() => setIsBillable(!isBillable)}
style={isBillable ? { background: 'linear-gradient(to right, #3726a662, transparent)' } : { background: '#3726a662' }}
>
<div
className={`bg-[#3826A6] w-4 h-4 rounded-full shadow-md transform transition-transform ${isBillable ? 'translate-x-6' : 'translate-x-0'}`}
/>
</div>
</div>
<div className='flex items-center'>
<div className="mb-4 w-[48%] mr-[4%]">
<label className="block text-gray-700 mb-1">Start time<span className="text-[#de5505e1] ml-1">*</span></label>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]"
required
/>
</div>

<div className="mb-4 w-[48%]"><label className="block text-gray-700 mb-1">End time<span className="text-[#de5505e1] ml-1">*</span></label>

<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]"
required
/>
</div>
</div>

<div className="mb-4 flex items-center">
<label className="block text-gray-700 mb-1">Added hours: </label>
<div
className="ml-[10px] p-[10px] flex items-center font-bold border-[#410a504e] rounded-[10px]"
style={{ background: 'linear-gradient(to right, #0085c8a4, #410a504e )' }}
>
<IoIosTimer size={20} className="mr-[10px]" />
{timeDifference}
</div>
</div>

<div className="mb-4">
<label className="block text-gray-700 mb-1">Team<span className="text-[#de5505e1] ml-1">*</span></label>
<select
value={team}
onChange={(e) => setTeam(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-[10px] font-bold"
>
{members?.map((member) => (
<option key={member.id} value={member.id}>{member.employee?.user?.firstName}</option>))
}
</select>
</div>

<div className="mb-4">
<label className="block text-gray-700 mb-1">Task<span className="text-[#de5505e1] ml-1">*</span></label>
<select
value={task}
onChange={(e) => setTask(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-[10px] font-bold"
>
{tasks?.map((task) => (
<option key={task.id} value={task.id}>{task.title}</option>
))}
</select>
</div>

<div className="mb-4">
<label className="block text-gray-700 mb-1">Description</label>
<textarea
value={description}
placeholder="What worked on?"
onChange={(e) => setDescription(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-[10px]"
/>
</div>

<div className="mb-4">
<label className="block text-gray-700 mb-1">Reason</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-[10px]"
/>
</div>

<div className="flex justify-between items-center">
<button type="button" className="text-[#3826A6] font-bold p-[12px] rounded-[10px] border-[1px] bo">View timesheet</button>
<button type="submit" className="bg-[#3826A6] font-bold min-w-[110px] flex flex-col items-center text-white p-[12px] rounded-[10px]">
{loading1 ? <FiLoader size={20} className="animate-spin" /> : "Add time"}
</button>
</div>
<div className="m-4 text-[#ff6a00de]">{errorMsg}</div>

</form>
</Modal>
</div>
</div >
);
}

Expand Down Expand Up @@ -364,7 +626,6 @@ function TabsNav({ hook }: { hook: I_TaskFilter }) {
</nav>
);
}

/**
* It renders a divider, a div with a flexbox layout, and filters buttons
* @returns A React component
Expand Down

0 comments on commit ba9477c

Please sign in to comment.