Skip to content

Commit 64454a6

Browse files
authored
Merge pull request #3385 from ever-co/develop
Release
2 parents c47ec5c + 9caa7b9 commit 64454a6

File tree

13 files changed

+377
-89
lines changed

13 files changed

+377
-89
lines changed

apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
6060
closeModal={closeModal}
6161
title={'+ Add Time Entry'}
6262
showCloseIcon
63-
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"
63+
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]"
6464
titleClass="font-bold flex justify-start w-full">
65-
<div className="flex flex-col w-full gap-4 justify-start overflow-y-auto">
65+
<div className="flex flex-col w-full gap-4 justify-start md:w-40 md:min-w-[32rem] p-4">
6666
<div className=" w-full mr-[4%]">
6767
<label className="block text-[#282048] dark:text-gray-400 font-medium mb-1">
6868
{t('sidebar.TASKS')}
@@ -207,7 +207,7 @@ const ShiftTimingSelect = ({ label, timeOptions, placeholder, className, onChang
207207
<SelectItem
208208
key={time}
209209
value={time}
210-
className="hover:bg-primary focus:bg-primary hover:text-white px-2 py-1 cursor-pointer"
210+
className="hover:bg-primary focus:bg-primary hover:text-white py-1 cursor-pointer"
211211
>
212212
{time}
213213
</SelectItem>
@@ -232,27 +232,31 @@ const OptimizedAccordion = ({ dateRange, handleFromChange, timeOptions, t }: {
232232
const [shifts, setShifts] = React.useState<Shift[]>([
233233
{ startTime: '', endTime: '', totalHours: '00:00h', dateFrom: new Date() },
234234
])
235-
const convertToMinutes = (time: string): number => {
236-
const [hours, minutes] = time.split(':').map(Number);
237-
return hours * 60 + minutes;
238-
};
239-
240-
const calculateTotalHours = React.useCallback((start: string, end: string): string => {
241-
if (!start || !end) return '00:00h';
242235

243-
const startMinutes = convertToMinutes(start);
244-
const endMinutes = convertToMinutes(end);
236+
const convertToMinutesHour = (time: string): number => {
237+
const [hourMinute, period] = time.split(' ');
238+
const [hours, minutes] = hourMinute.split(':').map(Number);
245239

246-
const totalMinutes = endMinutes >= startMinutes
247-
? endMinutes - startMinutes
248-
: 1440 - startMinutes + endMinutes;
249-
250-
const hours = Math.floor(totalMinutes / 60);
251-
const minutes = totalMinutes % 60;
252-
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}h`;
253-
}, []);
240+
let totalMinutes = (hours % 12) * 60 + minutes;
241+
if (period === 'PM') totalMinutes += 720;
254242

243+
return totalMinutes;
244+
}
255245

246+
const calculateTotalHoursHour = React.useCallback(
247+
(start: string, end: string): string => {
248+
if (!start || !end) return '00:00h';
249+
const startMinutes = convertToMinutesHour(start);
250+
const endMinutes = convertToMinutesHour(end);
251+
const totalMinutes = endMinutes >= startMinutes
252+
? endMinutes - startMinutes
253+
: 1440 - startMinutes + endMinutes;
254+
const hours = Math.floor(totalMinutes / 60);
255+
const minutes = totalMinutes % 60;
256+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}h`;
257+
},
258+
[]
259+
);
256260

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

271+
267272
const handleShiftChange = (index: number, field: keyof Shift, value: string) => {
268273
const updatedShifts = [...shifts];
269274
updatedShifts[index][field] = value;
270275

271276
if (field === 'startTime' || field === 'endTime') {
272277
const { startTime, endTime } = updatedShifts[index];
273-
updatedShifts[index].totalHours = calculateTotalHours(startTime, endTime);
278+
279+
if (!startTime || !endTime) return;
280+
281+
if (convertToMinutesHour(startTime) >= convertToMinutesHour(endTime)) {
282+
return;
283+
}
284+
updatedShifts[index].totalHours = calculateTotalHoursHour(startTime, endTime);
274285
const isOverlapping = shifts.some((shift, i) => {
275286
if (i === index || !shift.startTime || !shift.endTime) return false;
276-
const currentStart = convertToMinutes(startTime);
277-
const currentEnd = convertToMinutes(endTime);
278-
const shiftStart = convertToMinutes(shift.startTime);
279-
const shiftEnd = convertToMinutes(shift.endTime);
280-
return (currentStart >= shiftStart && currentStart < shiftEnd) ||
281-
(currentEnd > shiftStart && currentEnd <= shiftEnd);
287+
288+
const currentStart = convertToMinutesHour(startTime);
289+
const currentEnd = convertToMinutesHour(endTime);
290+
const shiftStart = convertToMinutesHour(shift.startTime);
291+
const shiftEnd = convertToMinutesHour(shift.endTime);
292+
return (
293+
(currentStart < shiftEnd && currentEnd > shiftStart) ||
294+
(currentStart === shiftStart && currentEnd === shiftEnd)
295+
);
282296
});
297+
283298
if (isOverlapping) {
284299
return;
285300
}
286301
}
302+
287303
setShifts(updatedShifts);
288304
};
289305

apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { EmployeeAvatar } from "./CompactTimesheetComponent";
1010
import { formatDate } from "@/app/helpers";
1111
import { ClockIcon } from "lucide-react";
1212

13-
export function CalendarView({ data }: { data?: GroupedTimesheet[] }) {
13+
export function CalendarView({ data, loading }: { data?: GroupedTimesheet[], loading: boolean }) {
1414
const t = useTranslations();
1515
return (
1616
<div className="grow h-full w-full bg-[#FFFFFF] dark:bg-dark--theme">

apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx

Lines changed: 110 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import { Modal, statusColor } from "@/lib/components";
22
import { IoMdArrowDropdown } from "react-icons/io";
33
import { FaRegClock } from "react-icons/fa";
44
import { DatePickerFilter } from "./TimesheetFilterDate";
5-
import { useState } from "react";
5+
import { FormEvent, useCallback, useState } from "react";
66
import { useTranslations } from "next-intl";
77
import { clsxm } from "@/app/utils";
88
import { Item, ManageOrMemberComponent, getNestedValue } from "@/lib/features/manual-time/manage-member-component";
99
import { useTeamTasks } from "@/app/hooks";
1010
import { CustomSelect, TaskNameInfoDisplay } from "@/lib/features";
1111
import { statusTable } from "./TimesheetAction";
1212
import { TimesheetLog } from "@/app/interfaces";
13+
import { secondsToTime } from "@/app/helpers";
14+
import { useTimesheet } from "@/app/hooks/features/useTimesheet";
1315

1416
export interface IEditTaskModalProps {
1517
isOpen: boolean;
@@ -19,34 +21,43 @@ export interface IEditTaskModalProps {
1921
export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskModalProps) {
2022
const { activeTeam } = useTeamTasks();
2123
const t = useTranslations();
22-
// const [dateRange, setDateRange] = useState<{ from: Date | null }>({
23-
// from: new Date(),
24-
// });
25-
// const [endTime, setEndTime] = useState<string>('');
26-
// const [startTime, setStartTime] = useState<string>('');
27-
// const [isBillable, setIsBillable] = useState<boolean>(dataTimesheet.isBillable);
28-
// const [notes, setNotes] = useState('');
24+
const { updateTimesheet } = useTimesheet({})
2925

30-
const [dateRange, setDateRange] = useState<{ from: Date | null }>({
31-
from: dataTimesheet.timesheet?.startedAt ? new Date(dataTimesheet.timesheet.startedAt) : new Date(),
26+
const [dateRange, setDateRange] = useState<{ date: Date | null }>({
27+
date: dataTimesheet.timesheet?.startedAt ? new Date(dataTimesheet.timesheet.startedAt) : new Date(),
3228
});
33-
const [endTime, setEndTime] = useState<string>(
34-
dataTimesheet.timesheet?.stoppedAt
35-
? new Date(dataTimesheet.timesheet.stoppedAt).toLocaleTimeString('en-US', { hour12: false }).slice(0, 5)
36-
: ''
37-
);
38-
const [startTime, setStartTime] = useState<string>(
39-
dataTimesheet.timesheet?.startedAt
40-
? new Date(dataTimesheet.timesheet.startedAt).toLocaleTimeString('en-US', { hour12: false }).slice(0, 5)
41-
: ''
42-
);
43-
const [isBillable, setIsBillable] = useState<boolean>(dataTimesheet.isBillable);
44-
const [notes, setNotes] = useState<string>('');
29+
30+
const { h: hours, m: minutes } = secondsToTime(dataTimesheet.timesheet.duration);
31+
32+
const [timeRange, setTimeRange] = useState<{ startTime: string; endTime: string }>({
33+
startTime: dataTimesheet.timesheet?.startedAt
34+
? dataTimesheet.timesheet.startedAt.toString().slice(0, 5)
35+
: '',
36+
endTime: dataTimesheet.timesheet?.stoppedAt
37+
? dataTimesheet.timesheet.stoppedAt.toString().slice(0, 5)
38+
: '',
39+
});
40+
41+
const updateTime = (key: 'startTime' | 'endTime', value: string) => {
42+
setTimeRange(prevState => ({
43+
...prevState,
44+
[key]: value,
45+
}));
46+
};
47+
const [timesheetData, setTimesheetData] = useState({
48+
isBillable: dataTimesheet.isBillable,
49+
projectId: dataTimesheet.project?.id || '',
50+
notes: dataTimesheet.description || '',
51+
});
52+
4553
const memberItemsLists = {
4654
Project: activeTeam?.projects as [],
4755
};
4856
const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => {
49-
// Handle value changes
57+
setTimesheetData((prev) => ({
58+
...prev,
59+
projectId: values['Project']?.id,
60+
}));
5061
};
5162
const selectedValues = {
5263
Teams: null,
@@ -55,29 +66,74 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
5566
// Handle field changes
5667
};
5768

69+
const handleUpdateSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
70+
e.preventDefault();
71+
if (!timeRange.startTime || !timeRange.endTime) {
72+
alert('Please enter valid start and end times.');
73+
return;
74+
}
75+
if (!/^\d{2}:\d{2}$/.test(timeRange.startTime) || !/^\d{2}:\d{2}$/.test(timeRange.endTime)) {
76+
alert('Time format should be HH:MM.');
77+
return;
78+
}
79+
80+
const baseDate = dateRange.date ?? new Date();
81+
const startedAt = new Date(
82+
Date.UTC(
83+
baseDate.getFullYear(),
84+
baseDate.getMonth(),
85+
baseDate.getDate(),
86+
...timeRange.startTime.split(':').map(Number)
87+
)
88+
);
89+
const stoppedAt = new Date(
90+
Date.UTC(
91+
baseDate.getFullYear(),
92+
baseDate.getMonth(),
93+
baseDate.getDate(),
94+
...timeRange.endTime.split(':').map(Number)
95+
)
96+
);
97+
await updateTimesheet({
98+
id: dataTimesheet.timesheetId,
99+
isBillable: timesheetData.isBillable,
100+
employeeId: dataTimesheet.employeeId,
101+
logType: dataTimesheet.logType,
102+
source: dataTimesheet.source,
103+
startedAt: startedAt,
104+
stoppedAt: stoppedAt,
105+
tenantId: dataTimesheet.tenantId,
106+
organizationId: dataTimesheet.organizationId,
107+
description: timesheetData.notes,
108+
projectId: timesheetData.projectId,
109+
});
110+
}, [dateRange, timeRange, timesheetData, dataTimesheet, updateTimesheet]);
111+
58112
const fields = [
59113
{
60114
label: t('sidebar.PROJECTS'),
61115
placeholder: 'Select a project',
62116
isRequired: true,
63117
valueKey: 'id',
64118
displayKey: 'name',
65-
element: 'Project'
119+
element: 'Project',
120+
defaultValue: dataTimesheet.project.name
121+
66122
},
67123
];
68124

69125
const handleFromChange = (fromDate: Date | null) => {
70-
setDateRange((prev) => ({ ...prev, from: fromDate }));
126+
setDateRange((prev) => ({ ...prev, date: fromDate }));
71127
};
72128
return (
73129
<Modal
74130
closeModal={closeModal}
75131
isOpen={isOpen}
76132
showCloseIcon
77133
title={'Edit Task'}
78-
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]"
134+
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:min-w-[32rem] justify-start h-[auto]"
79135
titleClass="font-bold flex justify-start w-full">
80-
<div className="flex flex-col w-full">
136+
<form onSubmit={handleUpdateSubmit} className="flex flex-col w-full">
81137
<div className="flex flex-col border-b border-b-slate-100 dark:border-b-gray-700">
82138
<TaskNameInfoDisplay
83139
task={dataTimesheet.task}
@@ -98,7 +154,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
98154
<span className="text-[#282048] dark:text-gray-500 ">{t('dailyPlan.TASK_TIME')}</span>
99155
<div className="flex items-center gap-x-2 ">
100156
<FaRegClock className="text-[#30B366]" />
101-
<span>08:10h</span>
157+
<span>{hours}:{minutes} h</span>
102158
</div>
103159
</div>
104160
<div className="flex items-center w-full">
@@ -111,8 +167,11 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
111167
aria-label="Start time"
112168
aria-describedby="start-time-error"
113169
type="time"
114-
value={startTime}
115-
onChange={(e) => setStartTime(e.target.value)}
170+
min="00:00"
171+
max="23:59"
172+
pattern="[0-9]{2}:[0-9]{2}"
173+
value={timeRange.startTime}
174+
onChange={(e) => updateTime("startTime", e.target.value)}
116175
className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md"
117176
required
118177
/>
@@ -128,8 +187,8 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
128187
aria-label="End time"
129188
aria-describedby="end-time-error"
130189
type="time"
131-
value={endTime}
132-
onChange={(e) => setEndTime(e.target.value)}
190+
value={timeRange.endTime}
191+
onChange={(e) => updateTime('endTime', e.target.value)}
133192
className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md"
134193
required
135194
/>
@@ -139,7 +198,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
139198
<div>
140199
<span className="block text-[#282048] dark:text-gray-500 mr-2">{t("manualTime.DATE")}</span>
141200
<DatePickerFilter
142-
date={dateRange.from}
201+
date={dateRange.date}
143202
setDate={handleFromChange}
144203
label="Oct 01 2024"
145204
/>
@@ -160,36 +219,45 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
160219
<label className="text-[#282048] dark:text-gray-500 mr-12 capitalize">{t('pages.timesheet.BILLABLE.BILLABLE').toLowerCase()}</label>
161220
<div className="flex items-center gap-3">
162221
<ToggleButton
163-
isActive={isBillable}
164-
onClick={() => setIsBillable(true)}
222+
isActive={timesheetData.isBillable}
223+
onClick={() => setTimesheetData((prev) => ({
224+
...prev,
225+
isBillable: true,
226+
}))}
165227
label={t('pages.timesheet.BILLABLE.YES')}
166228
/>
167229
<ToggleButton
168-
isActive={!isBillable}
169-
onClick={() => setIsBillable(false)}
230+
isActive={!timesheetData.isBillable}
231+
onClick={() => setTimesheetData((prev) => ({
232+
...prev,
233+
isBillable: false,
234+
}))}
170235
label={t('pages.timesheet.BILLABLE.NO')}
171236
/>
172237
</div>
173238
</div>
174239
<div className="w-full flex flex-col">
175240
<span className="text-[#282048] dark:text-gray-400">{t('common.NOTES')}</span>
176241
<textarea
177-
value={notes}
178-
onChange={(e) => setNotes(e.target.value)}
242+
value={timesheetData.notes}
243+
onChange={(e) => setTimesheetData((prev) => ({
244+
...prev,
245+
notes: e.target.value,
246+
}))}
179247
placeholder="Insert notes here..."
180248
className={clsxm(
181249
"bg-transparent focus:border-transparent focus:ring-2 focus:ring-transparent",
182250
"placeholder-gray-300 placeholder:font-normal resize-none p-2 grow w-full",
183251
"border border-gray-200 dark:border-slate-600 dark:bg-dark--theme-light rounded-md h-40 bg-[#FBB6500D]",
184-
notes.trim().length === 0 && "border-red-500"
252+
timesheetData.notes.trim().length === 0 && "border-red-500"
185253
)}
186254
maxLength={120}
187255
minLength={0}
188256
aria-label="Insert notes here"
189257
required
190258
/>
191259
<div className="text-sm text-[#282048] dark:text-gray-500 text-right">
192-
{notes.length}/{120}
260+
{timesheetData.notes.length}/{120}
193261
</div>
194262
</div>
195263
<div className="border-t border-t-gray-200 dark:border-t-gray-700 w-full"></div>
@@ -224,7 +292,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
224292
</div>
225293
</div>
226294
</div>
227-
</div>
295+
</form>
228296

229297
</Modal>
230298
)

0 commit comments

Comments
 (0)