Skip to content

Commit e0d2069

Browse files
authored
Scheduling a call, request new times, cancelling call, & edit profile flows in participant dash. Contact form and sign out button for participant and volunteer dash. (#81)
TDLR: Implemented scheduling call, request new times, cancelling call, & edit profile flows in participant dash. implemented contact form and sign out button for participant and volunteer dash i'm too lazy to write one so here's claude's pr message: ## Summary Implements comprehensive participant dashboard improvements including call scheduling, requesting new times, cancelling calls, profile editing, and contact form functionality. ## What Changed ### Participant Dashboard Features - **Call Scheduling**: Participants can now schedule calls by selecting from volunteer-proposed time blocks with a clean date/time picker interface - **Request New Times**: Two-step flow allowing participants to propose alternative availability when volunteer times don't work - **Cancel Calls**: Soft-delete flow with confirmation and success modals - **Edit Profile**: Comprehensive modal for editing personal details, cancer experience, and account settings - **Contact Form**: Shared component accessible from both participant and volunteer dashboards ### Backend API Enhancements - Added `/contact/submit` endpoint for contact form (with logging; email integration pending) - Enhanced match endpoints: `/schedule`, `/cancel`, `/request-new-times` - Updated `/user-data/me` PATCH to support timezone and language updates - Fixed logout endpoint to properly handle auth_id to user_id conversion - Match cancellation now uses soft-delete pattern (sets `deleted_at` and cleans up time blocks) ### UI/UX Improvements - Refactored participant dashboard components for better state management - Added timezone awareness (EST) across scheduling flows - Improved modal consistency and user feedback - Enhanced dashboard navigation with sidebar and profile dropdown - Better handling of past/future time blocks ### Data & Testing - Enhanced seed data with volunteer experiences, availability templates, and timezone information - More realistic test data for end-to-end testing ## Technical Details **New Components:** - `ParticipantEditProfileModal` - Full profile editing - `DaySelectionCalendar` - Multi-day selection calendar - `CancelCallConfirmationModal` & `CancelCallSuccessModal` - `RequestNewTimesSuccessModal` - `ViewContactDetailsModal` - `ContactForm` & `ContactSuccessModal` - `ReadOnlyDiagnosisField` - `AccountSettings` **New Pages:** - `/participant/schedule/[matchId]` - Call scheduling interface - `/participant/request-new-times/[matchId]` - Request new availability - `/participant/dashboard/contact` - Contact form - `/volunteer/dashboard/contact` - Volunteer contact form **API Client Updates:** - `participantMatchAPIClient` - Added `scheduleMatch`, `cancelMatch`, `requestNewTimes` - `contactAPIClient` - New client for contact form submission - `authAPIClient` - Enhanced logout to clear both backend and Firebase sessions ## Testing Checklist - [ ] Participant can schedule a call from proposed times - [ ] Participant can request new times when volunteer times don't work - [ ] Participant can cancel a scheduled call - [ ] Participant can edit profile including timezone and language - [ ] Contact form works for both participants and volunteers - [ ] Past time blocks are filtered out - [ ] Logout properly clears all session data - [ ] Seed data includes realistic test scenarios ## Known Limitations - Contact form logs messages but doesn't send emails yet (email integration pending) - All times displayed in EST (future: respect user timezone preference) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 84470d0 commit e0d2069

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4702
-1401
lines changed

backend/app/routes/auth.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,19 @@ async def logout(
6363
request: Request,
6464
credentials: HTTPAuthorizationCredentials = Depends(security),
6565
auth_service: AuthService = Depends(get_auth_service),
66+
user_service: UserService = Depends(get_user_service),
6667
):
6768
try:
68-
user_id = request.state.user_id
69-
if not user_id:
69+
auth_id = request.state.user_id # This is actually the Firebase auth_id
70+
if not auth_id:
7071
raise HTTPException(status_code=401, detail="Authentication required")
7172

73+
# Convert Firebase auth_id to database user_id (UUID)
74+
user_id = await user_service.get_user_id_by_auth_id(auth_id)
7275
auth_service.revoke_tokens(user_id)
7376
return {"message": "Successfully logged out"}
77+
except ValueError as e:
78+
raise HTTPException(status_code=404, detail=str(e))
7479
except Exception as e:
7580
raise HTTPException(status_code=500, detail=str(e))
7681

backend/app/routes/contact.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import logging
2+
3+
from fastapi import APIRouter, Depends, HTTPException, Request
4+
from sqlalchemy.orm import Session
5+
6+
from app.middleware.auth import has_roles
7+
from app.models import User
8+
from app.schemas.contact import ContactRequest, ContactResponse
9+
from app.schemas.user import UserRole
10+
from app.utilities.constants import LOGGER_NAME
11+
from app.utilities.db_utils import get_db
12+
13+
log = logging.getLogger(LOGGER_NAME("contact"))
14+
15+
router = APIRouter(
16+
prefix="/contact",
17+
tags=["contact"],
18+
)
19+
20+
21+
@router.post("/submit", response_model=ContactResponse)
22+
async def submit_contact_form(
23+
contact_data: ContactRequest,
24+
request: Request,
25+
db: Session = Depends(get_db),
26+
authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.VOLUNTEER, UserRole.ADMIN]),
27+
):
28+
"""
29+
Submit a contact form message from a user.
30+
31+
This endpoint receives contact form submissions from participants or volunteers
32+
and sends the message to the admin team.
33+
34+
Args:
35+
contact_data: The contact form data (name, email, message)
36+
request: The FastAPI request object (contains user_id from auth middleware)
37+
db: Database session
38+
39+
Returns:
40+
ContactResponse with success status and message
41+
"""
42+
try:
43+
# Get current user from auth middleware
44+
current_user_auth_id = request.state.user_id
45+
current_user = db.query(User).filter(User.auth_id == current_user_auth_id).first()
46+
47+
if not current_user:
48+
raise HTTPException(status_code=401, detail="User not found")
49+
50+
# Log the contact form submission
51+
log.info(
52+
f"Contact form submission from user {current_user.id} "
53+
f"(name: {contact_data.name}, email: {contact_data.email})"
54+
)
55+
log.info(f"Message: {contact_data.message}")
56+
57+
# TODO: Send email to admin team
58+
# This will be implemented in a future update
59+
# For now, we just log the message and return success
60+
61+
return ContactResponse(
62+
success=True,
63+
message="Your message has been sent successfully. A staff member will get back to you as soon as possible.",
64+
)
65+
66+
except HTTPException:
67+
raise
68+
except Exception as e:
69+
log.error(f"Error submitting contact form: {str(e)}")
70+
raise HTTPException(status_code=500, detail="Failed to submit contact form")

