From f1e911e430fa27363f86cbb73752abdbd11476d4 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 05:15:53 +0200 Subject: [PATCH 01/12] Add createTimesheetFromApi function to create a timesheet entry from API --- .../app/api/timer/timesheet/time-log/route.ts | 31 +++++++++++++++++ apps/web/app/hooks/features/useTimesheet.ts | 26 ++++++++++++--- apps/web/app/interfaces/timer/ITimerLog.ts | 33 +++++++++++++++---- .../services/client/api/timer/timer-log.ts | 11 +++++-- .../app/services/server/requests/timesheet.ts | 13 +++++++- apps/web/app/stores/time-logs.ts | 1 + .../calendar/table-time-sheet.tsx | 22 +++++++++---- 7 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 apps/web/app/api/timer/timesheet/time-log/route.ts 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..26e97bb69 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 } 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 { @@ -104,6 +104,7 @@ 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: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(createTimesheetFromApi) const getTaskTimesheet = useCallback( @@ -138,6 +139,19 @@ export function useTimesheet({ ] ); + const createTimesheet = useCallback(({ ...timesheetParams }: UpdateTimesheet) => { + if (!user) return; + queryUpdateTimesheet(timesheetParams).then((response) => { + console.log('Response data:', response.data); + setTimesheet((prevTimesheet) => { + const updatedTimesheet = prevTimesheet ? [{ ...response.data, ...prevTimesheet }] : [{ ...response.data }]; + return updatedTimesheet; + }); + }); + }, [queryUpdateTimesheet, user]); + + + const updateTimesheetStatus = useCallback( ({ status, ids }: { status: TimesheetStatus, ids: ID[] | ID }) => { if (!user) return; @@ -161,7 +175,7 @@ export function useTimesheet({ .catch((error) => { console.error('Error fetching timesheet:', error); }); - }, [queryUpdateTimesheetStatus]) + }, [queryUpdateTimesheetStatus, setTimesheet]) const getStatusTimesheet = (items: TimesheetLog[] = []) => { const STATUS_MAP: Record = { @@ -238,7 +252,7 @@ export function useTimesheet({ useEffect(() => { getTaskTimesheet({ startDate, endDate }); - }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, timesheet]); + }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays]); return { loadingTimesheet, @@ -251,6 +265,8 @@ export function useTimesheet({ statusTimesheet: getStatusTimesheet(timesheet.flat()), updateTimesheetStatus, loadingUpdateTimesheetStatus, - puTimesheetStatus + puTimesheetStatus, + createTimesheet, + loadingUpdateTimesheet }; } diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts index 13952ec97..5a09c6df8 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: "WEB_TIMER" | "MOBILE_APP" | "DESKTOP_APP" | 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,24 @@ export interface UpdateTimesheetStatus extends BaseEntity { employee: Employee; isEdited: boolean; } +export interface UpdateTimesheet extends Pick< + Partial, + | 'reason' + | 'organizationContactId' + | 'description' + | 'organizationTeamId' + | 'projectId' + | 'taskId' +>, + Pick< + TimesheetLog, + | '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..b11a36f74 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,10 @@ 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(); + return post('timesheet/time-log', { ...data, organizationId }, { tenantId }) +} diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index cc0f06a78..513edaa4b 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,14 @@ 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 + }) +} diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts index 31270c973..4e8a2e68c 100644 --- a/apps/web/app/stores/time-logs.ts +++ b/apps/web/app/stores/time-logs.ts @@ -19,3 +19,4 @@ export const timesheetFilterStatusState = atom([]); export const timesheetDeleteState = atom([]); export const timesheetGroupByDayState = atom('Daily') export const timesheetUpdateStatus = atom([]) +export const timesheetUpdateState = atom() 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..534496e85 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -62,7 +62,7 @@ import { useTranslations } from 'next-intl'; import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; -import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { TimeLogType, TimesheetLog, TimesheetStatus } from '@/app/interfaces'; export const columns: ColumnDef[] = [ { @@ -477,7 +477,7 @@ const TaskActionMenu = ({ dataTimesheet }: { dataTimesheet: TimesheetLog }) => { {t('common.EDIT')} - + {t('common.DELETE')} @@ -501,9 +501,10 @@ 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, createTimesheet } = useTimesheet({}); + return ( <> @@ -515,7 +516,7 @@ export const StatusTask = ({ ids }: { ids: string }) => { {statusTable?.map((status, index) => ( updateTimesheetStatus({ status: status.label as TimesheetStatus, - ids: [ids] + ids: [timesheet.timesheet.id] })} key={index} textValue={status.label} className="cursor-pointer">
@@ -532,12 +533,19 @@ export const StatusTask = ({ ids }: { ids: string }) => { - + createTimesheet({ + ...timesheet, isBillable: true, + logType: TimeLogType.MANUAL + })} textValue={'Yes'} className="cursor-pointer">
{t('pages.timesheet.BILLABLE.YES')}
- + createTimesheet({ + ...timesheet, + isBillable: false, + logType: TimeLogType.MANUAL + })} textValue={'No'} className="cursor-pointer">
{t('pages.timesheet.BILLABLE.NO')}
From 7f9a67e296b87441d2a267eea0fbebbe93930a49 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 05:19:29 +0200 Subject: [PATCH 02/12] fix:update url --- apps/web/app/services/client/api/timer/timer-log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b11a36f74..230fe6dac 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -132,5 +132,5 @@ export function updateStatusTimesheetFromApi(data: IUpdateTimesheetStatus) { export function createTimesheetFromApi(data: UpdateTimesheet) { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - return post('timesheet/time-log', { ...data, organizationId }, { tenantId }) + return post('/timesheet/time-log', { ...data, organizationId }, { tenantId }) } From 8bdd7c9ad6f7058b8a4f7188ee396da12d29ed80 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 09:13:55 +0200 Subject: [PATCH 03/12] refactor: optimize deleteTimesheet and updateTimesheetStatus function for better readability --- apps/web/app/hooks/features/useTimesheet.ts | 77 ++++++++++++------- .../calendar/table-time-sheet.tsx | 15 +--- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 26e97bb69..20e0aab85 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -104,7 +104,7 @@ 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: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(createTimesheetFromApi) + const { loading: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi) const getTaskTimesheet = useCallback( @@ -139,43 +139,58 @@ export function useTimesheet({ ] ); - const createTimesheet = useCallback(({ ...timesheetParams }: UpdateTimesheet) => { - if (!user) return; - queryUpdateTimesheet(timesheetParams).then((response) => { - console.log('Response data:', response.data); - setTimesheet((prevTimesheet) => { - const updatedTimesheet = prevTimesheet ? [{ ...response.data, ...prevTimesheet }] : [{ ...response.data }]; - return updatedTimesheet; - }); - }); - }, [queryUpdateTimesheet, user]); + const createTimesheet = useCallback( + async ({ ...timesheetParams }: UpdateTimesheet) => { + if (!user) { + console.error("User not authenticated"); + return; + } + try { + const response = await queryCreateTimesheet(timesheetParams); + console.log("Timesheet created successfully:", response.data); + setTimesheet((prevTimesheet) => [ + response.data, + ...(prevTimesheet || []) + ]); + } catch (error) { + console.error("Error creating timesheet:", error); + } + }, + [queryCreateTimesheet, 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, setTimesheet]) + }) + ); + 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 = { @@ -232,13 +247,17 @@ export function useTimesheet({ 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, queryDeleteTimesheet, logIds, handleDeleteTimesheet, setTimesheet]); + + const timesheetElementGroup = useMemo(() => { if (timesheetGroupByDays === 'Daily') { return groupByDate(timesheet); @@ -267,6 +286,6 @@ export function useTimesheet({ loadingUpdateTimesheetStatus, puTimesheetStatus, createTimesheet, - loadingUpdateTimesheet + loadingCreateTimesheet }; } 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 534496e85..15e1897af 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -62,7 +62,7 @@ import { useTranslations } from 'next-intl'; import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; -import { TimeLogType, TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; export const columns: ColumnDef[] = [ { @@ -503,7 +503,7 @@ const TaskDetails = ({ description, name }: { description: string; name: string export const StatusTask = ({ timesheet }: { timesheet: TimesheetLog }) => { const t = useTranslations(); - const { updateTimesheetStatus, createTimesheet } = useTimesheet({}); + const { updateTimesheetStatus } = useTimesheet({}); return ( <> @@ -533,19 +533,12 @@ export const StatusTask = ({ timesheet }: { timesheet: TimesheetLog }) => { - createTimesheet({ - ...timesheet, isBillable: true, - logType: TimeLogType.MANUAL - })} textValue={'Yes'} className="cursor-pointer"> +
{t('pages.timesheet.BILLABLE.YES')}
- createTimesheet({ - ...timesheet, - isBillable: false, - logType: TimeLogType.MANUAL - })} textValue={'No'} className="cursor-pointer"> +
{t('pages.timesheet.BILLABLE.NO')}
From bf2a88f44899bf6bf2f6c0d20a113e58eeb3380b Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 09:49:36 +0200 Subject: [PATCH 04/12] fix: deep scan --- apps/web/app/hooks/features/useTimesheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 20e0aab85..5655b3e98 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -255,7 +255,7 @@ export function useTimesheet({ console.error('Failed to delete timesheets:', error); throw error; } - }, [user, queryDeleteTimesheet, logIds, handleDeleteTimesheet, setTimesheet]); + }, [user, queryDeleteTimesheet, logIds, setTimesheet]); const timesheetElementGroup = useMemo(() => { From 6b412de08dbfa2fbfbee86ac2950bd5cfafa0a84 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 09:55:29 +0200 Subject: [PATCH 05/12] fix --- apps/web/app/hooks/features/useTimesheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 5655b3e98..dbbc654bb 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -255,7 +255,7 @@ export function useTimesheet({ console.error('Failed to delete timesheets:', error); throw error; } - }, [user, queryDeleteTimesheet, logIds, setTimesheet]); + }, [user, logIds, handleDeleteTimesheet, setTimesheet]); const timesheetElementGroup = useMemo(() => { From 66315d0f5671db8ffa2ddfd93eceb869fe76a729 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 10:39:44 +0200 Subject: [PATCH 06/12] fix: refactor some code --- apps/web/app/hooks/features/useTimesheet.ts | 6 ++-- apps/web/app/interfaces/timer/ITimerLog.ts | 2 +- .../services/client/api/timer/timer-log.ts | 9 +++++- apps/web/app/stores/time-logs.ts | 3 +- .../calendar/table-time-sheet.tsx | 30 ++++++++++++++----- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index dbbc654bb..a26dfa022 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -142,18 +142,16 @@ export function useTimesheet({ const createTimesheet = useCallback( async ({ ...timesheetParams }: UpdateTimesheet) => { if (!user) { - console.error("User not authenticated"); - return; + throw new Error("User not authenticated"); } try { const response = await queryCreateTimesheet(timesheetParams); - console.log("Timesheet created successfully:", response.data); setTimesheet((prevTimesheet) => [ response.data, ...(prevTimesheet || []) ]); } catch (error) { - console.error("Error creating timesheet:", error); + throw error; } }, [queryCreateTimesheet, setTimesheet, user] diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts index 5a09c6df8..0a26c7d4c 100644 --- a/apps/web/app/interfaces/timer/ITimerLog.ts +++ b/apps/web/app/interfaces/timer/ITimerLog.ts @@ -89,7 +89,7 @@ export interface TimesheetLog extends BaseEntity { stoppedAt: string; editedAt: string | null; logType: TimeLogType; - source: "WEB_TIMER" | "MOBILE_APP" | "DESKTOP_APP" | TimerSource; + source: TimerSource; description: string; reason: string | null; 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 230fe6dac..cb14b980e 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -132,5 +132,12 @@ export function updateStatusTimesheetFromApi(data: IUpdateTimesheetStatus) { export function createTimesheetFromApi(data: UpdateTimesheet) { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - return post('/timesheet/time-log', { ...data, organizationId }, { tenantId }) + 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'); + } } diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts index 4e8a2e68c..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,4 +20,4 @@ export const timesheetFilterStatusState = atom([]); export const timesheetDeleteState = atom([]); export const timesheetGroupByDayState = atom('Daily') export const timesheetUpdateStatus = atom([]) -export const timesheetUpdateState = 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 15e1897af..c468c48e5 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[] }) {
{ {statusTable?.map((status, index) => ( - updateTimesheetStatus({ - status: status.label as TimesheetStatus, - ids: [timesheet.timesheet.id] - })} 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} From 33c48980f30d7a05a3abe932b60d7518875dad74 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 10:45:59 +0200 Subject: [PATCH 07/12] fix: deep scan --- apps/web/app/hooks/features/useTimesheet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index a26dfa022..fdff2283a 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -151,7 +151,7 @@ export function useTimesheet({ ...(prevTimesheet || []) ]); } catch (error) { - throw error; + console.error('Error:', error); } }, [queryCreateTimesheet, setTimesheet, user] @@ -253,7 +253,7 @@ export function useTimesheet({ console.error('Failed to delete timesheets:', error); throw error; } - }, [user, logIds, handleDeleteTimesheet, setTimesheet]); + }, [user, logIds, queryDeleteTimesheet, setTimesheet]); const timesheetElementGroup = useMemo(() => { From 5e360154dab8e6bd85b23b93306a4a4b0d001a28 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 10:59:26 +0200 Subject: [PATCH 08/12] fix: Codacy Static Code Analysis --- apps/web/app/hooks/features/useTimesheet.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index fdff2283a..a3e37bfcf 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -223,15 +223,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'); @@ -240,7 +231,7 @@ 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 From 201e4a234e030c975d794036dbd76a2431545dc5 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 11:06:45 +0200 Subject: [PATCH 09/12] fix: build --- apps/web/app/hooks/features/useTimesheet.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index a3e37bfcf..ea494dd24 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -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; From ba054261e41d4677b3a19cdb779ae81a1daeef54 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 19:42:31 +0200 Subject: [PATCH 10/12] feat: update billable from api --- .../timer/timesheet/time-log/[id]/route.ts | 33 +++++++++++++++++++ apps/web/app/hooks/features/useTimesheet.ts | 18 ++++++++-- apps/web/app/interfaces/timer/ITimerLog.ts | 1 + .../services/client/api/timer/timer-log.ts | 14 ++++++++ .../app/services/server/requests/timesheet.ts | 10 ++++++ .../calendar/table-time-sheet.tsx | 28 +++++++++++++--- 6 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 apps/web/app/api/timer/timesheet/time-log/[id]/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/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index ea494dd24..69a5f1b92 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -3,7 +3,7 @@ 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, createTimesheetFromApi } 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, UpdateTimesheet } from '@/app/interfaces'; import { useTimelogFilterOptions } from './useTimelogFilterOptions'; @@ -97,7 +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: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi); + const { loading: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(updateTimesheetFromAPi); const getTaskTimesheet = useCallback( @@ -152,6 +153,15 @@ export function useTimesheet({ + const updateTimesheet = useCallback(async ({ ...timesheet }: UpdateTimesheet) => { + if (!user) return; + const response = await queryUpdateTimesheet(timesheet); + setTimesheet(prevTimesheet => [ + response.data, + ...prevTimesheet, + ]) + }, [queryUpdateTimesheet, setTimesheet, user]) + const updateTimesheetStatus = useCallback( async ({ status, ids }: { status: TimesheetStatus; ids: ID[] | ID }) => { @@ -268,6 +278,8 @@ export function useTimesheet({ loadingUpdateTimesheetStatus, puTimesheetStatus, createTimesheet, - loadingCreateTimesheet + loadingCreateTimesheet, + updateTimesheet, + loadingUpdateTimesheet }; } diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts index 0a26c7d4c..09368bbad 100644 --- a/apps/web/app/interfaces/timer/ITimerLog.ts +++ b/apps/web/app/interfaces/timer/ITimerLog.ts @@ -146,6 +146,7 @@ export interface UpdateTimesheet extends Pick< >, Pick< TimesheetLog, + | 'id' | 'startedAt' | 'stoppedAt' | 'tenantId' 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 cb14b980e..4e658918a 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -141,3 +141,17 @@ export function createTimesheetFromApi(data: UpdateTimesheet) { 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 513edaa4b..ef39e6abc 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -118,3 +118,13 @@ export function createTimesheetRequest(params: UpdateTimesheet, bearer_token: st 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/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index c468c48e5..4df5a38b8 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -62,7 +62,7 @@ import { useTranslations } from 'next-intl'; import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; -import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { TimeLogType, TimesheetLog, TimesheetStatus } from '@/app/interfaces'; export const columns: ColumnDef[] = [ { @@ -511,7 +511,23 @@ const TaskDetails = ({ description, name }: { description: string; name: 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 ( <> @@ -547,12 +563,16 @@ export const StatusTask = ({ timesheet }: { timesheet: TimesheetLog }) => { - + { + 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')}
From 474f18b06d8c3b93614c7ed043eeff6e24af8d7c Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 19:57:04 +0200 Subject: [PATCH 11/12] fix: conflits --- .../web/lib/features/integrations/calendar/table-time-sheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4df5a38b8..fb844453c 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -62,7 +62,7 @@ import { useTranslations } from 'next-intl'; import { formatDate } from '@/app/helpers'; import { GroupedTimesheet, useTimesheet } from '@/app/hooks/features/useTimesheet'; import { DisplayTimeForTimesheet, TaskNameInfoDisplay, TotalDurationByDate, TotalTimeDisplay } from '../../task/task-displays'; -import { TimeLogType, TimesheetLog, TimesheetStatus } from '@/app/interfaces'; +import { TimesheetLog, TimesheetStatus } from '@/app/interfaces'; export const columns: ColumnDef[] = [ { From 6bfbfbaa50912ba1a2fb768bce48fc67140b5d91 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 27 Nov 2024 20:02:11 +0200 Subject: [PATCH 12/12] fix: coderabbitai --- apps/web/app/hooks/features/useTimesheet.ts | 27 +++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 69a5f1b92..4afc705b2 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -153,14 +153,25 @@ export function useTimesheet({ - const updateTimesheet = useCallback(async ({ ...timesheet }: UpdateTimesheet) => { - if (!user) return; - const response = await queryUpdateTimesheet(timesheet); - setTimesheet(prevTimesheet => [ - response.data, - ...prevTimesheet, - ]) - }, [queryUpdateTimesheet, 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(