Merge branch 'release-v2.1.4' of https://github.com/Worklenz/worklenz into test/invitation-process

This commit is contained in:
shancds
2025-07-25 09:48:29 +05:30
8 changed files with 733 additions and 7 deletions

View File

@@ -0,0 +1,92 @@
import { API_BASE_URL } from '@/shared/constants';
import { toQueryString } from '@/utils/toQueryString';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.types';
import {
IProjectLogsBreakdown,
IRPTTimeMember,
IRPTTimeProject,
ITimeLogBreakdownReq,
} from '@/types/reporting/reporting.types';
const rootUrl = `${API_BASE_URL}/reporting`;
// Helper function to get user's timezone
const getUserTimezone = () => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
};
export const reportingTimesheetApiService = {
getTimeSheetData: async (
body = {},
archived = false
): Promise<IServerResponse<IAllocationViewModel>> => {
const q = toQueryString({ archived });
const bodyWithTimezone = {
...body,
timezone: getUserTimezone()
};
const response = await apiClient.post(`${rootUrl}/allocation/${q}`, bodyWithTimezone);
return response.data;
},
getAllocationProjects: async (body = {}) => {
const bodyWithTimezone = {
...body,
timezone: getUserTimezone()
};
const response = await apiClient.post(`${rootUrl}/allocation/allocation-projects`, bodyWithTimezone);
return response.data;
},
getProjectTimeSheets: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeProject[]>> => {
const q = toQueryString({ archived });
const bodyWithTimezone = {
...body,
timezone: getUserTimezone()
};
const response = await apiClient.post(`${rootUrl}/time-reports/projects/${q}`, bodyWithTimezone);
return response.data;
},
getMemberTimeSheets: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeMember[]>> => {
const q = toQueryString({ archived });
const bodyWithTimezone = {
...body,
timezone: getUserTimezone()
};
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, bodyWithTimezone);
return response.data;
},
getProjectTimeLogs: async (
body: ITimeLogBreakdownReq
): Promise<IServerResponse<IProjectLogsBreakdown[]>> => {
const bodyWithTimezone = {
...body,
timezone: getUserTimezone()
};
const response = await apiClient.post(`${rootUrl}/project-timelogs`, bodyWithTimezone);
return response.data;
},
getProjectEstimatedVsActual: async (
body = {},
archived = false
): Promise<IServerResponse<IRPTTimeProject[]>> => {
const q = toQueryString({ archived });
const bodyWithTimezone = {
...body,
timezone: getUserTimezone()
};
const response = await apiClient.post(`${rootUrl}/time-reports/estimated-vs-actual${q}`, bodyWithTimezone);
return response.data;
},
};

View File

@@ -0,0 +1,70 @@
import { useState, useEffect } from 'react';
/**
* Custom hook to get and manage user's timezone
* @returns {Object} Object containing timezone and related utilities
*/
export const useUserTimezone = () => {
const [timezone, setTimezone] = useState<string>('UTC');
const [timezoneOffset, setTimezoneOffset] = useState<string>('+00:00');
useEffect(() => {
// Get browser's timezone
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
setTimezone(browserTimezone);
// Calculate timezone offset
const date = new Date();
const offset = -date.getTimezoneOffset();
const hours = Math.floor(Math.abs(offset) / 60);
const minutes = Math.abs(offset) % 60;
const sign = offset >= 0 ? '+' : '-';
const formattedOffset = `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
setTimezoneOffset(formattedOffset);
}, []);
/**
* Format a date in the user's timezone
* @param date - Date to format
* @param format - Format options
* @returns Formatted date string
*/
const formatInUserTimezone = (date: Date | string, format?: Intl.DateTimeFormatOptions) => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return dateObj.toLocaleString('en-US', {
timeZone: timezone,
...format
});
};
/**
* Get the start of day in user's timezone
* @param date - Date to get start of day for
* @returns Date object representing start of day
*/
const getStartOfDayInTimezone = (date: Date = new Date()) => {
const localDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
localDate.setHours(0, 0, 0, 0);
return localDate;
};
/**
* Get the end of day in user's timezone
* @param date - Date to get end of day for
* @returns Date object representing end of day
*/
const getEndOfDayInTimezone = (date: Date = new Date()) => {
const localDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
localDate.setHours(23, 59, 59, 999);
return localDate;
};
return {
timezone,
timezoneOffset,
formatInUserTimezone,
getStartOfDayInTimezone,
getEndOfDayInTimezone,
setTimezone
};
};

View File

@@ -0,0 +1,67 @@
import { Card, Flex, Typography, Tooltip } from '@/shared/antd-imports';
import TimeReportPageHeader from '@/pages/reporting/timeReports/page-header/time-report-page-header';
import MembersTimeSheet, {
MembersTimeSheetRef,
} from '@/pages/reporting/time-reports/members-time-sheet/members-time-sheet';
import TimeReportingRightHeader from './timeReportingRightHeader/TimeReportingRightHeader';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { useRef } from 'react';
import { useUserTimezone } from '@/hooks/useUserTimezone';
import { InfoCircleOutlined } from '@ant-design/icons';
const { Text } = Typography;
const MembersTimeReports = () => {
const { t } = useTranslation('time-report');
const chartRef = useRef<MembersTimeSheetRef>(null);
const { timezone, timezoneOffset } = useUserTimezone();
useDocumentTitle('Reporting - Allocation');
const handleExport = (type: string) => {
if (type === 'png') {
chartRef.current?.exportChart();
}
};
return (
<Flex vertical>
<TimeReportingRightHeader
title={t('Members Time Sheet')}
exportType={[{ key: 'png', label: 'PNG' }]}
export={handleExport}
/>
<Card
style={{ borderRadius: '4px' }}
title={
<div style={{ padding: '16px 0' }}>
<Flex justify="space-between" align="center">
<TimeReportPageHeader />
<Tooltip
title={`All time logs are displayed in your local timezone. Times were logged by users in their respective timezones and converted for your viewing.`}
>
<Text type="secondary" style={{ fontSize: 12 }}>
<InfoCircleOutlined style={{ marginRight: 4 }} />
Timezone: {timezone} ({timezoneOffset})
</Text>
</Tooltip>
</Flex>
</div>
}
styles={{
body: {
maxHeight: 'calc(100vh - 300px)',
overflowY: 'auto',
padding: '16px',
},
}}
>
<MembersTimeSheet ref={chartRef} />
</Card>
</Flex>
);
};
export default MembersTimeReports;