Skip to content
Merged
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
1 change: 1 addition & 0 deletions backend/app/models/User.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class FormStatus(str, PyEnum):
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
COMPLETED = "completed"
REJECTED = "rejected"


class User(Base):
Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class FormStatus(str, Enum):
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
COMPLETED = "completed"
REJECTED = "rejected"


class UserBase(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""add rejected enum value to User model

Revision ID: 8d2cd99b9eb8
Revises: 9f1a6d727929
Create Date: 2025-10-30 19:50:23.495788

"""

from typing import Sequence, Union

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "8d2cd99b9eb8"
down_revision: Union[str, None] = "9f1a6d727929"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.execute("""
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'rejected' AND enumtypid = 'form_status_enum'::regtype) THEN
ALTER TYPE form_status_enum ADD VALUE 'rejected';
END IF;
END $$;
""")
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
43 changes: 43 additions & 0 deletions frontend/src/components/admin/DirectoryDataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import baseAPIClient from '@/APIClients/baseAPIClient';
import { useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import type { UserResponse } from '@/APIClients/authAPIClient';

interface DirectoryDataProviderProps {
children: (users: UserResponse[], loading: boolean, error: Error | null) => ReactNode;
}

export function DirectoryDataProvider({ children }: DirectoryDataProviderProps) {
const [users, setUsers] = useState<UserResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
const response = await baseAPIClient.get('/users');
const usersData = response.data?.users || response.data || [];
setUsers(Array.isArray(usersData) ? usersData : []);
} catch (err) {
const error =
err instanceof Error
? err
: new Error(
typeof err === 'object' && err !== null && 'message' in err
? String(err.message)
: 'Failed to fetch users. Please try again.',
);
setError(error);
console.error('Failed to fetch users:', err);
} finally {
setLoading(false);
}
};

fetchUsers();
}, []);

return <>{children(users, loading, error)}</>;
}
112 changes: 95 additions & 17 deletions frontend/src/components/intake/rejection-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';
import { Box, Text, VStack } from '@chakra-ui/react';
import React, { useEffect, useMemo, useState } from 'react';
import { Box, Link, Text, VStack } from '@chakra-ui/react';
import { COLORS } from '@/constants/form';
import type { AuthenticatedUser, UserRole } from '@/types/authTypes';
import { UserRole as UserRoleEnum } from '@/types/authTypes';
import { getCurrentUser, syncCurrentUser } from '@/APIClients/authAPIClient';
import { roleIdToUserRole } from '@/utils/roleUtils';

// X mark icon component
const XMarkIcon: React.FC = () => (
Expand Down Expand Up @@ -28,6 +32,53 @@ const XMarkIcon: React.FC = () => (
);

export function RejectionScreen() {
const [userRole, setUserRole] = useState<UserRole | null>(null);

useEffect(() => {
const resolveRole = (user: AuthenticatedUser): UserRole | null => {
if (!user) return null;
if ('role' in user && user.role) {
return user.role as UserRole;
}
const roleId = user.user?.roleId ?? (user.roleId as unknown as number | undefined) ?? null;
return roleIdToUserRole(roleId);
};

const hydrateRole = async () => {
const localUser = getCurrentUser();
let resolved = resolveRole(localUser);
if (!resolved) {
const synced = await syncCurrentUser();
resolved = resolveRole(synced);
}
setUserRole(resolved ?? UserRoleEnum.PARTICIPANT);
};

void hydrateRole();
}, []);

const content = useMemo(() => {
if (userRole === UserRoleEnum.VOLUNTEER) {
return {
title: "You're unable to continue this application.",
body: [
'Thank you for your interest in becoming a peer support volunteer.',
'You must meet all of the eligibility criteria to continue.',
'Please reach out to [email protected] for more information about volunteering with LLSC.',
],
};
}

return {
title: "You're unable to continue this application.",
body: [
'Thank you for your interest in First Connections.',
'To continue as a participant you must meet each of the eligibility requirements.',
'Please reach out to [email protected] if you have questions or need help finding other support options.',
],
};
}, [userRole]);

return (
<Box minH="100vh" bg="white" display="flex" alignItems="center" justifyContent="center" py={12}>
<VStack gap={6}>
Expand All @@ -40,23 +91,50 @@ export function RejectionScreen() {
color={COLORS.veniceBlue}
mb={2}
>
Your request has been declined.
{content.title}
</Text>

<Text
fontFamily="system-ui, -apple-system, sans-serif"
fontSize="18px"
color={COLORS.fieldGray}
lineHeight="1.6"
maxW="600px"
textAlign="center"
>
For any inquiries, please reach us at{' '}
<Text as="span" color={COLORS.teal} fontWeight={500}>
[email protected]
</Text>
.
</Text>
<VStack gap={1} maxW="640px">
{content.body.map((sentence, index) => {
const EMAIL_TOKEN = '[email protected]';
const hasEmail = sentence.includes(EMAIL_TOKEN);
let prefix = '';
let suffix = '';
if (hasEmail) {
const parts = sentence.split(EMAIL_TOKEN);
prefix = parts[0] ?? '';
suffix = parts[1] ?? '';
}

return (
<Text
key={`${sentence}-${index}`}
fontFamily="system-ui, -apple-system, sans-serif"
fontSize="18px"
color={COLORS.fieldGray}
lineHeight="1.6"
textAlign="center"
>
{hasEmail ? (
<>
{prefix}
<Link
href="mailto:[email protected]"
color={COLORS.teal}
textDecoration="underline"
fontWeight={600}
>
[email protected]
</Link>
{suffix}
</>
) : (
sentence
)}
</Text>
);
})}
</VStack>
</VStack>
</Box>
);
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/components/ui/directory-progress-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type React from 'react';
import { Box, Flex, Text } from '@chakra-ui/react';
import { FiCheck } from 'react-icons/fi';

interface DirectoryProgressSliderProps {
value: number; // 0-100
}

export const DirectoryProgressSlider: React.FC<DirectoryProgressSliderProps> = ({ value }) => {
const milestones = [25, 50, 75, 100];

return (
<Flex align="center" gap={3} w="full">
<Box position="relative" flex="1" h="24px" display={{ base: 'none', lg: 'block' }}>
{/* Track */}
<Box
position="absolute"
top="50%"
left="0"
right="0"
h="2px"
bg="#EAEAE6"
transform="translateY(-50%)"
zIndex={0}
/>

{/* Filled Track */}
<Box
position="absolute"
top="50%"
left="0"
h="2px"
bg="#027847"
transform="translateY(-50%)"
width={`${value}%`}
zIndex={0}
/>

{/* Milestone Markers */}
<Flex
position="absolute"
top="50%"
left="0"
right="0"
transform="translateY(-50%)"
justify="space-between"
align="center"
zIndex={1}
>
{milestones.map((milestone) => {
const isCompleted = value >= milestone;

return (
<Flex
key={milestone}
align="center"
justify="center"
w="20px"
h="20px"
borderRadius="full"
bg={isCompleted ? '#E7F8EE' : '#EAEAE6'}
border={isCompleted ? '2px solid #027847' : 'none'}
boxShadow="sm"
>
{isCompleted && <FiCheck size={14} color="#027847" strokeWidth={4} />}
</Flex>
);
})}
</Flex>
</Box>

{/* Percentage Display */}
<Text
fontFamily="'Open Sans', sans-serif"
fontSize="14px"
fontWeight={600}
lineHeight="1.429em"
color="#495D6C"
minW="60px"
>
{value}%
</Text>
</Flex>
);
};
3 changes: 3 additions & 0 deletions frontend/src/constants/formStatusRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const PARTICIPANT_STATUS_ROUTES: StatusRouteMap = {
[FormStatus.SECONDARY_APPLICATION_TODO]: '/participant/ranking',
[FormStatus.SECONDARY_APPLICATION_SUBMITTED]: '/participant/ranking/thank-you',
[FormStatus.COMPLETED]: '/participant/dashboard',
[FormStatus.REJECTED]: '/rejection',
};

export const VOLUNTEER_STATUS_ROUTES: StatusRouteMap = {
Expand All @@ -20,6 +21,7 @@ export const VOLUNTEER_STATUS_ROUTES: StatusRouteMap = {
[FormStatus.SECONDARY_APPLICATION_TODO]: '/volunteer/secondary-application',
[FormStatus.SECONDARY_APPLICATION_SUBMITTED]: '/volunteer/secondary-application/thank-you',
[FormStatus.COMPLETED]: '/volunteer/dashboard',
[FormStatus.REJECTED]: '/rejection',
};

export const ADMIN_STATUS_ROUTES: StatusRouteMap = {
Expand All @@ -30,6 +32,7 @@ export const ADMIN_STATUS_ROUTES: StatusRouteMap = {
[FormStatus.SECONDARY_APPLICATION_TODO]: '/admin',
[FormStatus.SECONDARY_APPLICATION_SUBMITTED]: '/admin',
[FormStatus.COMPLETED]: '/admin',
[FormStatus.REJECTED]: '/admin',
};

export const ROLE_STATUS_ROUTES: Record<UserRole, StatusRouteMap> = {
Expand Down
Loading