Skip to content

Commit

Permalink
[Feat]: Add time Entry Modal and Update Timesheet Status based on API…
Browse files Browse the repository at this point in the history
… response (#3365)

* feat: feat: add AddTimeEntyModal component for managing task entries with time and project association

* feat: update timesheet status based on API response

* feat: display total for pending tasks

* fix:coderabbitai

* fix:coderabbitai

* fix:coderabbitai

* fix:coderabbitai
  • Loading branch information
Innocent-Akim authored Nov 24, 2024
1 parent 8f6352d commit 528362c
Show file tree
Hide file tree
Showing 16 changed files with 476 additions and 59 deletions.
281 changes: 250 additions & 31 deletions apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
</div>
</div>
<div className="w-full flex flex-col">
<span className="text-[#282048] dark:text-gray-500 ">Notes</span>
<span className="text-[#282048] dark:text-gray-400 ">Notes</span>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
Expand Down Expand Up @@ -211,13 +211,13 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
<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")}>
className={clsxm("dark:text-primary-light 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-[#4435a1] h-[2.3rem] w-[5.5rem] justify-center font-normal flex items-center text-white px-2 rounded-lg',
'bg-primary dark:bg-primary-light h-[2.3rem] w-[5.5rem] justify-center font-normal flex items-center text-white px-2 rounded-lg',
)}>
{t('common.SAVE')}
</button>
Expand All @@ -242,17 +242,17 @@ export const ToggleButton = ({ isActive, onClick, label }: ToggleButtonProps) =>
onClick={onClick}
aria-pressed={isActive}
className={clsxm(
'flex items-center gap-x-2 p-2 rounded focus:outline-none focus:ring-2 focus:ring-primary/50',
'flex items-center gap-x-2 p-2 rounded focus:outline-none focus:ring-2 focus:ring-primary/50 dark:focus:ring-primary-light/50',
'transition-colors duration-200 ease-in-out'
)}
>
<div
className={clsxm(
'w-4 h-4 rounded-full transition-colors duration-200 ease-in-out',
isActive ? 'bg-primary' : 'bg-gray-200'
isActive ? 'bg-primary dark:bg-primary-light' : 'bg-gray-200'
)}
/>
<span className={clsxm('', isActive && 'text-primary')}>
<span className={clsxm('', isActive && 'text-primary dark:text-primary-light')}>
{label}
</span>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export const FilterCalendar = memo(function FuturePlansCalendar<T extends { date
toYear={
endYear ||
new Date(sortedPlansByDateDesc?.[sortedPlansByDateDesc?.length - 1]?.date ?? Date.now()).getFullYear() +
10
10
}
/>
);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
endDate: dateRange.to ?? ''
});



const lowerCaseSearch = useMemo(() => search?.toLowerCase() ?? '', [search]);
const filterDataTimesheet = useMemo(() => {
const filteredTimesheet =
Expand All @@ -66,8 +68,6 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
timesheet,
lowerCaseSearch,
]);


const {
isOpen: isManualTimeModalOpen,
openModal: openManualTimeModal,
Expand Down Expand Up @@ -119,7 +119,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
</div>
<div className="flex items-center justify-between w-full gap-6 pt-4">
<TimesheetCard
count={72}
count={statusTimesheet.PENDING.length}
title="Pending Tasks"
description="Tasks waiting for your approval"
icon={<GrTask className="font-bold" />}
Expand Down
69 changes: 69 additions & 0 deletions apps/web/app/api/timer/timesheet/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ID } from "@/app/interfaces";
import { authenticatedGuard } from "@/app/services/server/guards/authenticated-guard-app";
import { updateStatusTimesheetRequest } from "@/app/services/server/requests";
import { NextResponse } from "next/server";