backend/app/routes/user_data.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
from fastapi import APIRouter, Depends, HTTPException, Request
55
from pydantic import BaseModel, ConfigDict
6-
from sqlalchemy.orm import Session
6+
from sqlalchemy.orm import Session, joinedload
77

88
from app.middleware.auth import has_roles
99
from app.models import Experience, Treatment, User, UserData
10+
from app.models.User import Language
11+
from app.models.VolunteerData import VolunteerData
1012
from app.schemas.user import UserRole
1113
from app.utilities.db_utils import get_db
1214

@@ -62,6 +64,7 @@ class UserDataResponse(BaseModel):
6264
has_kids: Optional[str] = None
6365
other_ethnic_group: Optional[str] = None
6466
gender_identity_custom: Optional[str] = None
67+
timezone: Optional[str] = None
6568

6669
# Cancer Experience
6770
diagnosis: Optional[str] = None
@@ -84,6 +87,9 @@ class UserDataResponse(BaseModel):
8487
# Availability (list of availability templates)
8588
availability: List[AvailabilityTemplateResponse] = []
8689

90+
# Volunteer Data (for volunteers)
91+
volunteer_experience: Optional[str] = None
92+
8793

8894
# ===== Endpoints =====
8995

@@ -106,7 +112,9 @@ async def get_my_user_data(
106112
try:
107113
# Get current user from auth middleware
108114
current_user_auth_id = request.state.user_id
109-
current_user = db.query(User).filter(User.auth_id == current_user_auth_id).first()
115+
current_user = (
116+
db.query(User).options(joinedload(User.volunteer_data)).filter(User.auth_id == current_user_auth_id).first()
117+
)
110118

111119
if not current_user:
112120
raise HTTPException(status_code=401, detail="User not found")
@@ -128,6 +136,11 @@ async def get_my_user_data(
128136
if template.is_active
129137
]
130138

139+
# Get volunteer_data.experience if user is a volunteer
140+
volunteer_experience = None
141+
if current_user.volunteer_data:
142+
volunteer_experience = current_user.volunteer_data.experience
143+
131144
# Build response with all fields and resolved relationships
132145
response = UserDataResponse(
133146
# Personal Information
@@ -147,6 +160,7 @@ async def get_my_user_data(
147160
has_kids=user_data.has_kids,
148161
other_ethnic_group=user_data.other_ethnic_group,
149162
gender_identity_custom=user_data.gender_identity_custom,
163+
timezone=user_data.timezone,
150164
# Cancer Experience
151165
diagnosis=user_data.diagnosis,
152166
date_of_diagnosis=user_data.date_of_diagnosis.isoformat() if user_data.date_of_diagnosis else None,
@@ -166,6 +180,8 @@ async def get_my_user_data(
166180
caring_for_someone=user_data.caring_for_someone,
167181
# Availability
168182
availability=availability_templates,
183+
# Volunteer Data
184+
volunteer_experience=volunteer_experience,
169185
)
170186

171187
return response
@@ -192,7 +208,9 @@ async def update_my_user_data(
192208
try:
193209
# Get current user from auth middleware
194210
current_user_auth_id = request.state.user_id
195-
current_user = db.query(User).filter(User.auth_id == current_user_auth_id).first()
211+
current_user = (
212+
db.query(User).options(joinedload(User.volunteer_data)).filter(User.auth_id == current_user_auth_id).first()
213+
)
196214

197215
if not current_user:
198216
raise HTTPException(status_code=401, detail="User not found")
@@ -300,6 +318,32 @@ async def update_my_user_data(
300318
if experience:
301319
user_data.loved_one_experiences.append(experience)
302320

321+
# Update user language (stored on User model, not UserData)
322+
if "language" in update_data:
323+
try:
324+
language_value = update_data["language"]
325+
if language_value in ["en", "fr"]:
326+
current_user.language = Language(language_value)
327+
except (ValueError, AttributeError):
328+
pass # Invalid language value, skip
329+
330+
# Update user timezone (stored on UserData model)
331+
if "timezone" in update_data:
332+
user_data.timezone = update_data["timezone"]
333+
334+
# Handle volunteer_experience update if provided
335+
if "volunteer_experience" in update_data:
336+
volunteer_data = db.query(VolunteerData).filter(VolunteerData.user_id == current_user.id).first()
337+
if volunteer_data:
338+
volunteer_data.experience = update_data["volunteer_experience"]
339+
else:
340+
# Create volunteer_data if it doesn't exist
341+
volunteer_data = VolunteerData(
342+
user_id=current_user.id,
343+
experience=update_data["volunteer_experience"],
344+
)
345+
db.add(volunteer_data)
346+
303347
db.commit()
304348
db.refresh(user_data)
305349

@@ -314,6 +358,11 @@ async def update_my_user_data(
314358
if template.is_active
315359
]
316360

361+
# Get volunteer_data.experience if user is a volunteer
362+
volunteer_experience = None
363+
if current_user.volunteer_data:
364+
volunteer_experience = current_user.volunteer_data.experience
365+
317366
response = UserDataResponse(
318367
first_name=user_data.first_name,
319368
last_name=user_data.last_name,
@@ -330,6 +379,7 @@ async def update_my_user_data(
330379
has_kids=user_data.has_kids,
331380
other_ethnic_group=user_data.other_ethnic_group,
332381
gender_identity_custom=user_data.gender_identity_custom,
382+
timezone=user_data.timezone,
333383
diagnosis=user_data.diagnosis,
334384
date_of_diagnosis=user_data.date_of_diagnosis.isoformat() if user_data.date_of_diagnosis else None,
335385
treatments=[treatment.name for treatment in user_data.treatments],
@@ -345,6 +395,7 @@ async def update_my_user_data(
345395
has_blood_cancer=user_data.has_blood_cancer,
346396
caring_for_someone=user_data.caring_for_someone,
347397
availability=availability_templates,
398+
volunteer_experience=volunteer_experience,
348399
)
349400

350401
return response

backend/app/schemas/contact.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from pydantic import BaseModel
2+
3+
4+
class ContactRequest(BaseModel):
5+
"""Schema for contact form submission"""
6+
7+
name: str
8+
email: str
9+
message: str
10+
11+
12+
class ContactResponse(BaseModel):
13+
"""Schema for contact form response"""
14+
15+
success: bool
16+
message: str

backend/app/schemas/match.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class MatchVolunteerSummary(BaseModel):
4747
first_name: Optional[str] = None
4848
last_name: Optional[str] = None
4949
email: str
50+
phone: Optional[str] = None
5051
pronouns: Optional[List[str]] = None
5152
diagnosis: Optional[str] = None
5253
age: Optional[int] = None

backend/app/seeds/users.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Seed users data for testing matching functionality."""
22

33
import uuid
4-
from datetime import date
4+
from datetime import date, time
55

66
from sqlalchemy import delete
77
from sqlalchemy.orm import Session
@@ -131,6 +131,7 @@ def seed_users(session: Session) -> None:
131131
"date_of_diagnosis": date(2018, 4, 20), # Survivor
132132
"has_blood_cancer": "yes",
133133
"caring_for_someone": "no",
134+
"timezone": "EST",
134135
},
135136
"treatments": [
136137
TreatmentId.CHEMOTHERAPY,
@@ -143,6 +144,12 @@ def seed_users(session: Session) -> None:
143144
ExperienceId.FATIGUE,
144145
ExperienceId.RETURNING_TO_WORK,
145146
],
147+
"volunteer_experience": "My journey with blood cancer started when I was about twelve years old and getting treatment for the first time was extremely stress-inducing. My journey with blood cancer started when I was about twelve years old and getting treatment for the first time was extremely stress-inducing.",
148+
"availability_templates": [
149+
{"day_of_week": 1, "start_time": time(14, 0), "end_time": time(16, 0)}, # Tuesday 2-4pm
150+
{"day_of_week": 3, "start_time": time(14, 0), "end_time": time(17, 0)}, # Thursday 2-5pm
151+
{"day_of_week": 4, "start_time": time(10, 0), "end_time": time(12, 0)}, # Friday 10am-12pm
152+
],
146153
},
147154
{
148155
"role": "volunteer",
@@ -165,9 +172,16 @@ def seed_users(session: Session) -> None:
165172
"date_of_diagnosis": date(2020, 8, 15), # Survivor
166173
"has_blood_cancer": "yes",
167174
"caring_for_someone": "no",
175+
"timezone": "PST",
168176
},
169177
"treatments": [3, 6], # Chemotherapy, Radiation (matching Sarah's preferences)
170178
"experiences": [1, 3, 4], # Brain Fog, Feeling Overwhelmed, Fatigue (same as Sarah!)
179+
"volunteer_experience": "I was diagnosed with ALL at 34 and underwent intensive chemotherapy and radiation. The hardest part was balancing treatment with being a mother. I want to help others navigate this difficult journey and share what I learned.",
180+
"availability_templates": [
181+
{"day_of_week": 0, "start_time": time(16, 0), "end_time": time(18, 0)}, # Monday 4-6pm
182+
{"day_of_week": 2, "start_time": time(11, 0), "end_time": time(13, 0)}, # Wednesday 11am-1pm
183+
{"day_of_week": 4, "start_time": time(13, 0), "end_time": time(15, 0)}, # Friday 1-3pm
184+
],
171185
},
172186
{
173187
"role": "volunteer",
@@ -190,9 +204,16 @@ def seed_users(session: Session) -> None:
190204
"date_of_diagnosis": date(2020, 2, 14),
191205
"has_blood_cancer": "yes",
192206
"caring_for_someone": "no",
207+
"timezone": "EST",
193208
},
194209
"treatments": [3, 6], # Chemotherapy, Radiation
195210
"experiences": [10, 11, 7], # Anxiety/Depression, PTSD, Returning to work
211+
"volunteer_experience": "Fighting Hodgkin Lymphoma taught me resilience I never knew I had. The mental health challenges were just as tough as the physical ones. I'm here to listen and support anyone going through similar struggles.",
212+
"availability_templates": [
213+
{"day_of_week": 1, "start_time": time(9, 0), "end_time": time(11, 0)}, # Tuesday 9-11am
214+
{"day_of_week": 3, "start_time": time(14, 0), "end_time": time(16, 0)}, # Thursday 2-4pm
215+
{"day_of_week": 5, "start_time": time(10, 0), "end_time": time(12, 0)}, # Saturday 10am-12pm
216+
],
196217
},
197218
# High-matching volunteers for Sarah Johnson
198219
{
@@ -216,9 +237,15 @@ def seed_users(session: Session) -> None:
216237
"date_of_diagnosis": date(2019, 5, 10), # Survivor
217238
"has_blood_cancer": "yes",
218239
"caring_for_someone": "no",
240+
"timezone": "EST",
219241
},
220242
"treatments": [3, 6], # Chemotherapy, Radiation (matching Sarah's preferences)
221243
"experiences": [1, 3, 4], # Brain Fog, Feeling Overwhelmed, Fatigue (same as Sarah!)
244+
"volunteer_experience": "As a working mother of two, being diagnosed with leukemia turned my world upside down. Brain fog and fatigue made everyday tasks feel impossible. Now in remission, I'm passionate about helping others find their strength during treatment.",
245+
"availability_templates": [
246+
{"day_of_week": 2, "start_time": time(14, 0), "end_time": time(17, 0)}, # Wednesday 2-5pm
247+
{"day_of_week": 3, "start_time": time(16, 0), "end_time": time(18, 0)}, # Thursday 4-6pm
248+
],
222249
},
223250
{
224251
"role": "volunteer",
@@ -241,9 +268,15 @@ def seed_users(session: Session) -> None:
241268
"date_of_diagnosis": date(2021, 3, 18), # Survivor
242269
"has_blood_cancer": "yes",
243270
"caring_for_someone": "no",
271+
"timezone": "MST",
244272
},
245273
"treatments": [3, 6], # Chemotherapy, Radiation (matching Sarah's preferences)
246274
"experiences": [1, 3, 4, 10], # Brain Fog, Feeling Overwhelmed, Fatigue, Anxiety/Depression
275+
"volunteer_experience": "My cancer journey came with intense anxiety and depression alongside the physical symptoms. Through therapy and support groups, I learned to cope with the overwhelming emotions. I want to be that supportive voice for others facing the same battles.",
276+
"availability_templates": [
277+
{"day_of_week": 0, "start_time": time(10, 0), "end_time": time(12, 0)}, # Monday 10am-12pm
278+
{"day_of_week": 4, "start_time": time(14, 30), "end_time": time(16, 30)}, # Friday 2:30-4:30pm
279+
],
247280
},
248281
{
249282
"role": "volunteer",
@@ -266,9 +299,16 @@ def seed_users(session: Session) -> None:
266299
"date_of_diagnosis": date(2018, 9, 25), # Survivor
267300
"has_blood_cancer": "yes",
268301
"caring_for_someone": "no",
302+
"timezone": "CST",
269303
},
270304
"treatments": [3, 6, 7], # Chemotherapy, Radiation, Maintenance Chemo
271305
"experiences": [1, 4, 5], # Brain Fog, Feeling Overwhelmed, Fatigue (same as Sarah!)
306+
"volunteer_experience": "Seven years ago, ALL changed everything. The fatigue was relentless, and brain fog made me feel like a stranger in my own mind. But I made it through, and now I want to offer hope and practical advice to anyone starting this journey.",
307+
"availability_templates": [
308+
{"day_of_week": 1, "start_time": time(15, 0), "end_time": time(17, 0)}, # Tuesday 3-5pm
309+
{"day_of_week": 3, "start_time": time(10, 0), "end_time": time(12, 0)}, # Thursday 10am-12pm
310+
{"day_of_week": 5, "start_time": time(14, 0), "end_time": time(16, 0)}, # Saturday 2-4pm
311+
],
272312
},
273313
# Test Case 3: Participant who is a caregiver wanting caregiver volunteers
274314
{
@@ -434,6 +474,22 @@ def seed_users(session: Session) -> None:
434474
)
435475
session.add(volunteer_data)
436476

477+
# Add availability templates for volunteers (if specified)
478+
# Skip [email protected] so they can set their own availability
479+
if (
480+
user_info.get("availability_templates")
481+
and user_info["user_data"]["email"] != "[email protected]"
482+
):
483+
for template_info in user_info["availability_templates"]:
484+
availability_template = AvailabilityTemplate(
485+
user_id=user.id,
486+
day_of_week=template_info["day_of_week"],
487+
start_time=template_info["start_time"],
488+
end_time=template_info["end_time"],
489+
is_active=True,
490+
)
491+
session.add(availability_template)
492+
437493
created_users.append((user, user_info["role"]))
438494
print(f"Added {user_info['role']}: {user.first_name} {user.last_name}")
439495

0 commit comments

Comments
 (0)