Skip to content
Closed
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 @@ -120,9 +120,11 @@ describe('ExtensionStatusModal Component', () => {
});

test('should test formatToRelativeTime function', () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
const timestamp = 1640995200;
const result = formatToRelativeTime(timestamp);
expect(result).toBe('3 years ago');
jest.useRealTimers();
});

test('should open extension request form when request extension button is clicked', () => {
Expand Down
7 changes: 6 additions & 1 deletion __tests__/Unit/Components/Tasks/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
NEEDS_REVIEW,
VERIFIED,
} from '@/constants/task-status';
import moment from 'moment';

const DEFAULT_PROPS = {
content: {
Expand Down Expand Up @@ -537,6 +538,9 @@ describe('Task card', () => {
});

it('renders "Started" with a specific date if status is not AVAILABLE', () => {
const originalFromNow = moment.prototype.fromNow;
moment.prototype.fromNow = jest.fn(() => '4 years ago');

const { getByTestId } = renderWithRouter(
<Provider store={store()}>
<Card
Expand All @@ -548,7 +552,8 @@ describe('Task card', () => {
{}
);
const spanElement = screen.getByTestId('started-on');
expect(spanElement).toHaveTextContent('Started 4 years ago'); // Mocked date from moment
expect(spanElement).toHaveTextContent('Started 4 years ago');
moment.prototype.fromNow = originalFromNow;
});
it('Should show the status of the task', () => {
renderWithRouter(
Expand Down
1 change: 1 addition & 0 deletions src/app/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const api = createApi({
'User_Standup',
'TASK_REQUEST',
'Extension_Requests',
'Logs',
],
/**
* This api has endpoints injected in adjacent files,
Expand Down
42 changes: 42 additions & 0 deletions src/app/services/logsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { api } from './api';

export interface LogEntry {
Copy link
Contributor

Choose a reason for hiding this comment

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

  • any specific reason we are using interface over type

user?: string;
requestId: string;
from: number;
until: number;
type: string;
timestamp: number;
message?: string;
}

export interface LogsResponse {
message: string;
data: LogEntry[];
next: string | null;
prev: string | null;
}

export interface LogsQueryArgs {
username: string;
dev?: boolean;
format?: string;
type?: string;
}

export const logsApi = api.injectEndpoints({
endpoints: (build) => ({
getLogsByUsername: build.query<LogsResponse, LogsQueryArgs>({
query: ({
username,
dev = false,
format = 'feed',
type = 'REQUEST_CREATED',
}) =>
`/logs?dev=${dev}&format=${format}&type=${type}&username=${username}`,
providesTags: ['Logs'],
}),
}),
});

export const { useGetLogsByUsernameQuery } = logsApi;
57 changes: 47 additions & 10 deletions src/components/Calendar/UserSearchField.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

  • this search field doing so much ideally we should just search the user as the name suggested and pass to parent component

Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import { useState, useEffect, ChangeEvent, useRef } from 'react';
import classNames from './UserSearchField.module.scss';
import { useGetAllUsersQuery } from '@/app/services/usersApi';
import { useGetLogsByUsernameQuery } from '@/app/services/logsApi';
import { logs } from '@/constants/calendar';
import { userDataType } from '@/interfaces/user.type';
import { useOutsideAlerter } from '@/hooks/useOutsideAlerter';
import { LogEntry } from '@/app/services/logsApi';
import { LOG_DATA } from '@/utils/userStatusCalendar';

type SearchFieldProps = {
onSearchTextSubmitted: (user: userDataType | undefined, data: any) => void;
onSearchTextSubmitted: (
user: userDataType | undefined,
data: Array<{ userId: string; data: LOG_DATA[] }>,
oooLogsData?: LogEntry[]
) => void;
loading: boolean;
dev?: boolean;
};

const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => {
const SearchField = ({
onSearchTextSubmitted,
loading,
dev = false,
}: SearchFieldProps) => {
const handleOutsideClick = () => {
setDisplayList([]);
};
const suggestionInputRef = useRef(null);
useOutsideAlerter(suggestionInputRef, handleOutsideClick);
const [searchText, setSearchText] = useState<string>('');
const [selectedUser, setSelectedUser] = useState<userDataType | null>(null);
const lastProcessedUsername = useRef<string | null>(null);

const onSearchTextChanged = (e: ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
filterUser(e.target.value);
Expand All @@ -28,32 +43,54 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => {
const user = usersList.find(
(user: userDataType) => user.username === searchText
);
setSelectedUser(user || null);
lastProcessedUsername.current = null;
onSearchTextSubmitted(user, data);
};

const { data: userData, isError, isLoading } = useGetAllUsersQuery();
const [usersList, setUsersList] = useState<userDataType[]>([]);
const [displayList, setDisplayList] = useState<userDataType[]>([]);
const [data, setData] = useState([]);
const [data, setData] = useState<
Array<{ userId: string; data: LOG_DATA[] }>
Copy link
Contributor

Choose a reason for hiding this comment

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

define it type please

  • type UserLogData = Array<{ userId: string; data: LOG_DATA[] }>;

>([]);

const { data: logsData } = useGetLogsByUsernameQuery(
{ username: selectedUser?.username || '' },
{
skip: !dev || !selectedUser?.username,
}
);

useEffect(() => {
if (userData?.users) {
const users: userDataType[] = userData.users;
const filteredUsers: userDataType[] = users.filter(
(user: userDataType) => !user.incompleteUserDetails
);
const logData: any = filteredUsers.map((user: userDataType) => {
const log = logs[Math.floor(Math.random() * 4)];
return {
data: log,
userId: user.id,
};
});
const logData: Array<{ userId: string; data: LOG_DATA[] }> =
filteredUsers.map((user: userDataType) => {
const log = logs[Math.floor(Math.random() * 4)];
return {
Comment on lines +73 to +74
Copy link
Contributor

Choose a reason for hiding this comment

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

also what is this math.random doing here

data: log,
userId: user.id,
};
});
setData(logData);
setUsersList(filteredUsers);
}
}, [isLoading, userData]);
Comment on lines 65 to 82
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this file, it seems that we’re currently fetching all users from the database and then filtering them locally. Instead of doing that, I’d suggest using the user search API to fetch users directly based on their type.

For example, if we have 200 active users in the database but the getAllUsers only returns 100 users due to pagination, we might never find the intended user. Using the search API will make this process more efficient and reliable.


useEffect(() => {
const username = selectedUser?.username;

if (!dev || !logsData?.data || !username) return;
if (lastProcessedUsername.current === username) return;

lastProcessedUsername.current = username;
onSearchTextSubmitted(selectedUser, data, logsData.data);
}, [dev, logsData, selectedUser, data]);

const isValidUsername = () => {
const usernames = usersList.map((user: userDataType) => user.username);
if (usernames.includes(searchText)) {
Expand Down
1 change: 1 addition & 0 deletions src/constants/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export const DASHBOARD_URL = 'https://dashboard.realdevsquad.com';
export const USER_MANAGEMENT_URL = `${DASHBOARD_URL}/users/details/`;
export const TASK_REQUESTS_DETAILS_URL = `${DASHBOARD_URL}/task-requests/details/`;
export const TASK_EXTENSION_REQUEST_URL = `${DASHBOARD_URL}/extension-requests/`;
export const OOO_REQUEST_DETAILS_URL = `${DASHBOARD_URL}/requests/`;
134 changes: 109 additions & 25 deletions src/pages/calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,93 @@
import { FC, useState } from 'react';
import { useRouter } from 'next/router';
import Head from '@/components/head';
import Layout from '@/components/Layout';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import { SearchField } from '@/components/Calendar/UserSearchField';
import { processData } from '@/utils/userStatusCalendar';
import { processData, OOOEntry } from '@/utils/userStatusCalendar';
import { formatTimestampToDate } from '@/utils/time';
import { OOO_REQUEST_DETAILS_URL } from '@/constants/url';
import { MONTHS } from '@/constants/calendar';
import { userDataType } from '@/interfaces/user.type';

type ProcessCalendarData = [
Map<number, string>,
Map<number, string>,
Map<number, OOOEntry[]>
];

const UserStatusCalendar: FC = () => {
const [selectedDate, onDateChange] = useState<Date>(new Date());
const [selectedUser, setSelectedUser]: any = useState(null);
const [processedData, setProcessedData] = useState<any>(
processData(selectedUser ? selectedUser.id : null, [])
);
const router = useRouter();
const { dev } = router.query;
const isDevMode = dev === 'true';

const [message, setMessage]: any = useState(null);
const [loading, setLoading]: any = useState(false);
const [selectedDate, onDateChange] = useState<Date>(new Date());
Copy link
Contributor

Choose a reason for hiding this comment

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

  • follow React convention for naming state

const [selectedUser, setSelectedUser] = useState<userDataType | null>(null);
const [processedData, setProcessedData] = useState<ProcessCalendarData>([
new Map(),
new Map(),
new Map(),
]);
const [message, setMessage] = useState<string | JSX.Element | null>(null);

const setTileClassName = ({ activeStartDate, date, view }: any) => {
const setTileClassName = ({ date }: { date: Date }) => {
if (date.getDay() === 0) return 'sunday';
return processedData[0] ? processedData[0][date.getTime()] : null;

if (processedData[2].has(date.getTime())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

where is this processedData[2] , like what is the point of creating map

return 'OOO';
}

return processedData[0].get(date.getTime()) || null;
};

const formatOOOMessage = (oooEntries: OOOEntry[]): JSX.Element => {
return (
<>
{oooEntries.map((entry, index) => (
<div key={entry.requestId}>
{index > 0 && <div style={{ marginTop: '10px' }} />}
<div>From: {formatTimestampToDate(entry.from)}</div>
<div>Until: {formatTimestampToDate(entry.until)}</div>
<div>
Request ID:{' '}
<a
href={`${OOO_REQUEST_DETAILS_URL}${entry.requestId}`}
target="_blank"
rel="noopener noreferrer"
>
{entry.requestId}
</a>
</div>
{entry.message && <div>Message: {entry.message}</div>}
</div>
))}
</>
);
};

const formatOOODayMessage = (
value: Date,
selectedUser: userDataType | null,
oooEntries: OOOEntry[]
): JSX.Element => {
const dateStr = `${value.getDate()}-${
MONTHS[value.getMonth()]
}-${value.getFullYear()}`;
const userLine = `${selectedUser?.username} is OOO on ${dateStr}`;
const oooDetails = formatOOOMessage(oooEntries);
return (
<div>
<div>{userLine}</div>
<div style={{ marginTop: '10px' }}>{oooDetails}</div>
</div>
);
};

const handleDayClick = (value: Date, event: any) => {
const handleDayClick = (
value: Date,
event: React.MouseEvent<HTMLButtonElement>
) => {
if (value.getDay() === 0) {
setMessage(
`${value.getDate()}-${
Expand All @@ -31,36 +96,47 @@ const UserStatusCalendar: FC = () => {
);
return;
}

const oooEntries = processedData[2].get(value.getTime());
if (oooEntries) {
const message = formatOOODayMessage(
Copy link
Contributor

Choose a reason for hiding this comment

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

  • nit use different name for variable, we already have a state variable with name message

value,
selectedUser,
oooEntries
);
setMessage(message);
return;
}

if (event.currentTarget.classList.contains('OOO')) {
setMessage(
`${selectedUser.username} is OOO on ${value.getDate()}-${
`${selectedUser?.username} is OOO on ${value.getDate()}-${
Copy link
Contributor

Choose a reason for hiding this comment

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

  • this piece of code ${value.getDate()}-${MONTHS[value.getMonth()]}-${value.getFullYear()} is used in around 3 4 places in this file , can we please make a small util function and reuse it

MONTHS[value.getMonth()]
}-${value.getFullYear()}`
);
return;
}
if (event.currentTarget.classList.contains('IDLE')) {
setMessage(
`${selectedUser.username} is IDLE on ${value.getDate()}-${
`${selectedUser?.username} is IDLE on ${value.getDate()}-${
MONTHS[value.getMonth()]
}-${value.getFullYear()}`
);
return;
}
if (processedData[1] && processedData[1][value.getTime()]) {
const taskTitle = processedData[1].get(value.getTime());
if (taskTitle) {
setMessage(
`${selectedUser.username} is ACTIVE on ${value.getDate()}-${
`${selectedUser?.username} is ACTIVE on ${value.getDate()}-${
MONTHS[value.getMonth()]
}-${value.getFullYear()} having task with title - ${
processedData[1][value.getTime()]
}`
}-${value.getFullYear()} having task with title - ${taskTitle}`
);
return;
}

setMessage(
`No user status found for ${
selectedUser.username
selectedUser?.username
} on ${value.getDate()}-${
MONTHS[value.getMonth()]
}-${value.getFullYear()}!`
Expand All @@ -73,19 +149,27 @@ const UserStatusCalendar: FC = () => {

<div className="container calendar-container">
<SearchField
onSearchTextSubmitted={(user, data) => {
setSelectedUser(user);
setProcessedData(
processData(user ? user.id : null, data)
onSearchTextSubmitted={(user, data, oooLogsData) => {
setSelectedUser(user || null);
const processed = processData(
user ? user.id : null,
data,
oooLogsData
);
setProcessedData(processed);
setMessage(null);
}}
loading={loading}
loading={false}
dev={isDevMode}
/>
{selectedUser && (
<div className="calendar" data-testid="react-calendar">
<Calendar
onChange={onDateChange as any}
onChange={(value) => {
if (value instanceof Date) {
onDateChange(value);
}
}}
className="calendar-div"
value={selectedDate}
onClickDay={handleDayClick}
Expand Down
Loading