export async function PUT(req: Request) {
const res = new NextResponse();
const {
$res,
user,
tenantId,
organizationId,
access_token
} = await authenticatedGuard(req, res);
if (!user) return $res('Unauthorized');

try {
const { searchParams } = new URL(req.url);

const rawIds = searchParams.get('ids');
const status = searchParams.get('status');

if (!rawIds || !status) {
return $res({
success: false,
message: 'Missing required parameters'
});
}
let ids: ID[];
try {
ids = JSON.parse(rawIds);
if (!Array.isArray(ids) || !ids.length) {
throw new Error('Invalid ids format');
}
} catch (error) {
return $res({
success: false,
message: 'Invalid ids format'
});
}
const validStatuses = ['pending', 'approved', 'rejected'];
if (!validStatuses.includes(status)) {
return $res({
success: false,
message: 'Invalid status value'
});
}
const { data } = await updateStatusTimesheetRequest(
{
ids,
organizationId,
status,
tenantId
},
access_token
);

return $res({
success: true,
data
});
} catch (error) {
console.error('Error updating timesheet status:', error);
return $res({
success: false,
message: 'Failed to update timesheet status'
});
}
}
18 changes: 15 additions & 3 deletions apps/web/app/hooks/features/useTimelogFilterOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { timesheetDeleteState, timesheetGroupByDayState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState } from '@/app/stores';
import { timesheetDeleteState, timesheetGroupByDayState, timesheetFilterEmployeeState, timesheetFilterProjectState, timesheetFilterStatusState, timesheetFilterTaskState, timesheetUpdateStatus } from '@/app/stores';
import { useAtom } from 'jotai';
import React from 'react';

