Context
Linked to need4deed-org/fe#777 — the NGO/agent profile's "Volunteers" section (VolunteerAgents.tsx) is currently a stub with no data source at all, so it always shows 0 across every status tab even when the agent has volunteers matched via its opportunities.
Problem
There's no endpoint that returns "all volunteers connected to this agent through its opportunities." The equivalent already exists per-opportunity (GET /opportunity/:id/volunteer-linked), and per-agent for opportunities (GET /agent/:id/opportunity-linked), but not the agent→volunteer aggregate.
Proposed solution
Add GET /agent/:id/volunteer-linked, mirroring agent-opportunity.routes.ts / opportunity-volunteer.routes.ts:
- Query
OpportunityVolunteer filtered by opportunity.agentId = :id (join through the opportunity relation), with the same relations as the existing opportunity-scoped route (volunteer.person, volunteer.deal.dealActivity.activity, volunteer.deal.dealSkill.skill, volunteer.deal.dealLanguage.language, volunteer.deal.dealTimeslot.timeslot, volunteer.deal.dealDistrict.district).
- Reuse the existing
opportunityOpportunityVolunteerDTO (services/dto/dto-opportunity-volunteer.ts) — no new SDK types needed, response shape is the same ApiVolunteerOpportunityGet[] the opportunity-scoped route already returns.
- Register under
RoutePrefix.VOLUNTEER_LINKED (already exists) nested under /agent/:id, same pattern as agentOpportunityRoutes.
- Apply the same
makePiiSerialization PII masking as the sibling routes.
Acceptance criteria
Context
Linked to need4deed-org/fe#777 — the NGO/agent profile's "Volunteers" section (
VolunteerAgents.tsx) is currently a stub with no data source at all, so it always shows 0 across every status tab even when the agent has volunteers matched via its opportunities.Problem
There's no endpoint that returns "all volunteers connected to this agent through its opportunities." The equivalent already exists per-opportunity (
GET /opportunity/:id/volunteer-linked), and per-agent for opportunities (GET /agent/:id/opportunity-linked), but not the agent→volunteer aggregate.Proposed solution
Add
GET /agent/:id/volunteer-linked, mirroringagent-opportunity.routes.ts/opportunity-volunteer.routes.ts:OpportunityVolunteerfiltered byopportunity.agentId = :id(join through the opportunity relation), with the same relations as the existing opportunity-scoped route (volunteer.person,volunteer.deal.dealActivity.activity,volunteer.deal.dealSkill.skill,volunteer.deal.dealLanguage.language,volunteer.deal.dealTimeslot.timeslot,volunteer.deal.dealDistrict.district).opportunityOpportunityVolunteerDTO(services/dto/dto-opportunity-volunteer.ts) — no new SDK types needed, response shape is the sameApiVolunteerOpportunityGet[]the opportunity-scoped route already returns.RoutePrefix.VOLUNTEER_LINKED(already exists) nested under/agent/:id, same pattern asagentOpportunityRoutes.makePiiSerializationPII masking as the sibling routes.Acceptance criteria
GET /agent/:id/volunteer-linkedreturns all volunteers matched to any of the agent's opportunities, in the same shape asGET /opportunity/:id/volunteer-linked