Skip to content

Commit

Permalink
Merge pull request #3385 from ever-co/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
evereq authored Nov 30, 2024
2 parents c47ec5c + 9caa7b9 commit 64454a6
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 89 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EmployeeAvatar } from "./CompactTimesheetComponent";
import { formatDate } from "@/app/helpers";
import { ClockIcon } from "lucide-react";

export function CalendarView({ data }: { data?: GroupedTimesheet[] }) {
export function CalendarView({ data, loading }: { data?: GroupedTimesheet[], loading: boolean }) {
const t = useTranslations();
return (
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">
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
Loading

0 comments on commit 64454a6

Please sign in to comment.