Skip to content

Commit 1a36d75

Browse files
YashK2005UmairHundekar
authored andcommitted
Admin Profile View (#77)
## Notion ticket link <!-- Please replace with your ticket's URL --> [Ticket Name](https://www.notion.so/uwblueprintexecs/Task-Board-db95cd7b93f245f78ee85e3a8a6a316d) ## Implementation description * Replaced ad-hoc available_time records with recurring `AvailabilityTemplate`s: new model + migrations, schema updates, timezone utilities, and service logic that creates/deletes template slots, validates input, and projects active templates when Hydrating users/admin lists. * Added an admin `PATCH /users/{id}/user-data` flow backed by `UserDataUpdateRequest`, expands `UserService` to materialize treatments/ experiences/loved-one info, and ensures responses always include active availability templates; extended seeds and helper scripts to create/check/ undo match data plus availability seeding docs. * Hardened `MatchService` to only attach suggestions for volunteers with templates and to compare template weekdays in the volunteer’s timezone before converting to UTC; added comprehensive unit coverage (`test_availability_service`, `test_match_service`, `test_match_service_timezone`, `test_user_data_update`). * Built the admin user profile experience (`/admin/users/[id]`): navigation sidebar, profile summary card, editable cancer experience/loved-one sections, availability grid editor with drag-select, success messaging, and supporting hooks (`useUserProfile`, `useProfileEditing`, `useAvailabilityEditing`, `useIntakeOptions`) plus Chakra-based UI components. * Updated the admin directory/header to route into the new profile view, expanded API clients/types/utilities to handle user-data patching + availability templates, and added UI polish (single-select dropdown upgrades, user profile formatting helpers, date utilities, success banner, react- datepicker). ## Steps to test 1. `docker-compose up -d` then run `cd backend && pdm run alembic upgrade head` to apply the availability template migrations. 2. `cd backend && pdm run dev` and `cd frontend && npm run dev`; sign in to the admin portal. 3. Visit `/admin/directory`, open any volunteer, and verify the profile page renders summary, profile, cancer, loved-one, and availability sections with existing data. 4. Click “Edit” in Profile Summary to update personal/demographic fields, save, and confirm the success toast plus persisted values via a hard refresh; repeat for cancer and loved-one sections (including clearing dates). 5. Use “Edit Availability” to drag-select new slots, save, and confirm the grid + subsequent reload reflect the template changes (verify via network tab hitting `/availability`). 6. Optional back-end verification: run `cd backend && pdm run tests backend/tests/unit/test_availability_service.py backend/tests/unit/ test_match_service_timezone.py backend/tests/unit/test_user_data_update.py` to exercise the new logic, and use the helper scripts (`create_test_matches.py`, `check_matches.py`, `undo_test_changes.py`) to inspect match suggestion behavior. ## What should reviewers focus on? * **Migrations & data model:** ensure the `availability_templates` migration, seeds, and new helper scripts won’t clobber production data when rolled out. * **User data patching:** confirm `UserService.update_user_data_by_id` correctly hydrates treatments/experiences and returns availability templates so the admin UI state stays consistent. * **Matching/timezone logic:** double-check `_attach_initial_suggested_times` now uses volunteer-local weekdays and that suggested blocks remain correct across timezones. * **Admin UI flows:** exercise the new hooks/components for editing profile sections and availability (loading states, drag interactions, and API payloads) for both happy paths and cancellations. * **API client/type updates:** watch for any regressions in other consumers now that `UserResponse` carries availability templates and the auth API client maps the new schemas. ## Checklist - [ ] 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
1 parent bb04542 commit 1a36d75

Some content is hidden

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

52 files changed

+6615
-399
lines changed

.pre-commit-config.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# .pre-commit-config.yaml
2+
# Root-level config to handle both frontend and backend linting/formatting
3+
4+
default_language_version:
5+
python: python3.12
6+
7+
default_stages: [pre-commit, pre-push]
8+
9+
repos:
10+
# Backend: Ruff linting and formatting
11+
- repo: https://github.com/charliermarsh/ruff-pre-commit
12+
rev: v0.6.7
13+
hooks:
14+
- id: ruff
15+
args: [--fix, --line-length=120]
16+
files: ^backend/
17+
- id: ruff-format
18+
args: [--line-length=120]
19+
files: ^backend/
20+
21+
# Frontend: Prettier formatting
22+
- repo: https://github.com/pre-commit/mirrors-prettier
23+
rev: v4.0.0-alpha.8
24+
hooks:
25+
- id: prettier
26+
files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$
27+
exclude: ^frontend/(node_modules|\.next|out|build)/
28+
types_or: [file]
29+
additional_dependencies:
30+

backend/.pre-commit-config.yaml

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Time
2+
from sqlalchemy.dialects.postgresql import UUID
3+
from sqlalchemy.orm import relationship
4+
from sqlalchemy.sql import func
5+
6+
from .Base import Base
7+
8+
9+
class AvailabilityTemplate(Base):
10+
"""
11+
Stores recurring weekly availability patterns for volunteers.
12+
Each template represents a time slot on a specific day of the week.
13+
These templates are projected forward to create specific TimeBlocks for matches.
14+
"""
15+
16+
__tablename__ = "availability_templates"
17+
18+
id = Column(Integer, primary_key=True)
19+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
20+
21+
# Day of week: 0=Monday, 1=Tuesday, ..., 6=Sunday
22+
day_of_week = Column(Integer, nullable=False)
23+
24+
# Time of day (just time, no date)
25+
start_time = Column(Time, nullable=False) # e.g., 14:00:00
26+
end_time = Column(Time, nullable=False) # e.g., 16:00:00
27+
28+
# Optional: for future enhancements (e.g., temporarily disable a template)
29+
is_active = Column(Boolean, default=True)
30+
31+
created_at = Column(DateTime(timezone=True), server_default=func.now())
32+
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
33+
34+
user = relationship("User", back_populates="availability_templates")

backend/app/models/TimeBlock.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,3 @@ class TimeBlock(Base):
1414

1515
# suggested matches
1616
suggested_matches = relationship("Match", secondary="suggested_times", back_populates="suggested_time_blocks")
17-
18-
# the availability that the timeblock is a part of for a given user
19-
users = relationship("User", secondary="available_times", back_populates="availability")

backend/app/models/User.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class User(Base):
4545

4646
role = relationship("Role")
4747

48-
# time blocks in an availability for a user
49-
availability = relationship("TimeBlock", secondary="available_times", back_populates="users")
48+
# recurring availability templates (day of week + time)
49+
availability_templates = relationship("AvailabilityTemplate", back_populates="user")
5050

5151
participant_matches = relationship("Match", back_populates="participant", foreign_keys=[Match.participant_id])
5252

backend/app/models/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55

66
from app.utilities.constants import LOGGER_NAME
77

8-
from .AvailableTime import available_times
9-
108
# Make sure all models are here to reflect all current models
119
# when autogenerating new migration
10+
from .AvailabilityTemplate import AvailabilityTemplate
1211
from .Base import Base
1312
from .Experience import Experience
1413
from .Form import Form
@@ -35,8 +34,8 @@
3534
"Match",
3635
"MatchStatus",
3736
"User",
38-
"available_times",
3937
"suggested_times",
38+
"AvailabilityTemplate",
4039
"UserData",
4140
"Treatment",
4241
"Experience",

backend/app/routes/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# TODO: ADD RATE LIMITING
1818
@router.post("/register", response_model=UserCreateResponse)
1919
async def register_user(user: UserCreateRequest, user_service: UserService = Depends(get_user_service)):
20-
allowed_Admins = ["[email protected]", "[email protected]"]
20+
2121
if user.role == UserRole.ADMIN:
2222
if user.email not in allowed_Admins:
2323
raise HTTPException(status_code=403, detail="Access denied. Admin privileges required for admin portal")

backend/app/routes/user.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
UserRole,
1212
UserUpdateRequest,
1313
)
14+
from app.schemas.user_data import UserDataUpdateRequest
1415
from app.services.implementations.user_service import UserService
1516
from app.utilities.service_utils import get_user_service
1617

