Skip to content
Draft
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
2 changes: 2 additions & 0 deletions netlify/functions-src/functions/mentors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { handler as getMentorsHanler } from './modules/mentors/get';
import { handler as getApplicationsHandler } from './modules/mentors/applications/get';
import { handler as upsertApplicationHandler } from './modules/mentors/applications/post';
import { handler as updateApplicationHandler } from './modules/mentors/applications/put';
import { handler as getInactiveMentorsHandler } from './modules/mentors/inactive';
import { Role } from './common/interfaces/user.interface';

export const handler: ApiHandler = withDB(
withRouter([
['/', 'GET', withAuth(getMentorsHanler, { authRequired: false })],
['/inactive', 'GET', withAuth(getInactiveMentorsHandler, { role: Role.ADMIN })],
['/applications', 'GET', withAuth(getApplicationsHandler, { includeFullUser: true, role: Role.ADMIN })],
['/applications/:applicationId', 'PUT', withAuth(updateApplicationHandler, { includeFullUser: true, role: Role.ADMIN })],
['/applications', 'POST', withAuth(upsertApplicationHandler, { includeFullUser: true, })],
Expand Down
82 changes: 82 additions & 0 deletions netlify/functions-src/functions/modules/mentors/inactive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { handler } from './inactive';
import { getCollection } from '../../utils/db';
import { HandlerEvent, HandlerContext } from '@netlify/functions';
import { AuthContext } from '../../types';

jest.mock('../../utils/db');

const baseEvent: HandlerEvent = {
body: null,
headers: {},
multiValueHeaders: {},
httpMethod: 'GET',
isBase64Encoded: false,
path: '/mentors/inactive',
queryStringParameters: null,
multiValueQueryStringParameters: null,
rawQuery: '',
rawUrl: '',
};

const baseContext: AuthContext<any> = {
callbackWaitsForEmptyEventLoop: true,
functionName: 'mentors',
functionVersion: '1',
invokedFunctionArn: '',
memoryLimitInMB: '128',
awsRequestId: '',
logGroupName: '',
logStreamName: '',
getRemainingTimeInMillis: () => 0,
done: () => {},
fail: () => {},
succeed: () => {},
user: { auth0Id: 'admin|123' },
};

describe('getInactiveMentorsHandler', () => {
const projectMock = jest.fn();
const toArrayMock = jest.fn();
const findMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
toArrayMock.mockResolvedValue([]);
projectMock.mockReturnValue({ toArray: toArrayMock });
findMock.mockReturnValue({ project: projectMock });
(getCollection as unknown as jest.Mock).mockReturnValue({ find: findMock });
});

it('returns a success response with an empty array when no inactive mentors exist', async () => {
const response = await handler(baseEvent, baseContext);
const body = JSON.parse(response.body || '{}');

expect(response.statusCode).toBe(200);
expect(body.success).toBe(true);
expect(body.data).toEqual([]);
});

it('queries for users with Mentor role and available !== true', async () => {
await handler(baseEvent, baseContext);

expect(findMock).toHaveBeenCalledWith({
roles: 'Mentor',
available: { $ne: true },
});
});

it('returns inactive mentors in the response', async () => {
const mockMentors = [
{ _id: 'id1', name: 'Alice', email: 'alice@example.com', createdAt: new Date().toISOString() },
{ _id: 'id2', name: 'Bob', email: 'bob@example.com', createdAt: new Date().toISOString() },
];
toArrayMock.mockResolvedValue(mockMentors);

const response = await handler(baseEvent, baseContext);
const body = JSON.parse(response.body || '{}');

expect(response.statusCode).toBe(200);
expect(body.success).toBe(true);
expect(body.data).toEqual(mockMentors);
});
});
18 changes: 18 additions & 0 deletions netlify/functions-src/functions/modules/mentors/inactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Role } from '../../common/interfaces/user.interface';
import type { ApiHandler } from '../../types';
import { getCollection } from '../../utils/db';
import { success } from '../../utils/response';

export const handler: ApiHandler = async () => {
const collection = getCollection('users');

const mentors = await collection
.find({
roles: Role.MENTOR,
available: { $ne: true },
})
.project({ _id: 1, name: 1, email: 1, createdAt: 1, avatar: 1 })
.toArray();

return success({ data: mentors });
};
66 changes: 66 additions & 0 deletions src/Me/Routes/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import {
freezeMentor,
getAllMentorshipRequests,
getInactiveMentors,
getUserRecords,
sendMentorNotActive,
sendStaledRequestEmail,
Expand All @@ -26,6 +27,8 @@ import { Loader } from '../../components/Loader';
import { getChannelInfo } from '../../channelProvider';
import { useApi } from '../../context/apiContext/ApiContext';

type InactiveMentor = Pick<User, '_id' | 'name' | 'email' | 'createdAt' | 'avatar'>;

const Mentee = styled.span`
display: flex;
align-items: center;
Expand All @@ -46,6 +49,68 @@ const Filters = styled.div`
const pending = (status: Status) =>
status === STATUS.new || status === STATUS.viewed;

const InactiveMentorsSection = () => {
const [mentors, setMentors] = useState<InactiveMentor[]>([]);
const [isLoading, setIsLoading] = useState(true);
const api = useApi();

useEffect(() => {
getInactiveMentors(api)
.then((response) => {
if (response?.success) {
setMentors(response.data);
}
})
.finally(() => setIsLoading(false));
}, [api]);

return (
<Card title="Approved Mentors – Never Set Active" className="wide">
{isLoading ? (
<Loader />
) : mentors.length === 0 ? (
<p>No inactive mentors found.</p>
) : (
<table>
<thead>
<tr>
{['Name', 'Email', 'Joined', 'Profile'].map((col) => (
<th key={col}>{col}</th>
))}
</tr>
</thead>
<tbody>
{mentors.map(({ _id, name, email, createdAt, avatar }) => (
<tr key={_id}>
<td>
{avatar && (
<img
src={avatar}
width="20"
alt=""
style={{ borderRadius: '50%', marginRight: 4 }}
/>
)}
{name}
</td>
<td>
<a href={`mailto:${email}`}>{email}</a>
</td>
<td>{formatTimeAgo(new Date(createdAt))}</td>
<td>
<a target="_blank" rel="noreferrer" href={`/u/${_id}`}>
🔗
</a>
</td>
</tr>
))}
</tbody>
</table>
)}
</Card>
);
};

const UserDetails = ({
userId,
mentorships,
Expand Down Expand Up @@ -269,6 +334,7 @@ const Admin = () => {
mentorships={filteredMentorshipRequests}
/>
)}
<InactiveMentorsSection />
<Card className='wide'>
<Filters>
<FormField>
Expand Down
8 changes: 7 additions & 1 deletion src/api/admin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ApiService, { paths } from '.';
import { MentorshipRequest, UserRecord } from '../types/models';
import { MentorshipRequest, User, UserRecord } from '../types/models';

export function getAllMentorshipRequests(apiService: ApiService, numMonthAgo: number = 1) {
const monthAgo = new Date();
Expand Down Expand Up @@ -34,4 +34,10 @@ export async function freezeMentor(apiService: any, mentorId: string) {

export function getUserRecords(apiService: ApiService, userId: string) {
return apiService.makeApiCall<UserRecord[]>(`${paths.USERS}/${userId}/records`);
}

export function getInactiveMentors(apiService: ApiService) {
return apiService.makeApiCall<Pick<User, '_id' | 'name' | 'email' | 'createdAt' | 'avatar'>[]>(
`${paths.MENTORS}/inactive`
);
}