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]: Display Timesheet Data and Refactor Code #3342

Merged
merged 3 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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";
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved


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