Expand All @@ -8,12 +8,21 @@ export function useTimelogFilterOptions() {
const [statusState, setStatusState] = useAtom(timesheetFilterStatusState);
const [taskState, setTaskState] = useAtom(timesheetFilterTaskState);
const [selectTimesheet, setSelectTimesheet] = useAtom(timesheetDeleteState);
const [timesheetGroupByDays, setTimesheetGroupByDays] = useAtom(timesheetGroupByDayState)
const [timesheetGroupByDays, setTimesheetGroupByDays] = useAtom(timesheetGroupByDayState);
const [puTimesheetStatus, setPuTimesheetStatus] = useAtom(timesheetUpdateStatus)

const employee = employeeState;
const project = projectState;
const task = taskState

const generateTimeOptions = (interval = 15) => {
const totalSlots = (24 * 60) / interval; // Total intervals in a day
return Array.from({ length: totalSlots }, (_, i) => {
const hour = Math.floor((i * interval) / 60).toString().padStart(2, '0');
const minutes = ((i * interval) % 60).toString().padStart(2, '0');
return `${hour}:${minutes}`;
});
};
const handleSelectRowTimesheet = (items: string) => {
setSelectTimesheet((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items])
}
Expand All @@ -34,6 +43,9 @@ export function useTimelogFilterOptions() {
selectTimesheet,
setSelectTimesheet,
timesheetGroupByDays,
setTimesheetGroupByDays
setTimesheetGroupByDays,
generateTimeOptions,
setPuTimesheetStatus,
puTimesheetStatus
};
}
41 changes: 35 additions & 6 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { useAtom } from 'jotai';
import { timesheetRapportState } from '@/app/stores/time-logs';
import { useQuery } from '../useQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log';
import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi, updateStatusTimesheetFromApi } from '@/app/services/client/api/timer/timer-log';
import moment from 'moment';
import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { ID, TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { useTimelogFilterOptions } from './useTimelogFilterOptions';

interface TimesheetParams {
Expand Down Expand Up @@ -100,9 +100,10 @@ export function useTimesheet({
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
const { employee, project, task, statusState, selectTimesheet: logIds, timesheetGroupByDays } = useTimelogFilterOptions();
const { employee, project, task, statusState, selectTimesheet: logIds, timesheetGroupByDays, puTimesheetStatus } = useTimelogFilterOptions();
const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi)
const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi);
const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi)


const getTaskTimesheet = useCallback(
Expand Down Expand Up @@ -137,6 +138,31 @@ export function useTimesheet({
]
);

const updateTimesheetStatus = useCallback(
({ status, ids }: { status: TimesheetStatus, ids: ID[] | ID }) => {
if (!user) return;
queryUpdateTimesheetStatus({ ids, status })
.then((response) => {
const updatedData = timesheet.map(item => {
const newItem = response.data.find(newItem => newItem.id === item.timesheet.id);
if (newItem) {
return {
...item,
timesheet: {
...item.timesheet,
status: newItem.status
}
};
}
return item;
});
setTimesheet(updatedData);
})
.catch((error) => {
console.error('Error fetching timesheet:', error);
});
}, [queryUpdateTimesheetStatus])

const getStatusTimesheet = (items: TimesheetLog[] = []) => {
const STATUS_MAP: Record<TimesheetStatus, TimesheetLog[]> = {
PENDING: [],
Expand Down Expand Up @@ -212,7 +238,7 @@ export function useTimesheet({

useEffect(() => {
getTaskTimesheet({ startDate, endDate });
}, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays]);
}, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, timesheet]);

return {
loadingTimesheet,
Expand All @@ -222,6 +248,9 @@ export function useTimesheet({
deleteTaskTimesheet,
getStatusTimesheet,
timesheetGroupByDays,
statusTimesheet: getStatusTimesheet(timesheet.flat())
statusTimesheet: getStatusTimesheet(timesheet.flat()),
updateTimesheetStatus,
loadingUpdateTimesheetStatus,
puTimesheetStatus
};
}
26 changes: 26 additions & 0 deletions apps/web/app/hooks/useScrollListener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';
import React from 'react';
import { TimesheetLog } from '../interfaces';

export function useScrollListener() {
const [scrolling, setScrolling] = React.useState(false);
Expand All @@ -20,3 +21,28 @@ export function useScrollListener() {

return { scrolling };
}



export const useInfinityScroll = (timesheet: TimesheetLog[]) => {

const [items, setItems] = React.useState<TimesheetLog[]>(timesheet);
const [page, setPage] = React.useState(1);
const [isLoading, setIsLoading] = React.useState(false);


React.useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500 && !isLoading) {
setPage((prevPage) => prevPage + 1);
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [isLoading]);


return { items, page, setIsLoading, setItems }
}
7 changes: 7 additions & 0 deletions apps/web/app/interfaces/IDailyPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,11 @@ export enum DailyPlanStatusEnum {
COMPLETED = 'completed'
}

export interface IUpdateTimesheetStatus {
ids: ID[] | ID,
organizationId?: ID,
status: ID,
tenantId?: ID
}

export type IDailyPlanMode = 'today' | 'tomorrow' | 'custom';
29 changes: 29 additions & 0 deletions apps/web/app/interfaces/timer/ITimerLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,32 @@ export interface TimesheetLog extends BaseEntity {
duration: number;
isEdited: boolean;
}



export interface UpdateTimesheetStatus extends BaseEntity {
isActive: boolean;
isArchived: boolean;
archivedAt: string | null;
duration: number;
keyboard: number;
mouse: number;
overall: number;
startedAt: string;
stoppedAt: string;
approvedAt: string | null;
submittedAt: string | null;
lockedAt: string | null;
editedAt: string | null;
isBilled: boolean;
status:
| "DRAFT"
| "PENDING"
| "IN REVIEW"
| "DENIED"
| "APPROVED";
employeeId: string;
approvedById: string | null;
employee: Employee;
isEdited: boolean;
}
11 changes: 9 additions & 2 deletions apps/web/app/services/client/api/timer/timer-log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TimesheetLog, ITimerStatus } from '@app/interfaces';
import { get, deleteApi } from '../../axios';
import { TimesheetLog, ITimerStatus, IUpdateTimesheetStatus, UpdateTimesheetStatus } from '@app/interfaces';
import { get, deleteApi, put } from '../../axios';
import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers';

export async function getTimerLogs(
tenantId: string,
Expand Down Expand Up @@ -120,3 +121,9 @@ export async function deleteTaskTimesheetLogsApi({
throw new Error(`Failed to delete timesheet logs`);
}
}

export function updateStatusTimesheetFromApi(data: IUpdateTimesheetStatus) {
const organizationId = getOrganizationIdCookie();
const tenantId = getTenantIdCookie();
return put<UpdateTimesheetStatus[]>(`/timesheet/status`, { ...data, organizationId }, { tenantId });
}
14 changes: 13 additions & 1 deletion apps/web/app/services/server/requests/timesheet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ITasksTimesheet } from '@app/interfaces/ITimer';
import { serverFetch } from '../fetch';
import qs from 'qs';
import { TimesheetLog } from '@/app/interfaces/timer/ITimerLog';
import { TimesheetLog, UpdateTimesheetStatus } from '@/app/interfaces/timer/ITimerLog';
import { IUpdateTimesheetStatus } from '@/app/interfaces';

export type TTasksTimesheetStatisticsParams = {
tenantId: string;
Expand Down Expand Up @@ -95,3 +96,14 @@ export function deleteTaskTimesheetRequest(params: IDeleteTimesheetProps, bearer
tenantId: params.tenantId
});
}


export function updateStatusTimesheetRequest(params: IUpdateTimesheetStatus, bearer_token: string) {
return serverFetch<UpdateTimesheetStatus[]>({
path: '/timesheet/status',
method: 'PUT',
body: { ...params },
bearer_token,
tenantId: params.tenantId,
})
}
Loading

0 comments on commit 528362c

Please sign in to comment.