Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: Add time Entry Modal and Update Timesheet Status based on API response #3365

Merged
merged 7 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 243 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>
Comment on lines +245 to 257
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility for toggle buttons

While the dark mode styling is improved, the toggle buttons could benefit from better accessibility support.

Consider these accessibility improvements:

+ const ToggleButtonGroup = ({ children }: { children: React.ReactNode }) => (
+   <div 
+     role="group" 
+     aria-label="Billable status toggle"
+     className="flex items-center gap-3"
+   >
+     {children}
+   </div>
+ );

 export const ToggleButton = ({ isActive, onClick, label }: ToggleButtonProps) => (
   <button
     type="button"
     onClick={onClick}
     aria-pressed={isActive}
+    role="switch"
     className={clsxm(
       '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 dark:bg-primary-light' : 'bg-gray-200'
       )}
+      aria-hidden="true"
     />
     <span className={clsxm('', isActive && 'text-primary dark:text-primary-light')}>
       {label}
     </span>
   </button>
 )

Usage example:

- <div className="flex items-center gap-3">
+ <ToggleButtonGroup>
    <ToggleButton
      isActive={isBillable}
      onClick={() => setIsBillable(true)}
      label={t('pages.timesheet.BILLABLE.YES')}
    />
    <ToggleButton
      isActive={!isBillable}
      onClick={() => setIsBillable(false)}
      label={t('pages.timesheet.BILLABLE.NO')}
    />
- </div>
+ </ToggleButtonGroup>

Committable suggestion skipped: line range outside the PR's diff.

</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
}
Comment on lines 338 to 340
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle edge case when no plans exist

The toYear calculation could throw an error if sortedPlansByDateDesc is empty, as it would try to access the date of an undefined plan.

Consider this safer implementation:

-      endYear ||
-      new Date(sortedPlansByDateDesc?.[sortedPlansByDateDesc?.length - 1]?.date ?? Date.now()).getFullYear() +
-      10
+      endYear || new Date().getFullYear() + 10
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
new Date(sortedPlansByDateDesc?.[sortedPlansByDateDesc?.length - 1]?.date ?? Date.now()).getFullYear() +
10
10
}
endYear || new Date().getFullYear() + 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');
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

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) => {
Comment on lines +18 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider optimizing the time generation function

While the implementation is functional, there are several opportunities for improvement:

  1. The array is recreated on every call
  2. No validation for the interval parameter
  3. No AM/PM indication in the time format

Consider applying these improvements:

+const MINUTES_IN_DAY = 24 * 60;
+
+const generateTimeOptions = React.useMemo(() => (interval = 15) => {
+    if (interval <= 0 || interval > MINUTES_IN_DAY) {
+        throw new Error('Invalid interval value');
+    }
+    const totalSlots = MINUTES_IN_DAY / interval;
     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 period = hour >= 12 ? 'PM' : 'AM';
+        const displayHour = hour % 12 || 12;
+        return `${displayHour}:${minutes} ${period}`;
     });
-};
+}, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 MINUTES_IN_DAY = 24 * 60;
const generateTimeOptions = React.useMemo(() => (interval = 15) => {
if (interval <= 0 || interval > MINUTES_IN_DAY) {
throw new Error('Invalid interval value');
}
const totalSlots = MINUTES_IN_DAY / interval;
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');
const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour}:${minutes} ${period}`;
});
}, []);

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential typo in variable name 'puTimesheetStatus'

The variable puTimesheetStatus may be misspelled or incorrectly named. Please verify if it should be putTimesheetStatus or timesheetStatus.

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])

Comment on lines +141 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incomplete dependency array in useCallback for updateTimesheetStatus

The useCallback hook's dependency array for updateTimesheetStatus is missing dependencies. It should include user, timesheet, and setTimesheet since they are used within the callback.

Apply this diff to fix the dependency array:

 }, [queryUpdateTimesheetStatus
+  , user, timesheet, setTimesheet
 ])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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, user, timesheet, setTimesheet])

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]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Possible infinite loop due to timesheet in useEffect dependencies

Including timesheet in the dependency array of useEffect may cause an infinite loop, as getTaskTimesheet updates timesheet, triggering the effect repeatedly.

Consider removing timesheet from the dependency array if it's not necessary.

Apply this diff:

 }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays
- , timesheet
 ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, timesheet]);
}, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays]);


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
Comment on lines +251 to +254
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Inconsistent variable naming: 'puTimesheetStatus'

The variable puTimesheetStatus in the returned object may be incorrectly named. Ensure that the variable name matches its intended use, possibly putTimesheetStatus or updateTimesheetStatus.

};
}
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);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
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]);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved


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
}
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

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;
}
Comment on lines +114 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor to reduce code duplication with existing Timesheet interface

The new UpdateTimesheetStatus interface largely duplicates properties from the existing Timesheet interface. Consider refactoring to reuse the existing interface:

-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;
-}
+type TimesheetStatus = "DRAFT" | "PENDING" | "IN REVIEW" | "DENIED" | "APPROVED";
+
+export interface UpdateTimesheetStatus extends Omit<Timesheet, 'status'> {
+    status: TimesheetStatus;
+}

Committable suggestion skipped: line range outside the PR's diff.

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
Loading