@@ -89,6 +90,22 @@ async def update_user(
8990
raise HTTPException(status_code=500, detail=str(e))
9091

9192

93+
# admin only update user_data (cancer experience, treatments, experiences, etc.)
94+
@router.patch("/{user_id}/user-data", response_model=UserResponse)
95+
async def update_user_data(
96+
user_id: str,
97+
user_data_update: UserDataUpdateRequest,
98+
user_service: UserService = Depends(get_user_service),
99+
authorized: bool = has_roles([UserRole.ADMIN]),
100+
):
101+
try:
102+
return await user_service.update_user_data_by_id(user_id, user_data_update)
103+
except HTTPException as http_ex:
104+
raise http_ex
105+
except Exception as e:
106+
raise HTTPException(status_code=500, detail=str(e))
107+
108+
92109
# admin only delete user
93110
@router.delete("/{user_id}")
94111
async def delete_user(
@@ -110,6 +127,7 @@ async def delete_user(
110127
async def deactivate_user(
111128
user_id: str,
112129
user_service: UserService = Depends(get_user_service),
130+
authorized: bool = has_roles([UserRole.ADMIN]),
113131
):
114132
try:
115133
await user_service.soft_delete_user_by_id(user_id)
@@ -118,3 +136,19 @@ async def deactivate_user(
118136
raise http_ex
119137
except Exception as e:
120138
raise HTTPException(status_code=500, detail=str(e))
139+
140+
141+
# reactivate user
142+
@router.post("/{user_id}/reactivate")
143+
async def reactivate_user(
144+
user_id: str,
145+
user_service: UserService = Depends(get_user_service),
146+
authorized: bool = has_roles([UserRole.ADMIN]),
147+
):
148+
try:
149+
await user_service.reactivate_user_by_id(user_id)
150+
return {"message": "User reactivated successfully"}
151+
except HTTPException as http_ex:
152+
raise http_ex
153+
except Exception as e:
154+
raise HTTPException(status_code=500, detail=str(e))
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
from datetime import time
12
from typing import List
23
from uuid import UUID
34

45
from pydantic import BaseModel
56

6-
from app.schemas.time_block import TimeBlockEntity, TimeRange
7+
8+
class AvailabilityTemplateSlot(BaseModel):
9+
"""Represents a single availability template slot (day of week + time range)"""
10+
11+
day_of_week: int # 0=Monday, 1=Tuesday, ..., 6=Sunday
12+
start_time: time # e.g., 14:00:00
13+
end_time: time # e.g., 16:00:00
714

815

916
class CreateAvailabilityRequest(BaseModel):
1017
user_id: UUID
11-
available_times: List[TimeRange]
18+
templates: List[AvailabilityTemplateSlot]
1219

1320

1421
class CreateAvailabilityResponse(BaseModel):
@@ -22,17 +29,15 @@ class GetAvailabilityRequest(BaseModel):
2229

2330
class AvailabilityEntity(BaseModel):
2431
user_id: UUID
25-
available_times: List[TimeBlockEntity]
32+
templates: List[AvailabilityTemplateSlot]
2633

2734

2835
class DeleteAvailabilityRequest(BaseModel):
2936
user_id: UUID
30-
delete: list[TimeRange] = []
37+
templates: List[AvailabilityTemplateSlot] = []
3138

3239

3340
class DeleteAvailabilityResponse(BaseModel):
3441
user_id: UUID
3542
deleted: int
36-
37-
# return the user’s availability after the update
38-
availability: List[TimeBlockEntity]
43+
templates: List[AvailabilityTemplateSlot] # remaining templates after deletion

backend/app/schemas/user.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
1111

12+
from .availability import AvailabilityTemplateSlot
13+
from .user_data import UserDataResponse
14+
from .volunteer_data import VolunteerDataResponse
15+
1216
# TODO:
1317
# confirm complexity rules for fields (such as password)
1418

@@ -135,8 +139,12 @@ class UserResponse(BaseModel):
135139
role_id: int
136140
auth_id: str
137141
approved: bool
142+
active: bool
138143
role: "RoleResponse"
139144
form_status: FormStatus
145+
user_data: Optional[UserDataResponse] = None
146+
volunteer_data: Optional[VolunteerDataResponse] = None
147+
availability: List[AvailabilityTemplateSlot] = []
140148

141149
model_config = ConfigDict(from_attributes=True)
142150

0 commit comments

Comments
 (0)