Skip to content

Commit eef6972

Browse files
chene0ryan-n-gunawanYashK2005
authored andcommitted
Admin Page - Directory (#67)
Create these pages at /admin/directory This is a list of all volunteers and users You should be able to filter by user type and status - Not started : intake-todo - Completed : completed - Rejected: rejected - In progress : intake-submitted, ranking-todo, ranking-submitted, secondary-application-todo, secondary-application-submitted You might need to create api’s on the backend to fetch all users <!-- Please replace with your ticket's URL --> [Ticket Name](https://www.notion.so/uwblueprintexecs/Admin-Page-Directory-27d10f3fb1dc809f9602f39304da70a8?source=copy_link) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> * <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> 1. <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> * - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR --------- Co-authored-by: Ryan Gunawan <[email protected]> Co-authored-by: YashK2005 <[email protected]>
1 parent bceb845 commit eef6972

File tree

9 files changed

+1061
-17
lines changed

9 files changed

+1061
-17
lines changed

backend/app/models/User.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class FormStatus(str, PyEnum):
1818
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
1919
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
2020
COMPLETED = "completed"
21+
REJECTED = "rejected"
2122

2223

2324
class User(Base):

backend/app/schemas/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class FormStatus(str, Enum):
4343
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
4444
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
4545
COMPLETED = "completed"
46+
REJECTED = "rejected"
4647

4748

4849
class UserBase(BaseModel):
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""add rejected enum value to User model
2+
3+
Revision ID: 8d2cd99b9eb8
4+
Revises: 9f1a6d727929
5+
Create Date: 2025-10-30 19:50:23.495788
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = "8d2cd99b9eb8"
15+
down_revision: Union[str, None] = "9f1a6d727929"
16+
branch_labels: Union[str, Sequence[str], None] = None
17+
depends_on: Union[str, Sequence[str], None] = None
18+
19+
20+
def upgrade() -> None:
21+
op.execute("""
22+
DO $$ BEGIN
23+
IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'rejected' AND enumtypid = 'form_status_enum'::regtype) THEN
24+
ALTER TYPE form_status_enum ADD VALUE 'rejected';
25+
END IF;
26+
END $$;
27+
""")
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
pass
30+
# ### end Alembic commands ###
31+
32+
33+
def downgrade() -> None:
34+
# ### commands auto generated by Alembic - please adjust! ###
35+
pass
36+
# ### end Alembic commands ###
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import baseAPIClient from '@/APIClients/baseAPIClient';
2+
import { useEffect, useState } from 'react';
3+
import type { ReactNode } from 'react';
4+
import type { UserResponse } from '@/APIClients/authAPIClient';
5+
6+
interface DirectoryDataProviderProps {
7+
children: (users: UserResponse[], loading: boolean, error: Error | null) => ReactNode;
8+
}
9+
10+
export function DirectoryDataProvider({ children }: DirectoryDataProviderProps) {
11+
const [users, setUsers] = useState<UserResponse[]>([]);
12+
const [loading, setLoading] = useState(true);
13+
const [error, setError] = useState<Error | null>(null);
14+
15+
useEffect(() => {
16+
const fetchUsers = async () => {
17+
try {
18+
setLoading(true);
19+
setError(null);
20+
const response = await baseAPIClient.get('/users');
21+
const usersData = response.data?.users || response.data || [];
22+
setUsers(Array.isArray(usersData) ? usersData : []);
23+
} catch (err) {
24+
const error =
25+
err instanceof Error
26+
? err
27+
: new Error(
28+
typeof err === 'object' && err !== null && 'message' in err
29+
? String(err.message)
30+
: 'Failed to fetch users. Please try again.',
31+
);
32+
setError(error);
33+
console.error('Failed to fetch users:', err);
34+
} finally {
35+
setLoading(false);
36+
}
37+
};
38+
39+
fetchUsers();
40+
}, []);
41+
42+
return <>{children(users, loading, error)}</>;
43+
}

frontend/src/components/intake/rejection-screen.tsx

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import React from 'react';
2-
import { Box, Text, VStack } from '@chakra-ui/react';
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
import { Box, Link, Text, VStack } from '@chakra-ui/react';
33
import { COLORS } from '@/constants/form';
4+
import type { AuthenticatedUser, UserRole } from '@/types/authTypes';
5+
import { UserRole as UserRoleEnum } from '@/types/authTypes';
6+
import { getCurrentUser, syncCurrentUser } from '@/APIClients/authAPIClient';
7+
import { roleIdToUserRole } from '@/utils/roleUtils';
48

59
// X mark icon component
610
const XMarkIcon: React.FC = () => (
@@ -28,6 +32,53 @@ const XMarkIcon: React.FC = () => (
2832
);
2933

3034
export function RejectionScreen() {
35+
const [userRole, setUserRole] = useState<UserRole | null>(null);
36+
37+
useEffect(() => {
38+
const resolveRole = (user: AuthenticatedUser): UserRole | null => {
39+
if (!user) return null;
40+
if ('role' in user && user.role) {
41+
return user.role as UserRole;
42+
}
43+
const roleId = user.user?.roleId ?? (user.roleId as unknown as number | undefined) ?? null;
44+
return roleIdToUserRole(roleId);
45+
};
46+
47+
const hydrateRole = async () => {
48+
const localUser = getCurrentUser();
49+
let resolved = resolveRole(localUser);
50+
if (!resolved) {
51+
const synced = await syncCurrentUser();
52+
resolved = resolveRole(synced);
53+
}
54+
setUserRole(resolved ?? UserRoleEnum.PARTICIPANT);
55+
};
56+
57+
void hydrateRole();
58+
}, []);
59+
60+
const content = useMemo(() => {
61+
if (userRole === UserRoleEnum.VOLUNTEER) {
62+
return {
63+
title: "You're unable to continue this application.",
64+
body: [
65+
'Thank you for your interest in becoming a peer support volunteer.',
66+
'You must meet all of the eligibility criteria to continue.',
67+
'Please reach out to [email protected] for more information about volunteering with LLSC.',
68+
],
69+
};
70+
}
71+
72+
return {
73+
title: "You're unable to continue this application.",
74+
body: [
75+
'Thank you for your interest in First Connections.',
76+
'To continue as a participant you must meet each of the eligibility requirements.',
77+
'Please reach out to [email protected] if you have questions or need help finding other support options.',
78+
],
79+
};
80+
}, [userRole]);
81+
3182
return (
3283
<Box minH="100vh" bg="white" display="flex" alignItems="center" justifyContent="center" py={12}>
3384
<VStack gap={6}>
@@ -40,23 +91,50 @@ export function RejectionScreen() {
4091
color={COLORS.veniceBlue}
4192
mb={2}
4293
>
43-
Your request has been declined.
94+
{content.title}
4495
</Text>
4596

46-
<Text
47-
fontFamily="system-ui, -apple-system, sans-serif"
48-
fontSize="18px"
49-
color={COLORS.fieldGray}
50-
lineHeight="1.6"
51-
maxW="600px"
52-
textAlign="center"
53-
>
54-
For any inquiries, please reach us at{' '}
55-
<Text as="span" color={COLORS.teal} fontWeight={500}>
56-
57-
</Text>
58-
.
59-
</Text>
97+
<VStack gap={1} maxW="640px">
98+
{content.body.map((sentence, index) => {
99+
const EMAIL_TOKEN = '[email protected]';
100+
const hasEmail = sentence.includes(EMAIL_TOKEN);
101+
let prefix = '';
102+
let suffix = '';
103+
if (hasEmail) {
104+
const parts = sentence.split(EMAIL_TOKEN);
105+
prefix = parts[0] ?? '';
106+
suffix = parts[1] ?? '';
107+
}
108+
109+
return (
110+
<Text
111+
key={`${sentence}-${index}`}
112+
fontFamily="system-ui, -apple-system, sans-serif"
113+
fontSize="18px"
114+
color={COLORS.fieldGray}
115+
lineHeight="1.6"
116+
textAlign="center"
117+
>
118+
{hasEmail ? (
119+
<>
120+
{prefix}
121+
<Link
122+
href="mailto:[email protected]"
123+
color={COLORS.teal}
124+
textDecoration="underline"
125+
fontWeight={600}
126+
>
127+
128+
</Link>
129+
{suffix}
130+
</>
131+
) : (
132+
sentence
133+
)}
134+
</Text>
135+
);
136+
})}
137+
</VStack>
60138
</VStack>
61139
</Box>
62140
);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type React from 'react';
2+
import { Box, Flex, Text } from '@chakra-ui/react';
3+
import { FiCheck } from 'react-icons/fi';
4+
5+
interface DirectoryProgressSliderProps {
6+
value: number; // 0-100
7+
}
8+
9+
export const DirectoryProgressSlider: React.FC<DirectoryProgressSliderProps> = ({ value }) => {
10+
const milestones = [25, 50, 75, 100];
11+
12+
return (
13+
<Flex align="center" gap={3} w="full">
14+
<Box position="relative" flex="1" h="24px" display={{ base: 'none', lg: 'block' }}>
15+
{/* Track */}
16+
<Box
17+
position="absolute"
18+
top="50%"
19+
left="0"
20+
right="0"
21+
h="2px"
22+
bg="#EAEAE6"
23+
transform="translateY(-50%)"
24+
zIndex={0}
25+
/>
26+
27+
{/* Filled Track */}
28+
<Box
29+
position="absolute"
30+
top="50%"
31+
left="0"
32+
h="2px"
33+
bg="#027847"
34+
transform="translateY(-50%)"
35+
width={`${value}%`}
36+
zIndex={0}
37+
/>
38+
39+
{/* Milestone Markers */}
40+
<Flex
41+
position="absolute"
42+
top="50%"
43+
left="0"
44+
right="0"
45+
transform="translateY(-50%)"
46+
justify="space-between"
47+
align="center"
48+
zIndex={1}
49+
>
50+
{milestones.map((milestone) => {
51+
const isCompleted = value >= milestone;
52+
53+
return (
54+
<Flex
55+
key={milestone}
56+
align="center"
57+
justify="center"
58+
w="20px"
59+
h="20px"
60+
borderRadius="full"
61+
bg={isCompleted ? '#E7F8EE' : '#EAEAE6'}
62+
border={isCompleted ? '2px solid #027847' : 'none'}
63+
boxShadow="sm"
64+
>
65+
{isCompleted && <FiCheck size={14} color="#027847" strokeWidth={4} />}
66+
</Flex>
67+
);
68+
})}
69+
</Flex>
70+
</Box>
71+
72+
{/* Percentage Display */}
73+
<Text
74+
fontFamily="'Open Sans', sans-serif"
75+
fontSize="14px"
76+
fontWeight={600}
77+
lineHeight="1.429em"
78+
color="#495D6C"
79+
minW="60px"
80+
>
81+
{value}%
82+
</Text>
83+
</Flex>
84+
);
85+
};

frontend/src/constants/formStatusRoutes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const PARTICIPANT_STATUS_ROUTES: StatusRouteMap = {
1010
[FormStatus.SECONDARY_APPLICATION_TODO]: '/participant/ranking',
1111
[FormStatus.SECONDARY_APPLICATION_SUBMITTED]: '/participant/ranking/thank-you',
1212
[FormStatus.COMPLETED]: '/participant/dashboard',
13+
[FormStatus.REJECTED]: '/rejection',
1314
};
1415

1516
export const VOLUNTEER_STATUS_ROUTES: StatusRouteMap = {
@@ -20,6 +21,7 @@ export const VOLUNTEER_STATUS_ROUTES: StatusRouteMap = {
2021
[FormStatus.SECONDARY_APPLICATION_TODO]: '/volunteer/secondary-application',
2122
[FormStatus.SECONDARY_APPLICATION_SUBMITTED]: '/volunteer/secondary-application/thank-you',
2223
[FormStatus.COMPLETED]: '/volunteer/dashboard',
24+
[FormStatus.REJECTED]: '/rejection',
2325
};
2426

2527
export const ADMIN_STATUS_ROUTES: StatusRouteMap = {
@@ -30,6 +32,7 @@ export const ADMIN_STATUS_ROUTES: StatusRouteMap = {
3032
[FormStatus.SECONDARY_APPLICATION_TODO]: '/admin/dashboard',
3133
[FormStatus.SECONDARY_APPLICATION_SUBMITTED]: '/admin/dashboard',
3234
[FormStatus.COMPLETED]: '/admin/dashboard',
35+
[FormStatus.REJECTED]: '/admin/dashboard',
3336
};
3437

3538
export const ROLE_STATUS_ROUTES: Record<UserRole, StatusRouteMap> = {

0 commit comments

Comments
 (0)