From 4eccf2302c5fd364f981e7813c8ead23302cd975 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:03:51 +0200 Subject: [PATCH] [Refactor]: Optimize DeleteTimesheet and UpdateTimesheetStatus (#3375) * Add createTimesheetFromApi function to create a timesheet entry from API * fix:update url * refactor: optimize deleteTimesheet and updateTimesheetStatus function for better readability * fix: deep scan * fix * fix: refactor some code * fix: deep scan * fix: Codacy Static Code Analysis * fix: build * feat: update billable from api * fix: conflits * fix: coderabbitai --- .../timer/timesheet/time-log/[id]/route.ts | 33 +++++ .../app/api/timer/timesheet/time-log/route.ts | 31 +++++ apps/web/app/hooks/features/useTimesheet.ts | 116 ++++++++++++------ apps/web/app/interfaces/timer/ITimerLog.ts | 34 +++-- .../services/client/api/timer/timer-log.ts | 32 ++++- .../app/services/server/requests/timesheet.ts | 23 +++- apps/web/app/stores/time-logs.ts | 2 + .../calendar/table-time-sheet.tsx | 61 +++++++-- 8 files changed, 271 insertions(+), 61 deletions(-) create mode 100644 apps/web/app/api/timer/timesheet/time-log/[id]/route.ts create mode 100644 apps/web/app/api/timer/timesheet/time-log/route.ts diff --git a/apps/web/app/api/timer/timesheet/time-log/[id]/route.ts b/apps/web/app/api/timer/timesheet/time-log/[id]/route.ts new file mode 100644 index 000000000..ed3c2f18c --- /dev/null +++ b/apps/web/app/api/timer/timesheet/time-log/[id]/route.ts @@ -0,0 +1,33 @@ +import { UpdateTimesheet } from "@/app/interfaces"; +import { authenticatedGuard } from "@/app/services/server/guards/authenticated-guard-app"; +import { updateTimesheetRequest } 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 id = searchParams.get('id') as string; + const body = (await req.json()) as UpdateTimesheet; + const { data } = await updateTimesheetRequest( + { ...body, tenantId, organizationId, id }, + access_token + ); + return $res(data); + } catch (error) { + console.error('Error updating timesheet status:', error); + return $res({ + success: false, + message: 'Failed to update timesheet status' + }); + } +} diff --git a/apps/web/app/api/timer/timesheet/time-log/route.ts b/apps/web/app/api/timer/timesheet/time-log/route.ts new file mode 100644 index 000000000..38919df8e --- /dev/null +++ b/apps/web/app/api/timer/timesheet/time-log/route.ts @@ -0,0 +1,31 @@ +import { UpdateTimesheet } from "@/app/interfaces"; +import { authenticatedGuard } from "@/app/services/server/guards/authenticated-guard-app"; +import { createTimesheetRequest } from "@/app/services/server/requests"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + const res = new NextResponse(); + const { + $res, + user, + tenantId, + organizationId, + access_token + } = await authenticatedGuard(req, res); + if (!user) return $res('Unauthorized'); + + try { + const body = (await req.json()) as UpdateTimesheet; + const { data } = await createTimesheetRequest( + { ...body, tenantId, organizationId }, + access_token + ); + return $res(data); + } catch (error) { + console.error('Error updating timesheet status:', error); + return $res({ + success: false, + message: 'Failed to update timesheet status' + }); + } +} diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 75be26e60..4afc705b2 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -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, updateStatusTimesheetFromApi } from '@/app/services/client/api/timer/timer-log'; +import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi, updateStatusTimesheetFromApi, createTimesheetFromApi, updateTimesheetFromAPi } from '@/app/services/client/api/timer/timer-log'; import moment from 'moment'; -import { ID, TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { ID, TimesheetLog, TimesheetStatus, UpdateTimesheet } from '@/app/interfaces'; import { useTimelogFilterOptions } from './useTimelogFilterOptions'; interface TimesheetParams { @@ -19,13 +19,6 @@ export interface GroupedTimesheet { } -interface DeleteTimesheetParams { - organizationId: string; - tenantId: string; - logIds: string[]; -} - - const groupByDate = (items: TimesheetLog[]): GroupedTimesheet[] => { if (!items?.length) return []; type GroupedMap = Record; @@ -104,6 +97,8 @@ export function useTimesheet({ const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi); const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi); const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi) + const { loading: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi); + const { loading: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(updateTimesheetFromAPi); const getTaskTimesheet = useCallback( @@ -138,30 +133,76 @@ export function useTimesheet({ ] ); + const createTimesheet = useCallback( + async ({ ...timesheetParams }: UpdateTimesheet) => { + if (!user) { + throw new Error("User not authenticated"); + } + try { + const response = await queryCreateTimesheet(timesheetParams); + setTimesheet((prevTimesheet) => [ + response.data, + ...(prevTimesheet || []) + ]); + } catch (error) { + console.error('Error:', error); + } + }, + [queryCreateTimesheet, setTimesheet, user] + ); + + + + const updateTimesheet = useCallback<(params: UpdateTimesheet) => Promise>( + async ({ ...timesheet }: UpdateTimesheet) => { + if (!user) { + throw new Error("User not authenticated"); + } + try { + const response = await queryUpdateTimesheet(timesheet); + setTimesheet(prevTimesheet => + prevTimesheet.map(item => + item.timesheet.id === response.data.timesheet.id + ? response.data + : item + ) + ); + } catch (error) { + console.error('Error updating timesheet:', error); + throw error; + } + }, [queryUpdateTimesheet, setTimesheet, user]) + + const updateTimesheetStatus = useCallback( - ({ status, ids }: { status: TimesheetStatus, ids: ID[] | ID }) => { + async ({ 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) { + const idsArray = Array.isArray(ids) ? ids : [ids]; + try { + const response = await queryUpdateTimesheetStatus({ ids: idsArray, status }); + const responseMap = new Map(response.data.map(item => [item.id, item])); + setTimesheet(prevTimesheet => + prevTimesheet.map(item => { + const updatedItem = responseMap.get(item.timesheet.id); + if (updatedItem) { return { ...item, timesheet: { ...item.timesheet, - status: newItem.status + status: updatedItem.status } }; } return item; - }); - setTimesheet(updatedData); - }) - .catch((error) => { - console.error('Error fetching timesheet:', error); - }); - }, [queryUpdateTimesheetStatus]) + }) + ); + console.log('Timesheet status updated successfully!'); + } catch (error) { + console.error('Error updating timesheet status:', error); + } + }, + [queryUpdateTimesheetStatus, setTimesheet, user] + ); const getStatusTimesheet = (items: TimesheetLog[] = []) => { const STATUS_MAP: Record = { @@ -196,15 +237,6 @@ export function useTimesheet({ } - const handleDeleteTimesheet = async (params: DeleteTimesheetParams) => { - try { - return await queryDeleteTimesheet(params); - } catch (error) { - console.error('Error deleting timesheet:', error); - throw error; - } - }; - const deleteTaskTimesheet = useCallback(async () => { if (!user) { throw new Error('User not authenticated'); @@ -213,18 +245,22 @@ export function useTimesheet({ throw new Error('No timesheet IDs provided for deletion'); } try { - await handleDeleteTimesheet({ + await queryDeleteTimesheet({ organizationId: user.employee.organizationId, tenantId: user.tenantId ?? "", logIds }); + setTimesheet(prevTimesheet => + prevTimesheet.filter(item => !logIds.includes(item.timesheet.id)) + ); + } catch (error) { console.error('Failed to delete timesheets:', error); throw error; } - }, - [user, queryDeleteTimesheet, logIds, handleDeleteTimesheet] // deepscan-disable-line - ); + }, [user, logIds, queryDeleteTimesheet, setTimesheet]); + + const timesheetElementGroup = useMemo(() => { if (timesheetGroupByDays === 'Daily') { return groupByDate(timesheet); @@ -238,7 +274,7 @@ export function useTimesheet({ useEffect(() => { getTaskTimesheet({ startDate, endDate }); - }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, timesheet]); + }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays]); return { loadingTimesheet, @@ -251,6 +287,10 @@ export function useTimesheet({ statusTimesheet: getStatusTimesheet(timesheet.flat()), updateTimesheetStatus, loadingUpdateTimesheetStatus, - puTimesheetStatus + puTimesheetStatus, + createTimesheet, + loadingCreateTimesheet, + updateTimesheet, + loadingUpdateTimesheet }; } diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts index 13952ec97..09368bbad 100644 --- a/apps/web/app/interfaces/timer/ITimerLog.ts +++ b/apps/web/app/interfaces/timer/ITimerLog.ts @@ -1,4 +1,5 @@ -import { ITeamTask } from "../ITask"; +import { ITeamTask, TimesheetStatus } from "../ITask"; +import { TimeLogType, TimerSource } from "../ITimer"; interface BaseEntity { id: string; @@ -69,7 +70,7 @@ interface Timesheet extends BaseEntity { lockedAt: string | null; editedAt: string | null; isBilled: boolean; - status: string; + status: TimesheetStatus; employeeId: string; approvedById: string | null; isEdited: boolean; @@ -87,8 +88,8 @@ export interface TimesheetLog extends BaseEntity { startedAt: string; stoppedAt: string; editedAt: string | null; - logType: "TRACKED" | "MANUAL"; - source: "WEB_TIMER" | "MOBILE_APP" | "DESKTOP_APP"; + logType: TimeLogType; + source: TimerSource; description: string; reason: string | null; isBillable: boolean; @@ -112,9 +113,6 @@ export interface TimesheetLog extends BaseEntity { export interface UpdateTimesheetStatus extends BaseEntity { - isActive: boolean; - isArchived: boolean; - archivedAt: string | null; duration: number; keyboard: number; mouse: number; @@ -137,3 +135,25 @@ export interface UpdateTimesheetStatus extends BaseEntity { employee: Employee; isEdited: boolean; } +export interface UpdateTimesheet extends Pick< + Partial, + | 'reason' + | 'organizationContactId' + | 'description' + | 'organizationTeamId' + | 'projectId' + | 'taskId' +>, + Pick< + TimesheetLog, + | 'id' + | 'startedAt' + | 'stoppedAt' + | 'tenantId' + | 'logType' + | 'source' + | 'employeeId' + | 'organizationId' + > { + isBillable: boolean; +} diff --git a/apps/web/app/services/client/api/timer/timer-log.ts b/apps/web/app/services/client/api/timer/timer-log.ts index 8e4682ef5..4e658918a 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -1,5 +1,5 @@ -import { TimesheetLog, ITimerStatus, IUpdateTimesheetStatus, UpdateTimesheetStatus } from '@app/interfaces'; -import { get, deleteApi, put } from '../../axios'; +import { TimesheetLog, ITimerStatus, IUpdateTimesheetStatus, UpdateTimesheetStatus, UpdateTimesheet } from '@app/interfaces'; +import { get, deleteApi, put, post } from '../../axios'; import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers'; export async function getTimerLogs( @@ -127,3 +127,31 @@ export function updateStatusTimesheetFromApi(data: IUpdateTimesheetStatus) { const tenantId = getTenantIdCookie(); return put(`/timesheet/status`, { ...data, organizationId }, { tenantId }); } + + +export function createTimesheetFromApi(data: UpdateTimesheet) { + const organizationId = getOrganizationIdCookie(); + const tenantId = getTenantIdCookie(); + if (!organizationId || !tenantId) { + throw new Error('Required parameters missing: organizationId and tenantId are required'); + } + try { + return post('/timesheet/time-log', { ...data, organizationId }, { tenantId }) + } catch (error) { + throw new Error('Failed to create timesheet log'); + } +} + +export function updateTimesheetFromAPi(params: UpdateTimesheet) { + const { id, ...data } = params + const organizationId = getOrganizationIdCookie(); + const tenantId = getTenantIdCookie(); + if (!organizationId || !tenantId) { + throw new Error('Required parameters missing: organizationId and tenantId are required'); + } + try { + return put(`/timesheet/time-log/${params.id}`, { ...data, organizationId }, { tenantId }) + } catch (error) { + throw new Error('Failed to create timesheet log'); + } +} diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index cc0f06a78..ef39e6abc 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -1,7 +1,7 @@ import { ITasksTimesheet } from '@app/interfaces/ITimer'; import { serverFetch } from '../fetch'; import qs from 'qs'; -import { TimesheetLog, UpdateTimesheetStatus } from '@/app/interfaces/timer/ITimerLog'; +import { TimesheetLog, UpdateTimesheet, UpdateTimesheetStatus } from '@/app/interfaces/timer/ITimerLog'; import { IUpdateTimesheetStatus } from '@/app/interfaces'; export type TTasksTimesheetStatisticsParams = { @@ -107,3 +107,24 @@ export function updateStatusTimesheetRequest(params: IUpdateTimesheetStatus, bea tenantId: params.tenantId, }) } + + +export function createTimesheetRequest(params: UpdateTimesheet, bearer_token: string) { + return serverFetch({ + path: '/timesheet/time-log', + method: 'POST', + body: { ...params }, + bearer_token, + tenantId: params.tenantId + }) +} + +export function updateTimesheetRequest(params: UpdateTimesheet, bearer_token: string) { + return serverFetch({ + path: `/timesheet/time-log/${params.id}`, + method: 'PUT', + body: { ...params }, + bearer_token, + tenantId: params.tenantId + }); +} diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts index 31270c973..19ad5da66 100644 --- a/apps/web/app/stores/time-logs.ts +++ b/apps/web/app/stores/time-logs.ts @@ -7,6 +7,7 @@ interface IFilterOption { label: string; } + export const timerLogsDailyReportState = atom([]); export const timesheetRapportState = atom([]) @@ -19,3 +20,4 @@ export const timesheetFilterStatusState = atom([]); export const timesheetDeleteState = atom([]); export const timesheetGroupByDayState = atom('Daily') export const timesheetUpdateStatus = atom([]) +export const timesheetUpdateState = atom(null) diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index 3860a9d19..fb844453c 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -155,8 +155,11 @@ export const columns: ColumnDef[] = [ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { const { isOpen, openModal, closeModal } = useModal(); - const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet } = useTimesheet({}); + + + const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet, updateTimesheetStatus } = useTimesheet({}); const { handleSelectRowTimesheet, selectTimesheet, setSelectTimesheet, timesheetGroupByDays, handleSelectRowByStatusAndDate } = useTimelogFilterOptions(); + const [isDialogOpen, setIsDialogOpen] = React.useState(false); const handleConfirm = () => { try { @@ -201,10 +204,15 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) { const handleSort = (key: string, order: SortOrder) => { console.log(`Sorting ${key} in ${order} order`); }; - const handleButtonClick = (action: StatusAction) => { + const handleButtonClick = async (action: StatusAction) => { switch (action) { case 'Approved': - // TODO: Implement approval logic + if (selectTimesheet.length > 0) { + await updateTimesheetStatus({ + status: 'APPROVED', + ids: selectTimesheet + }) + } break; case 'Denied': openModal(); @@ -220,7 +228,7 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
{ {t('common.EDIT')} - + {t('common.DELETE')} @@ -501,9 +509,26 @@ const TaskDetails = ({ description, name }: { description: string; name: string ); }; -export const StatusTask = ({ ids }: { ids: string }) => { +export const StatusTask = ({ timesheet }: { timesheet: TimesheetLog }) => { const t = useTranslations(); - const { updateTimesheetStatus } = useTimesheet({}); + const { updateTimesheetStatus, updateTimesheet } = useTimesheet({}); + const handleUpdateTimesheet = async (isBillable: boolean) => { + await updateTimesheet({ + id: timesheet.timesheetId, + isBillable: isBillable, + employeeId: timesheet.employeeId, + logType: timesheet.logType, + source: timesheet.source, + stoppedAt: timesheet.stoppedAt, + startedAt: timesheet.startedAt, + tenantId: timesheet.tenantId, + organizationId: timesheet.organizationId, + description: timesheet.description, + projectId: timesheet.projectId, + reason: timesheet.reason, + }); + }; + return ( <> @@ -513,10 +538,16 @@ export const StatusTask = ({ ids }: { ids: string }) => { {statusTable?.map((status, index) => ( - updateTimesheetStatus({ - status: status.label as TimesheetStatus, - ids: [ids] - })} key={index} textValue={status.label} className="cursor-pointer"> + { + try { + await updateTimesheetStatus({ + status: status.label as TimesheetStatus, + ids: [timesheet.timesheet.id] + }); + } catch (error) { + console.error('Failed to update timesheet status:'); + } + }} key={index} textValue={status.label} className="cursor-pointer">
{status.label} @@ -532,12 +563,16 @@ export const StatusTask = ({ ids }: { ids: string }) => { - + { + await handleUpdateTimesheet(true) + }} textValue={'Yes'} className="cursor-pointer">
{t('pages.timesheet.BILLABLE.YES')}
- + { + await handleUpdateTimesheet(false) + }} textValue={'No'} className="cursor-pointer">
{t('pages.timesheet.BILLABLE.NO')}