Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
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
Copy Markdown
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 = true,
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated
format = 'feed',
Comment thread
RishiChaubey31 marked this conversation as resolved.
type = 'REQUEST_CREATED',
Comment thread
RishiChaubey31 marked this conversation as resolved.
Comment thread
RishiChaubey31 marked this conversation as resolved.
}) =>
`/logs?dev=${dev}&format=${format}&type=${type}&username=${username}`,
Comment thread
RishiChaubey31 marked this conversation as resolved.
Comment thread
RishiChaubey31 marked this conversation as resolved.
providesTags: ['Logs'],
}),
}),
});

export const { useGetLogsByUsernameQuery } = logsApi;
40 changes: 38 additions & 2 deletions src/components/Calendar/UserSearchField.tsx

Copy link
Copy Markdown
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,36 @@
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';

type SearchFieldProps = {
onSearchTextSubmitted: (user: userDataType | undefined, data: any) => void;
onSearchTextSubmitted: (
user: userDataType | undefined,
data: any,
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated
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,6 +42,9 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => {
const user = usersList.find(
(user: userDataType) => user.username === searchText
);
setSelectedUser(user || null);
// Reset the tracking ref when submitting a new user
lastProcessedUsername.current = null;
onSearchTextSubmitted(user, data);
};

Expand All @@ -36,6 +53,12 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => {
const [displayList, setDisplayList] = useState<userDataType[]>([]);
const [data, setData] = useState([]);

// Fetch OOO logs data when dev=true and user is selected
const { data: logsData } = useGetLogsByUsernameQuery(
{ username: selectedUser?.username || '' },
{ skip: !dev || !selectedUser?.username }
);
Comment thread
RishiChaubey31 marked this conversation as resolved.

useEffect(() => {
if (userData?.users) {
const users: userDataType[] = userData.users;
Expand All @@ -54,6 +77,19 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => {
}
}, [isLoading, userData]);
Comment on lines 65 to 82

Copy link
Copy Markdown
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.


// Update parent component when logs data changes - only once per user
useEffect(() => {
if (
dev &&
logsData?.data &&
selectedUser &&
lastProcessedUsername.current !== selectedUser.username
) {
lastProcessedUsername.current = selectedUser.username || null;
onSearchTextSubmitted(selectedUser, data, logsData.data);
}
}, [dev, logsData, selectedUser]);
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated

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/`;
115 changes: 97 additions & 18 deletions src/pages/calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,72 @@
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';

const UserStatusCalendar: FC = () => {
const router = useRouter();
const { dev } = router.query;
const isDevMode = dev === 'true';

const [selectedDate, onDateChange] = useState<Date>(new Date());

Copy link
Copy Markdown
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]: any = useState(null);
const [processedData, setProcessedData] = useState<any>(
processData(selectedUser ? selectedUser.id : null, [])
);
const [selectedUser, setSelectedUser] = useState<userDataType | null>(null);
const [processedData, setProcessedData] = useState<
[
Record<number, string>,
Record<number, string>,
Record<number, OOOEntry[]>
]
>([{}, {}, {}] as [
Record<number, string>,
Record<number, string>,
Record<number, OOOEntry[]>
]);
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated

const [message, setMessage]: any = useState(null);
const [loading, setLoading]: any = useState(false);
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';

// Check for OOO entries first (new API data)
if (processedData[2] && processedData[2][date.getTime()]) {
return 'OOO';
}

// Check for existing status (mock data)
return processedData[0] ? processedData[0][date.getTime()] : null;
};

const handleDayClick = (value: Date, event: any) => {
const formatOOOMessage = (oooEntries: OOOEntry[]): string => {
return oooEntries
.map((entry) => {
const fromDate = formatTimestampToDate(entry.from);
const untilDate = formatTimestampToDate(entry.until);
const requestLink = `${OOO_REQUEST_DETAILS_URL}`;

let messageText = `From: ${fromDate}\nUntil: ${untilDate}\nRequest ID: `;
messageText += `<a href="${requestLink}" target="_blank" rel="noopener noreferrer">${entry.requestId}</a>`;

if (entry.message) {
messageText += `\nMessage: ${entry.message}`;
}

return messageText;
})
.join('\n\n---\n\n');
};
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated

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

// Check for OOO entries first (new feature)
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated
if (processedData[2] && processedData[2][value.getTime()]) {
const oooEntries = processedData[2][value.getTime()];
const formattedMessage = formatOOOMessage(oooEntries);
setMessage(
<div>
<div>{`${
selectedUser?.username
} is OOO on ${value.getDate()}-${
MONTHS[value.getMonth()]
}-${value.getFullYear()}`}</div>
<div
style={{ marginTop: '10px', whiteSpace: 'pre-line' }}
dangerouslySetInnerHTML={{ __html: formattedMessage }}
/>
</div>
);
return;
}

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

Copy link
Copy Markdown
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()]) {
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()]
Expand All @@ -60,7 +125,7 @@ const UserStatusCalendar: FC = () => {

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

<div className="container calendar-container">
<SearchField
onSearchTextSubmitted={(user, data) => {
setSelectedUser(user);
onSearchTextSubmitted={(user, data, oooLogsData) => {
setSelectedUser(user || null);
const processed = processData(
user ? user.id : null,
data,
oooLogsData
);
setProcessedData(
processData(user ? user.id : null, data)
processed as [
Record<number, string>,
Record<number, string>,
Record<number, OOOEntry[]>
]
);
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
10 changes: 10 additions & 0 deletions src/styles/calendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
border-radius: 10px;
text-align: center;
position: relative;
white-space: pre-line;
}

.messageDiv a {
color: #2563eb;
text-decoration: underline;
}

.messageDiv a:hover {
color: #1d4ed8;
}
Comment thread
RishiChaubey31 marked this conversation as resolved.

.messageDiv:before {
Expand Down
22 changes: 22 additions & 0 deletions src/utils/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,25 @@ export const getDateRelativeToToday = (
);
}
};

export const formatTimestampToDate = (timestamp: number): string => {
const date = new Date(timestamp);
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated
Comment thread
RishiChaubey31 marked this conversation as resolved.
Outdated

return `${
months[date.getMonth()]
} ${date.getDate()}, ${date.getFullYear()}`;
};
Comment on lines +26 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • since we already have moment then we can just do this

export const formatTimestampToDate = (timestamp: number): string => {
return moment(timestamp).format('MMM D, YYYY');
};

Loading
Loading