diff --git a/src/app/services/logsApi.ts b/src/app/services/logsApi.ts new file mode 100644 index 000000000..dbeb7688a --- /dev/null +++ b/src/app/services/logsApi.ts @@ -0,0 +1,50 @@ +import { api } from './api'; + +export type LogEntry = any; + +export type LogsResponse = { + data: LogEntry[]; + next?: string | null; + prev?: string | null; +}; + +export type GetLogsQuery = { + username?: string; + startDate?: number; // seconds since epoch + endDate?: number; // seconds since epoch + type?: string; // comma-separated list of types + format?: 'feed'; + dev?: boolean; + nextLink?: string; +}; + +export const logsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getLogs: build.query({ + query: ({ + username, + startDate, + endDate, + type, + format = 'feed', + dev = true, + nextLink, + }) => { + if (nextLink) { + return nextLink; + } + const queryParams = new URLSearchParams(); + if (dev) queryParams.set('dev', 'true'); + if (format) queryParams.set('format', format); + if (type) queryParams.set('type', type); + if (username) queryParams.set('username', username); + if (startDate) queryParams.set('startDate', String(startDate)); + if (endDate) queryParams.set('endDate', String(endDate)); + return `/logs?${queryParams.toString()}`; + }, + providesTags: ['Status'], + }), + }), +}); + +export const { useLazyGetLogsQuery } = logsApi; diff --git a/src/app/services/tasksApi.ts b/src/app/services/tasksApi.ts index a5e6e1f2d..bd6731613 100644 --- a/src/app/services/tasksApi.ts +++ b/src/app/services/tasksApi.ts @@ -60,7 +60,7 @@ export const tasksApi = api.injectEndpoints({ ), next: response.next, prev: response.prev, - }; + } as TasksResponseType; }, }), @@ -138,6 +138,7 @@ export const tasksApi = api.injectEndpoints({ export const { useGetAllTasksQuery, + useLazyGetAllTasksQuery, useGetMineTasksQuery, useAddTaskMutation, useUpdateTaskMutation, diff --git a/src/components/Calendar/UserSearchField.tsx b/src/components/Calendar/UserSearchField.tsx index 607abe100..6a55ce379 100644 --- a/src/components/Calendar/UserSearchField.tsx +++ b/src/components/Calendar/UserSearchField.tsx @@ -1,9 +1,10 @@ import { useState, useEffect, ChangeEvent, useRef } from 'react'; import classNames from './UserSearchField.module.scss'; import { useGetAllUsersQuery } from '@/app/services/usersApi'; -import { logs } from '@/constants/calendar'; import { userDataType } from '@/interfaces/user.type'; import { useOutsideAlerter } from '@/hooks/useOutsideAlerter'; +import { useLazyGetLogsQuery } from '@/app/services/logsApi'; +import { useLazyGetAllTasksQuery } from '@/app/services/tasksApi'; type SearchFieldProps = { onSearchTextSubmitted: (user: userDataType | undefined, data: any) => void; @@ -22,19 +23,112 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => { filterUser(e.target.value); }; - const handleOnSubmit = (e: React.FormEvent) => { + const { data: userData, isLoading: isUsersLoading } = useGetAllUsersQuery(); + const [usersList, setUsersList] = useState([]); + const [displayList, setDisplayList] = useState([]); + + const [triggerGetLogs, { isFetching: isLogsFetching }] = + useLazyGetLogsQuery(); + const [triggerGetTasks, { isFetching: isTasksFetching }] = + useLazyGetAllTasksQuery(); + + const toMs = (value?: number | string) => { + if (typeof value === 'string') { + const parsed = Date.parse(value); + return isNaN(parsed) ? undefined : parsed; + } + if (typeof value !== 'number') return undefined as unknown as number; + return value >= 1e12 ? value : value * 1000; + }; + + const handleOnSubmit = async (e: React.FormEvent) => { e.preventDefault(); setDisplayList([]); const user = usersList.find( (user: userDataType) => user.username === searchText ); - onSearchTextSubmitted(user, data); - }; - const { data: userData, isError, isLoading } = useGetAllUsersQuery(); - const [usersList, setUsersList] = useState([]); - const [displayList, setDisplayList] = useState([]); - const [data, setData] = useState([]); + if (!user) { + onSearchTextSubmitted(undefined, []); + return; + } + + // Fetch logs (events) and tasks (continuous ranges). Try tasks by both userId and username. + const [logsResult, tasksByIdResult, tasksByUsernameResult] = + await Promise.all([ + triggerGetLogs({ + username: user.username || undefined, + type: ['task', 'REQUEST_CREATED'].join(','), + format: 'feed', + dev: true, + }), + triggerGetTasks({ assignee: user.id }), + triggerGetTasks({ assignee: user.username || '' }), + ]); + + const logsResponse = logsResult.data; + const tasksById = tasksByIdResult.data?.tasks || []; + const tasksByUsername = tasksByUsernameResult.data?.tasks || []; + + // Merge and de-duplicate tasks by id + const taskMap: Record = {}; + [...tasksById, ...tasksByUsername].forEach((t: any) => { + if (t?.id) taskMap[t.id] = t; + }); + const mergedTasks = Object.values(taskMap); + + const eventEntries = (logsResponse?.data || []) + .filter((log: any) => !!log) + .map((log: any) => { + // OOO ranges + if (log.type === 'REQUEST_CREATED' && log.from && log.until) { + return { + startTime: toMs(log.from), + endTime: toMs(log.until), + status: 'OOO', + }; + } + // Task events => single-day ACTIVE + if (log.type === 'task') { + const ts = toMs(log.timestamp); + return { + startTime: ts, + endTime: ts, + status: 'ACTIVE', + taskTitle: log.taskTitle, + }; + } + return null; + }) + .filter(Boolean); + + const taskRangeEntries = mergedTasks + .filter((t: any) => t) + .map((t: any) => { + const start = toMs(t.startedOn); + const end = toMs(t.endsOn); + const taskUrl = t.github?.issue?.html_url || undefined; + return { + startTime: start, + endTime: end ?? start, + status: 'ACTIVE', + taskTitle: t.title, + taskUrl, + }; + }) + .filter((e: any) => e.startTime); + + const calendarDataForUser = [...eventEntries, ...taskRangeEntries]; + + const mapped = [ + { + userId: user.id, + data: calendarDataForUser, + }, + ]; + + onSearchTextSubmitted(user, mapped); + }; useEffect(() => { if (userData?.users) { @@ -42,17 +136,9 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => { 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, - }; - }); - setData(logData); setUsersList(filteredUsers); } - }, [isLoading, userData]); + }, [isUsersLoading, userData]); const isValidUsername = () => { const usernames = usersList.map((user: userDataType) => user.username); @@ -112,7 +198,11 @@ const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => {