Skip to content

Commit

Permalink
[Feat]: Display Timesheet Data and Refactor Code (#3342)
Browse files Browse the repository at this point in the history
* feat: display timesheet data and refactor code

* fix: codeRabbit

* Update apps/web/lib/features/task/task-card.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Ruslan Konviser <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 18, 2024
1 parent 5c1cada commit 0fdda8a
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 206 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ type ITimesheetButton = {
title?: string,
onClick?: () => void,
className?: string,
icon?: ReactNode
icon?: ReactNode,
disabled?: boolean

}
export const TimesheetButton = ({ className, icon, onClick, title }: ITimesheetButton) => {
export const TimesheetButton = ({ className, icon, onClick, title, disabled }: ITimesheetButton) => {
return (
<button onClick={onClick} className={clsxm("flex items-center gap-1 text-gray-400 font-normal leading-3", className)}>
<button disabled={disabled}
onClick={onClick}
className={clsxm("flex items-center gap-1 text-gray-400 font-normal leading-3", className)}>
<div className="w-[16px] h-[16px] text-[#293241]">
{icon}
</div>
Expand All @@ -23,32 +26,33 @@ export const TimesheetButton = ({ className, icon, onClick, title }: ITimesheetB
}


export type StatusType = "Pending" | "Approved" | "Rejected";
export type StatusAction = "Deleted" | "Approved" | "Rejected";
export type StatusType = "Pending" | "Approved" | "Denied";
export type StatusAction = "Deleted" | "Approved" | "Denied";


// eslint-disable-next-line @typescript-eslint/no-empty-function
export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, onClick: (action: StatusAction) => void) => {
export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, disabled: boolean, onClick: (action: StatusAction) => void) => {

const buttonsConfig: Record<StatusType, { icon: JSX.Element; title: string; action: StatusAction }[]> = {
Pending: [
{ icon: <FaClipboardCheck className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Rejected" },
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
],
Approved: [
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Rejected" },
{ icon: <IoClose className="!bg-[#2932417c] dark:!bg-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
],
Rejected: [
Denied: [
{ icon: <FaClipboardCheck className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" },
{ icon: <RiDeleteBin6Fill className="!text-[#2932417c] dark:!text-gray-400 rounded" />, title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" }
]
};

return (buttonsConfig[status] || buttonsConfig.Rejected).map((button, index) => (
return (buttonsConfig[status] || buttonsConfig.Denied).map((button, index) => (
<TimesheetButton
className="hover:underline"
className="hover:underline text-sm gap-2"
disabled={disabled}
key={index}
icon={button.icon}
onClick={() => onClick(button.action)}
Expand All @@ -60,5 +64,5 @@ export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, onC
export const statusTable: { label: StatusType; description: string }[] = [
{ label: "Pending", description: "Awaiting approval or review" },
{ label: "Approved", description: "The item has been approved" },
{ label: "Rejected", description: "The item has been rejected" },
{ label: "Denied", description: "The item has been rejected" },
];
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function TimesheetFilterDate({
</div>
)
}
<div className="border border-slate-100 dark:border-gray-800 my-1"></div>
{isVisible && <div className="border border-slate-100 dark:border-gray-800 my-1"></div>}
<div className="flex flex-col p-2">
{[
t('common.FILTER_TODAY'),
Expand Down
58 changes: 50 additions & 8 deletions apps/web/app/hooks/features/useTimesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useQuery } from '../useQuery';
import { useCallback, useEffect } from 'react';
import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi } from '@/app/services/client/api/timer/timer-log';
import moment from 'moment';
import { ITimeSheet } from '@/app/interfaces';
import { TimesheetLog, TimesheetStatus } from '@/app/interfaces';
import { useTimelogFilterOptions } from './useTimelogFilterOptions';

interface TimesheetParams {
Expand All @@ -15,25 +15,35 @@ interface TimesheetParams {

export interface GroupedTimesheet {
date: string;
tasks: ITimeSheet[];
tasks: TimesheetLog[];
}


interface DeleteTimesheetParams {
organizationId: string;
tenantId: string;
logIds: string[];
}

const groupByDate = (items: ITimeSheet[]): GroupedTimesheet[] => {

const groupByDate = (items: TimesheetLog[]): GroupedTimesheet[] => {
if (!items?.length) return [];
type GroupedMap = Record<string, ITimeSheet[]>;
type GroupedMap = Record<string, TimesheetLog[]>;

const groupedByDate = items.reduce<GroupedMap>((acc, item) => {
if (!item?.createdAt) return acc;
if (!item?.timesheet?.createdAt) {
console.warn('Skipping item with missing timesheet or createdAt:', item);
return acc;
}
try {
const date = new Date(item.createdAt).toISOString().split('T')[0];
const date = new Date(item.timesheet.createdAt).toISOString().split('T')[0];
if (!acc[date]) acc[date] = [];
acc[date].push(item);
} catch (error) {
console.error('Invalid date format:', item.createdAt);
console.error(
`Failed to process date for timesheet ${item.timesheet.id}:`,
{ createdAt: item.timesheet.createdAt, error }
);
}
return acc;
}, {});
Expand Down Expand Up @@ -83,6 +93,37 @@ export function useTimesheet({
]
);

const getStatusTimesheet = (items: TimesheetLog[] = []) => {
const STATUS_MAP: Record<TimesheetStatus, TimesheetLog[]> = {
PENDING: [],
APPROVED: [],
DENIED: [],
DRAFT: [],
'IN REVIEW': []
};

return items.reduce((acc, item) => {
const status = item.timesheet.status;
if (isTimesheetStatus(status)) {
acc[status].push(item);
} else {
console.warn(`Invalid timesheet status: ${status}`);
}
return acc;
}, STATUS_MAP);
}

// Type guard
function isTimesheetStatus(status: unknown): status is TimesheetStatus {
const timesheetStatusValues: TimesheetStatus[] = [
"DRAFT",
"PENDING",
"IN REVIEW",
"DENIED",
"APPROVED"
];
return Object.values(timesheetStatusValues).includes(status as TimesheetStatus);
}


const handleDeleteTimesheet = async (params: DeleteTimesheetParams) => {
Expand Down Expand Up @@ -126,6 +167,7 @@ export function useTimesheet({
timesheet: groupByDate(timesheet),
getTaskTimesheet,
loadingDeleteTimesheet,
deleteTaskTimesheet
deleteTaskTimesheet,
getStatusTimesheet
};
}
8 changes: 8 additions & 0 deletions apps/web/app/interfaces/ITask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ export type ITaskStatusField =
| 'tags'
| 'status type';

export type TimesheetStatus =
| "DRAFT"
| "PENDING"
| "IN REVIEW"
| "DENIED"
| "APPROVED";


export type ITaskStatusStack = {
status: ITaskStatus;
size: ITaskSize;
Expand Down
110 changes: 72 additions & 38 deletions apps/web/app/interfaces/timer/ITimerLog.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,99 @@
import { ITaskIssue } from "..";
import { ITeamTask } from "../ITask";

interface Project {
interface BaseEntity {
id: string;
name: string;
imageUrl: string;
membersCount: number;
image: string | null;
}

interface Task {
id: string;
title: string;
issueType?: ITaskIssue | null;
estimate: number | null;
taskStatus: string | null;
taskNumber: string;
isActive: boolean;
isArchived: boolean;
tenantId: string;
organizationId: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
archivedAt: string | null;
}

interface OrganizationContact {
id: string;
name: string;
imageUrl: string;
interface ImageEntity {
imageUrl: string | null;
image: string | null;
}

interface User {
id: string;
interface User extends BaseEntity {
firstName: string;
lastName: string;
imageUrl: string;
image: string | null;
name: string;
imageUrl: string | null;
image: string | null;
}

interface Employee {
id: string;
interface Employee extends BaseEntity {
isOnline: boolean;
isAway: boolean;
user: User;
fullName: string;
}

export interface ITimeSheet {
deletedAt: string | null;
id: string;
createdAt: string;
updatedAt: string;
isActive: boolean;
isArchived: boolean;
archivedAt: string | null;
tenantId: string;
organizationId: string;
interface TaskStatus extends BaseEntity {
name: string;
value: string;
description: string;
order: number;
icon: string;
color: string;
isSystem: boolean;
isCollapsed: boolean;
isDefault: boolean;
isTodo: boolean;
isInProgress: boolean;
isDone: boolean;
projectId: string | null;
organizationTeamId: string | null;
fullIconUrl: string;
}
interface Task extends ITeamTask {
taskStatus: TaskStatus | null,
number: number;
description: string;
startDate: string | null;
}


interface Timesheet extends BaseEntity {
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: string;
employeeId: string;
approvedById: string | null;
isEdited: boolean;
}
interface Project extends BaseEntity, ImageEntity {
name: string;
membersCount: number;
}

interface OrganizationContact extends BaseEntity, ImageEntity {
name: string;
}

export interface TimesheetLog extends BaseEntity {
startedAt: string;
stoppedAt: string;
editedAt: string | null;
logType: string;
source: string;
logType: "TRACKED" | "MANUAL";
source: "WEB_TIMER" | "MOBILE_APP" | "DESKTOP_APP";
description: string;
reason: string | null;
isBillable: boolean;
isRunning: boolean;
version: number | null;
version: string | null;
employeeId: string;
timesheetId: string;
projectId: string;
Expand All @@ -71,6 +104,7 @@ export interface ITimeSheet {
task: Task;
organizationContact: OrganizationContact;
employee: Employee;
timesheet: Timesheet,
duration: number;
isEdited: boolean;
}
8 changes: 5 additions & 3 deletions apps/web/app/services/client/api/timer/timer-log.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ITimeSheet, ITimerStatus } from '@app/interfaces';
import { TimesheetLog, ITimerStatus } from '@app/interfaces';
import { get, deleteApi } from '../../axios';

export async function getTimerLogs(
Expand Down Expand Up @@ -53,7 +53,9 @@ export async function getTaskTimesheetLogsApi({
'relations[1]': 'task',
'relations[2]': 'organizationContact',
'relations[3]': 'employee.user',
'relations[4]': 'task.taskStatus'
'relations[4]': 'task.taskStatus',
'relations[5]': 'timesheet'

});

projectIds.forEach((id, index) => {
Expand All @@ -64,7 +66,7 @@ export async function getTaskTimesheetLogsApi({
params.append(`employeeIds[${index}]`, id);
});
const endpoint = `/timesheet/time-log?${params.toString()}`;
return get<ITimeSheet[]>(endpoint, { tenantId });
return get<TimesheetLog[]>(endpoint, { tenantId });
}


Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/services/server/requests/timesheet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ITasksTimesheet } from '@app/interfaces/ITimer';
import { serverFetch } from '../fetch';
import qs from 'qs';
import { ITimeSheet } from '@/app/interfaces/timer/ITimerLog';
import { TimesheetLog } from '@/app/interfaces/timer/ITimerLog';

export type TTasksTimesheetStatisticsParams = {
tenantId: string;
Expand Down Expand Up @@ -72,7 +72,7 @@ type ITimesheetProps = {

export function getTaskTimesheetRequest(params: ITimesheetProps, bearer_token: string) {
const queries = qs.stringify(params);
return serverFetch<ITimeSheet[]>({
return serverFetch<TimesheetLog[]>({
path: `/timesheet/time-log?activityLevel?${queries.toString()}`,
method: 'GET',
bearer_token,
Expand All @@ -88,7 +88,7 @@ type IDeleteTimesheetProps = {

export function deleteTaskTimesheetRequest(params: IDeleteTimesheetProps, bearer_token: string) {
const { logIds = [] } = params;
return serverFetch<ITimeSheet[]>({
return serverFetch<TimesheetLog[]>({
path: `/timesheet/time-log/${logIds.join(',')}`,
method: 'DELETE',
bearer_token,
Expand Down
Loading

0 comments on commit 0fdda8a

Please sign in to comment.