Skip to content

Commit

Permalink
[Fix]: Handle Timesheet Update (#3384)
Browse files Browse the repository at this point in the history
* Fix: handle timesheet update with precise time ranges and billing information

* Fix: deepscan

* Fix: deepscan

* Fix: deepscan

* fix: deep scan

* fix: deep scan
  • Loading branch information
Innocent-Akim authored Nov 30, 2024
1 parent 5062e7c commit 9caa7b9
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
closeModal={closeModal}
title={'+ Add Time Entry'}
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] overflow-y-auto"
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-40 md:min-w-[32rem] justify-start h-[auto]"
titleClass="font-bold flex justify-start w-full">
<div className="flex flex-col w-full gap-4 justify-start overflow-y-auto">
<div className="flex flex-col w-full gap-4 justify-start md:w-40 md:min-w-[32rem] p-4">
<div className=" w-full mr-[4%]">
<label className="block text-[#282048] dark:text-gray-400 font-medium mb-1">
{t('sidebar.TASKS')}
Expand Down Expand Up @@ -207,7 +207,7 @@ const ShiftTimingSelect = ({ label, timeOptions, placeholder, className, onChang
<SelectItem
key={time}
value={time}
className="hover:bg-primary focus:bg-primary hover:text-white px-2 py-1 cursor-pointer"
className="hover:bg-primary focus:bg-primary hover:text-white py-1 cursor-pointer"
>
{time}
</SelectItem>
Expand All @@ -232,27 +232,31 @@ const OptimizedAccordion = ({ dateRange, handleFromChange, timeOptions, t }: {
const [shifts, setShifts] = React.useState<Shift[]>([
{ startTime: '', endTime: '', totalHours: '00:00h', dateFrom: new Date() },
])
const convertToMinutes = (time: string): number => {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
};

const calculateTotalHours = React.useCallback((start: string, end: string): string => {
if (!start || !end) return '00:00h';

const startMinutes = convertToMinutes(start);
const endMinutes = convertToMinutes(end);
const convertToMinutesHour = (time: string): number => {
const [hourMinute, period] = time.split(' ');
const [hours, minutes] = hourMinute.split(':').map(Number);

const totalMinutes = endMinutes >= startMinutes
? endMinutes - startMinutes
: 1440 - startMinutes + endMinutes;

const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}h`;
}, []);
let totalMinutes = (hours % 12) * 60 + minutes;
if (period === 'PM') totalMinutes += 720;

return totalMinutes;
}

const calculateTotalHoursHour = React.useCallback(
(start: string, end: string): string => {
if (!start || !end) return '00:00h';
const startMinutes = convertToMinutesHour(start);
const endMinutes = convertToMinutesHour(end);
const totalMinutes = endMinutes >= startMinutes
? endMinutes - startMinutes
: 1440 - startMinutes + endMinutes;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}h`;
},
[]
);

const handleAddShift = () => {
setShifts([...shifts,
Expand All @@ -264,26 +268,38 @@ const OptimizedAccordion = ({ dateRange, handleFromChange, timeOptions, t }: {
setShifts(updatedShifts);
};


const handleShiftChange = (index: number, field: keyof Shift, value: string) => {
const updatedShifts = [...shifts];
updatedShifts[index][field] = value;

if (field === 'startTime' || field === 'endTime') {
const { startTime, endTime } = updatedShifts[index];
updatedShifts[index].totalHours = calculateTotalHours(startTime, endTime);

if (!startTime || !endTime) return;

if (convertToMinutesHour(startTime) >= convertToMinutesHour(endTime)) {
return;
}
updatedShifts[index].totalHours = calculateTotalHoursHour(startTime, endTime);
const isOverlapping = shifts.some((shift, i) => {
if (i === index || !shift.startTime || !shift.endTime) return false;
const currentStart = convertToMinutes(startTime);
const currentEnd = convertToMinutes(endTime);
const shiftStart = convertToMinutes(shift.startTime);
const shiftEnd = convertToMinutes(shift.endTime);
return (currentStart >= shiftStart && currentStart < shiftEnd) ||
(currentEnd > shiftStart && currentEnd <= shiftEnd);

const currentStart = convertToMinutesHour(startTime);
const currentEnd = convertToMinutesHour(endTime);
const shiftStart = convertToMinutesHour(shift.startTime);
const shiftEnd = convertToMinutesHour(shift.endTime);
return (
(currentStart < shiftEnd && currentEnd > shiftStart) ||
(currentStart === shiftStart && currentEnd === shiftEnd)
);
});

if (isOverlapping) {
return;
}
}

setShifts(updatedShifts);
};

Expand Down
152 changes: 110 additions & 42 deletions apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { Modal, statusColor } from "@/lib/components";
import { IoMdArrowDropdown } from "react-icons/io";
import { FaRegClock } from "react-icons/fa";
import { DatePickerFilter } from "./TimesheetFilterDate";
import { useState } from "react";
import { FormEvent, useCallback, useState } from "react";
import { useTranslations } from "next-intl";
import { clsxm } from "@/app/utils";
import { Item, ManageOrMemberComponent, getNestedValue } from "@/lib/features/manual-time/manage-member-component";
import { useTeamTasks } from "@/app/hooks";
import { CustomSelect, TaskNameInfoDisplay } from "@/lib/features";
import { statusTable } from "./TimesheetAction";
import { TimesheetLog } from "@/app/interfaces";
import { secondsToTime } from "@/app/helpers";
import { useTimesheet } from "@/app/hooks/features/useTimesheet";

export interface IEditTaskModalProps {
isOpen: boolean;
Expand All @@ -19,34 +21,43 @@ export interface IEditTaskModalProps {
export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskModalProps) {
const { activeTeam } = useTeamTasks();
const t = useTranslations();
// const [dateRange, setDateRange] = useState<{ from: Date | null }>({
// from: new Date(),
// });
// const [endTime, setEndTime] = useState<string>('');
// const [startTime, setStartTime] = useState<string>('');
// const [isBillable, setIsBillable] = useState<boolean>(dataTimesheet.isBillable);
// const [notes, setNotes] = useState('');
const { updateTimesheet } = useTimesheet({})

const [dateRange, setDateRange] = useState<{ from: Date | null }>({
from: dataTimesheet.timesheet?.startedAt ? new Date(dataTimesheet.timesheet.startedAt) : new Date(),
const [dateRange, setDateRange] = useState<{ date: Date | null }>({
date: dataTimesheet.timesheet?.startedAt ? new Date(dataTimesheet.timesheet.startedAt) : new Date(),
});
const [endTime, setEndTime] = useState<string>(
dataTimesheet.timesheet?.stoppedAt
? new Date(dataTimesheet.timesheet.stoppedAt).toLocaleTimeString('en-US', { hour12: false }).slice(0, 5)
: ''
);
const [startTime, setStartTime] = useState<string>(
dataTimesheet.timesheet?.startedAt
? new Date(dataTimesheet.timesheet.startedAt).toLocaleTimeString('en-US', { hour12: false }).slice(0, 5)
: ''
);
const [isBillable, setIsBillable] = useState<boolean>(dataTimesheet.isBillable);
const [notes, setNotes] = useState<string>('');

const { h: hours, m: minutes } = secondsToTime(dataTimesheet.timesheet.duration);

const [timeRange, setTimeRange] = useState<{ startTime: string; endTime: string }>({
startTime: dataTimesheet.timesheet?.startedAt
? dataTimesheet.timesheet.startedAt.toString().slice(0, 5)
: '',
endTime: dataTimesheet.timesheet?.stoppedAt
? dataTimesheet.timesheet.stoppedAt.toString().slice(0, 5)
: '',
});

const updateTime = (key: 'startTime' | 'endTime', value: string) => {
setTimeRange(prevState => ({
...prevState,
[key]: value,
}));
};
const [timesheetData, setTimesheetData] = useState({
isBillable: dataTimesheet.isBillable,
projectId: dataTimesheet.project?.id || '',
notes: dataTimesheet.description || '',
});

const memberItemsLists = {
Project: activeTeam?.projects as [],
};
const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => {
// Handle value changes
setTimesheetData((prev) => ({
...prev,
projectId: values['Project']?.id,
}));
};
const selectedValues = {
Teams: null,
Expand All @@ -55,29 +66,74 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
// Handle field changes
};

const handleUpdateSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!timeRange.startTime || !timeRange.endTime) {
alert('Please enter valid start and end times.');
return;
}
if (!/^\d{2}:\d{2}$/.test(timeRange.startTime) || !/^\d{2}:\d{2}$/.test(timeRange.endTime)) {
alert('Time format should be HH:MM.');
return;
}

const baseDate = dateRange.date ?? new Date();
const startedAt = new Date(
Date.UTC(
baseDate.getFullYear(),
baseDate.getMonth(),
baseDate.getDate(),
...timeRange.startTime.split(':').map(Number)
)
);
const stoppedAt = new Date(
Date.UTC(
baseDate.getFullYear(),
baseDate.getMonth(),
baseDate.getDate(),
...timeRange.endTime.split(':').map(Number)
)
);
await updateTimesheet({
id: dataTimesheet.timesheetId,
isBillable: timesheetData.isBillable,
employeeId: dataTimesheet.employeeId,
logType: dataTimesheet.logType,
source: dataTimesheet.source,
startedAt: startedAt,
stoppedAt: stoppedAt,
tenantId: dataTimesheet.tenantId,
organizationId: dataTimesheet.organizationId,
description: timesheetData.notes,
projectId: timesheetData.projectId,
});
}, [dateRange, timeRange, timesheetData, dataTimesheet, updateTimesheet]);

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

},
];

const handleFromChange = (fromDate: Date | null) => {
setDateRange((prev) => ({ ...prev, from: fromDate }));
setDateRange((prev) => ({ ...prev, date: fromDate }));
};
return (
<Modal
closeModal={closeModal}
isOpen={isOpen}
showCloseIcon
title={'Edit Task'}
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]"
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:min-w-[32rem] justify-start h-[auto]"
titleClass="font-bold flex justify-start w-full">
<div className="flex flex-col w-full">
<form onSubmit={handleUpdateSubmit} className="flex flex-col w-full">
<div className="flex flex-col border-b border-b-slate-100 dark:border-b-gray-700">
<TaskNameInfoDisplay
task={dataTimesheet.task}
Expand All @@ -98,7 +154,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
<span className="text-[#282048] dark:text-gray-500 ">{t('dailyPlan.TASK_TIME')}</span>
<div className="flex items-center gap-x-2 ">
<FaRegClock className="text-[#30B366]" />
<span>08:10h</span>
<span>{hours}:{minutes} h</span>
</div>
</div>
<div className="flex items-center w-full">
Expand All @@ -111,8 +167,11 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
aria-label="Start time"
aria-describedby="start-time-error"
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
min="00:00"
max="23:59"
pattern="[0-9]{2}:[0-9]{2}"
value={timeRange.startTime}
onChange={(e) => updateTime("startTime", 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
/>
Expand All @@ -128,8 +187,8 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
aria-label="End time"
aria-describedby="end-time-error"
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
value={timeRange.endTime}
onChange={(e) => updateTime('endTime', 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
/>
Expand All @@ -139,7 +198,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
<div>
<span className="block text-[#282048] dark:text-gray-500 mr-2">{t("manualTime.DATE")}</span>
<DatePickerFilter
date={dateRange.from}
date={dateRange.date}
setDate={handleFromChange}
label="Oct 01 2024"
/>
Expand All @@ -160,36 +219,45 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
<label className="text-[#282048] dark:text-gray-500 mr-12 capitalize">{t('pages.timesheet.BILLABLE.BILLABLE').toLowerCase()}</label>
<div className="flex items-center gap-3">
<ToggleButton
isActive={isBillable}
onClick={() => setIsBillable(true)}
isActive={timesheetData.isBillable}
onClick={() => setTimesheetData((prev) => ({
...prev,
isBillable: true,
}))}
label={t('pages.timesheet.BILLABLE.YES')}
/>
<ToggleButton
isActive={!isBillable}
onClick={() => setIsBillable(false)}
isActive={!timesheetData.isBillable}
onClick={() => setTimesheetData((prev) => ({
...prev,
isBillable: false,
}))}
label={t('pages.timesheet.BILLABLE.NO')}
/>
</div>
</div>
<div className="w-full flex flex-col">
<span className="text-[#282048] dark:text-gray-400">{t('common.NOTES')}</span>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
value={timesheetData.notes}
onChange={(e) => setTimesheetData((prev) => ({
...prev,
notes: 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"
timesheetData.notes.trim().length === 0 && "border-red-500"
)}
maxLength={120}
minLength={0}
aria-label="Insert notes here"
required
/>
<div className="text-sm text-[#282048] dark:text-gray-500 text-right">
{notes.length}/{120}
{timesheetData.notes.length}/{120}
</div>
</div>
<div className="border-t border-t-gray-200 dark:border-t-gray-700 w-full"></div>
Expand Down Expand Up @@ -224,7 +292,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
</div>
</div>
</div>
</div>
</form>

</Modal>
)
Expand Down
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 @@ -229,7 +229,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
);
});

export default withAuthentication(TimeSheet, { displayName: 'TimeSheet' });
export default withAuthentication(TimeSheet, { displayName: 'TimeSheet', showPageSkeleton: true });

const ViewToggleButton: React.FC<ViewToggleButtonProps> = ({ mode, active, icon, onClick, t }) => (
<button
Expand Down
Loading

0 comments on commit 9caa7b9

Please sign in to comment.