Staffing — Departments, Employees, Projects, Tasks, HR Pool
In plain terms: this is P4P's own internal PM/staffing tool — the place the company runs its own departments, projects, and tasks, and reviews people who want to work with P4P. Despite the /team/* URL shape looking like a customer organization's workspace, it isn't one: see Architectural tiers — this whole module is Platform-tier, gated by platform:staffing:* and a PlatformStaffMembership (you must be recognized P4P staff to see any of it), not by anything Organization-scoped.
All routes share middleware: 'platform-access' + requiresPlatformPermission: 'platform:staffing:read' (page-level gate; individual mutating actions require :manage, see below) and layout: 'team'. Backend: apps/core-api/src/modules/staffing/*.
How to...
Grouped to mirror the technical sections below — each page's own help icon links straight into the group relevant to it, not this whole list.
Departments
Create a department — Departments → New department → Name, pick a Leader → Create department. See field guide below for what each field means.
Edit a department — open the department → the ⋯ menu (top right) → Edit department. Same fields as creating one — see the field guide below.
Create/edit a department — field guide
- Name (required) — the department's display name, shown everywhere in the app (lists, breadcrumbs, cards). Must be 2–100 characters — leaving it blank or typing a single character shows "Must be at least 2 characters," not "This field is required" (the 2-character minimum is what actually triggers, even on an empty field).
- Leader (required) — search and pick a staff member; they become this department's leader, which grants them direct access to its own finance book and role catalog (see the permission model above) — no separate permission to assign, leadership alone is the access.
- Description (optional) — context about what this department does, shown on its Overview tab. Up to 500 characters.
- Parent department (optional) — leave as "None — top-level department" unless this is meant to be a sub-department. A sub-department can't itself have sub-departments — only one level deep, so this dropdown only offers top-level departments to pick from.
Delete a department — open the department → ⋯ → Delete. Blocked while it still has active sub-departments — see Sub-departments.
Members
Add someone to a department — open the department → Members tab → Add member → pick staff members → Add (N). See field guide below.
Assign or change a member's department role — department → Members tab → change their role from the row's own control (leader or :manage). See field guide below.
Remove someone from a department — department → Members tab → the row's ⋯ → Remove from department.
Members tab — field guide
- Add member — one field: search and pick one or more staff members. The list only shows staff who aren't already in this department. No text validation — the Add (N) button just stays disabled until you've picked at least one person.
- Change role — one field: a dropdown of this department's own roles (see Roles field guide below), or "No role." Picking a role shows its functions text right underneath automatically — there's no separate field to fill in for that. If the department has no roles yet, the dialog links you to the Roles tab instead of showing an empty dropdown.
Roles
Create a department role — department → Roles tab → New role → Name, optionally Functions → Create role. See field guide below.
Edit or delete a department role — department → Roles tab → the row's ⋯ → Edit or Delete. A role still assigned to members can't be deleted — reassign them first.
Create/edit a department role — field guide
A department role is a job-title-like label with a free-text description — it does not carry any permissions or access rights (that's what organization roles and platform roles are for, an entirely different concept). It exists purely to organize and describe who does what within the department.
- Name (required) — the role's display name, e.g. "Recruiter." 1–100 characters. The submit button is only disabled while this is empty — going over 100 characters isn't caught until you actually submit, where it shows "Must be at most 100 characters."
- Functions (optional) — free text describing what this role is responsible for. Shown to everyone assigned this role, and previewed automatically whenever someone picks the role from the Members tab's role dropdown. Up to 2,000 characters.
The single automatically-created Leader role can't be renamed (its Name field is disabled) — it always follows whoever is Department.leaderId, never manually assigned.
Projects (department's Projects tab)
Attach a project to a department — department → Projects tab → Attach project → pick one → confirm. See also Projects → Departments for the reverse direction (same relationship, attached from the project's own side instead).
Detach a project from a department — department → Projects tab → the row's ⋯ → Detach from department.
Sub-departments
Create a sub-department — department → Sub-departments tab → New sub-department (same form as creating any department, with Parent department pre-filled).
Employees
Adding someone here isn't a staffing action — it's the same platform-staff invitation flow as Platform Administration's How to..., just linked from this page for convenience.
Send someone on leave, or bring them back — open their profile → the ⋯ menu → Send on leave / Return from leave. Internal-staffing status only, not an account suspension — see Platform Administration → Users.
Projects
Create a project — Projects → New project → fill it in → Create. See field guide below.
Edit a project — either from the Projects list (row's ⋯ → Edit) or from inside the project itself (pencil icon next to its title). Same fields as creating one, plus Status and Progress — see field guide below.
Delete a project — Projects list → the row's ⋯ → Delete. Permanently deletes everything in it — departments, stages, tasks, and participants.
Create/edit a project — field guide
- Name (required) — the project's display name everywhere in the app. 1–100 characters.
- Description (optional) — a short summary, shown in project lists and cards. Up to 1,000 characters.
- Vision (optional) — a longer, richly-formatted field (bold, lists, links) for why this project matters and what "done" looks like — shown on the project's Overview tab, and also editable directly there. Up to 2,000 characters.
- Icon (required) — pick one from the grid; there's no default. Trying to submit without picking one shows "This field is required" right under the grid and the request never goes out — easy to miss since nothing else on the form calls attention to it.
- Status (edit only) — Planned / Active / Completed / Archived. Not offered when creating — every new project starts as Planned.
- Progress (edit only) — a 0–100 number, shown as the project's progress bar on the Overview tab. Set by hand, not derived from task or stage completion — nothing recalculates it for you (stages have their own, separate mismatch warning — see Stages field guide below).
- Owner (optional) — search and pick a staff member; leave unset and it defaults to you. Purely informational (shown on the Overview tab) — doesn't gate any permission by itself.
Departments (project's Departments tab)
Attach a department to a project — project → Departments tab → Attach department → pick one → Attach department. See also Departments → Projects for the reverse direction.
Detach a department from a project — project → Departments tab → the row's ⋯ → Detach department.
Participants
Attach a contractor to a project — project → Participants tab → Attach participant → pick someone from the HR Pool → confirm. Everyone from an attached department shows up here automatically (via their department membership) — no separate step for them. See field guide below.
Edit or detach a participant — project → Participants tab → the row's ⋯ → Edit participant (their project role/responsibilities) or Detach participant.
Attach/edit a participant — field guide
This dialog only ever attaches HR Pool contractors — a staff member from an attached department is already reachable as a task assignee through their department membership, with nothing to attach here (see Participants above).
- Person (required, attach only — not shown when editing) — only Approved HR Pool entries appear in this list; anyone still pending review isn't offered. If there are none, the dropdown says so instead of showing nothing.
- Role (optional) — free text, e.g. "Frontend Developer" — this project's own label for what they do, separate from any department role.
- Responsibilities (optional) — free text describing what they're doing on this project. No length limit either field enforces.
Stages
Add a stage — project → Stages tab → Add stage → Title/Description/dates → create.
Reorder a project's roadmap — project → Stages tab → move a stage with the ↑/↓ controls.
Edit or delete a stage — project → Stages tab → the row's ⋯ → Edit stage or Delete stage. Deleting detaches its tasks rather than deleting them.
Add/edit a stage — field guide
- Title (required) — 1–100 characters.
- Description (optional) — up to 500 characters.
- Start date / Due date (both optional) — pick either or both. The two date pickers watch each other live: Start date won't let you pick anything past whatever Due date is already set to, and vice versa — an invalid range is prevented at the calendar itself, not caught after the fact with an error message.
- Status (edit only) — Planned / In progress / Completed. Not offered when creating — every new stage starts as Planned. This is set by hand, not derived from its tasks — the stage list separately flags a mismatch ("Marked completed, but tasks are still open" or "All tasks are done, but the stage isn't marked completed") when this status and the tasks' own statuses disagree, but it never corrects either side for you.
- Owner (optional) — a staff member, purely informational, same as a project's own Owner field.
Tasks (project's Tasks tab)
Creating and managing tasks from inside a project uses the exact same flow as Tasks below, just pre-scoped to this project.
Tasks
Create a task from anywhere — Tasks (the cross-project board) → New task → title → Select a project → Select a member → Create task. See field guide below.
Change a task's status/assignee/etc. — open the task; every field is inline-editable, click-to-edit (Jira-style) — no separate "edit" mode. See field guide below for the fields only reachable this way, not from the create dialog.
Reconfigure the task status list — Tasks → gear icon (platform:staffing:configure only) → Configure columns → add/rename/reorder/deactivate statuses. The 5 seeded system statuses can only be deactivated, not deleted. See field guide below.
Delete a task — open the task → the ⋯ menu → Delete.
Create/edit a task — field guide
The New task dialog only covers the fields below; Department, Price, Subtask of, and reassigning Status are not offered when creating — they only exist as inline-editable fields on the task detail page once it exists (see the intro to this list above).
- Title (required) — 1–150 characters. Unlike most other create dialogs in this module, the Create task button itself stays disabled until Title, Project, and Assignee are all filled — you won't get a validation message after clicking, the button simply won't respond yet.
- Project (required) — search and pick one; every other field on this form (Stage, Assignee, Helpers) depends on which project is chosen and resets whenever you change it.
- Priority — Urgent / High / Medium / Low. Defaults to Medium.
- Stage (optional) — only appears once a project is picked; offers that project's own stages, or "No stages yet" if it has none.
- Description (optional) — up to 2,000 characters.
- Assignee (required) — the task's RESPONSIBLE person. Only people eligible for the chosen project show up here: staff from the project's attached departments, plus HR Pool contractors already attached to that specific project (not the whole HR Pool) — see Participants field guide.
- Due date (optional).
- Helpers (optional) — any number of additional people in a supporting HELPER role, picked from the same eligible list as Assignee, minus whoever is already the Assignee.
- Department (edit only, inline on the task page) — must be one of the task's project's own attached departments (see Departments above) — enforced server-side, not just a dropdown limitation.
- Price (edit only, inline) — a plain number, internal budget bookkeeping only; doesn't create or link to any real ledger entry by itself (compare Task payouts, which does).
- Subtask of (edit only, inline) — must be a top-level task in the same project; a subtask can't itself have subtasks (one level deep, same cap as departments and sub-departments).
Configure columns — field guide
Each row here is a status a task can sit in — the same statuses that become kanban board columns in every project's Tasks tab and the cross-project Tasks page.
- Name — 1–50 characters. On the 5 seeded system rows (To Do, In Progress, In Review, Blocked, Done, each tagged System) this field isn't editable at all — their display name is translated per-language rather than stored as plain text, so editing it here would silently do nothing; the box for those rows just shows the name, it isn't an input. Custom rows are free text in whatever language you want, committed on blur/Enter rather than live per keystroke. Going over 50 characters doesn't stop you from typing, but the save is rejected — the box keeps showing what you typed until you shorten it and try again.
- Color — one of 6 swatches (Gray/Blue/Violet/Amber/Red/Green), purely visual — no effect on behavior, just which color the column/badge renders in everywhere the status appears.
- Category — TODO / In progress / Done. This is not cosmetic: it drives how a task's Progress % is calculated. Every active, non-Done status is split into an equal band of the 0–100 range in status order (e.g. 3 such statuses → bands of 0–33/33–67/67–100); moving a task into any Done-category status pins its progress straight to 100, regardless of where it sits in the reorder list.
- Reorder (↑/↓ arrows) — changes both the kanban column order and the Category progress bands above, since the bands are computed in this same order.
- Active toggle (system rows only) — the restorable alternative to deleting a system status: switching it off hides it from the board and status pickers everywhere but keeps its history. The default status (the one new tasks start in — currently To Do) can't be switched off; make another status default first. If the status still has tasks on it, turning it off asks where to move them first — same picker as delete, below.
- Delete (trash icon, custom rows only — system rows can only be deactivated, never deleted) — disabled if it's the only status left. Deleting one that still has tasks requires picking another status to move them to first; nothing is ever silently stranded.
- New status name / color / category (bottom row) — adds a new custom status at the end of the order; the Add button stays disabled until you type a name.
Dashboard & Activity
Both are read-only views — nothing to configure here. The dashboard's "My payouts" banner and the activity feed's filters are covered in Finance and the module sections below, respectively.
HR Pool
Move an HR Pool applicant through review — HR Pool → the row's ⋯ menu → Take in review, then Approve, then Invite to platform (sends a platform staff invitation). The steps don't have to happen one at a time — a fresh Submitted entry can be Approved directly, skipping Take in review.
Edit an entry's classification — open the entry → ⋯ menu → Edit. See field guide below.
Add a review note — open the entry → Review notes → write one. Separate from the applicant's own self-reported notes, and only visible to staff. See field guide below.
Delete an HR Pool entry — the row's ⋯ menu → Delete. Only possible for an entry with no project/task history.
Shared Documents
Upload a shared document — Shared Documents → the Files tab → Upload document.
Delete or restore a shared document — the row's ⋯ menu → Delete (moves it to Trash, 30-day retention); from the Trash tab, Restore or Delete permanently.
The permission model in one table
Four permissions, one pattern repeated across every controller in this module:
| Permission | Grants |
|---|---|
platform:staffing:read | View everything — every list/detail page's baseline gate |
platform:staffing:manage | Create/edit/delete departments, projects, tasks, HR pool entries; manage any task regardless of who created it |
platform:staffing:manage_own | On a task you created or are assigned to (RESPONSIBLE/HELPER) only: change status/progress/description/due date/priority, and comment — never checked via a route decorator, always resolved in TasksService.resolveTaskWriteAccess() against the specific task |
platform:staffing:configure | The task-status registry only (colors/order/system-status activation) — a global, shared setting, deliberately split from day-to-day :manage |
A few routes intentionally carry no @RequirePlatformPermission decorator at all — department finance-book entries and department role-management routes use a data-driven "is this actor the department's leader" check instead (DepartmentLeaderOrStaffingManageGuard or a raw service-level check), because that eligibility is per-record, not a thing a platform permission string can express (same rule docs/iam/PERMISSIONS_CATALOG.md documents for organization-level per-record access).
Departments (/team/departments)
In plain terms: the org chart. A department has a leader, members, its own roles (job titles within it, separate from platform roles), attached projects, and — if the leader chooses to keep one — its own finance book.
- List/tree view + card view (persisted locally). "New department" (
:manage) opens the same form dialog used for editing. - Detail page tabs: Overview (stats, sub-departments, active projects, recent activity), Activity, Finances (see Finance → Department's own book), Members, Projects, Roles, Sub-departments — each covered in its own section below.
- Deleting a department (
:manage) is blocked server-side while it still has active sub-departments.
Members (department's Members tab)
Add (:manage only — the leader carve-out does not extend to adding people), remove (:manage), change a member's department role (leader or :manage, via DepartmentLeaderOrStaffingManageGuard — no permission string).
Roles (department's Roles tab)
A department's own DepartmentRole catalog (leader or :manage) — distinct from platform roles and from organization roles elsewhere in the wiki; a role in active use can't be deleted.
Projects (department's Projects tab)
Attach/detach a project to this department (:manage), with a per-department task/budget breakdown. The reverse direction — attaching a department to a project, and everything else project-side — is covered under Projects below.
Sub-departments (department's Sub-departments tab)
Capped at one level deep — a sub-department can't itself have sub-departments. This is also why deleting a department with active sub-departments is blocked (see above): promoting or reassigning them first is a deliberate step, not something a delete should do silently.
Employees (/team/members)
In plain terms: every P4P staff member's own profile — but adding someone here isn't a staffing action, it's the same platform-staff invitation flow from Platform Administration, just linked from this page for convenience.
- List: search + department/status filters. "Add member" is gated by
:manageand by whoever can actually send the invite (SUPER_ADMINorP4P Owner— mirrors the real backend gate,SuperAdminOrOwnerGuard, on the invitation endpoint), and opens the exact same invite dialog as/platform/staff. - Detail tabs: Overview (contact info, skills/interests, departments), Activity (things this person did, not things done to them), Candidate application (only shown if they came through HR Pool — a read-only snapshot of their original application), Projects, Tasks.
- The "⋯" menu's "Send on leave"/"Return from leave" and "Revoke staff" actions call IAM/Users routes (
POST /admin/users/:id/leave,DELETE /admin/users/:id/platform-staff), not staffing ones — this page is a convenient front door into actions that live in Platform Administration → Users.
Projects (/team/projects)
In plain terms: internal project management — departments and people attached to a project, a stage roadmap, and a task board, plus (if you can see the platform's finances) that project's slice of the owner ledger.
- List: grid/list toggle, status/department filters. "New project" (
:manage). - Detail tabs: Overview (progress stats, per-stage progress, recent tasks, an inline-editable rich-text "Vision" field), Departments, Participants, Stages, Tasks, Activity, and Finances (see Finance → Project finance tab) — each covered in its own section below.
- A task's department must be one of the project's attached departments — enforced server-side, not just a UI constraint (see Departments → Projects for the reverse direction).
Departments (project's Departments tab)
Attach/detach a department to this project (:manage) — the reverse direction from Departments → Projects. A task's department can only be picked from the project's attached departments.
Participants (project's Participants tab)
Two views: Contractors — real assignments picked from the HR Pool, not the staff directory; and a read-only Departments roster of everyone in every attached department (via their department membership, not a direct project assignment).
Stages (project's Stages tab)
An ordered roadmap, reorder via ↑/↓ (:manage). Each task can be assigned to a stage; the Overview tab's per-stage progress derives from this.
Tasks (project's Tasks tab)
List or kanban board, 5 grouping modes — scoped to this project only. The cross-project equivalent covering every project at once is Tasks below.
Tasks (/team/tasks)
In plain terms: a cross-project task board — every task across every project in one place, unlike the per-project Tasks tab above.
- List/kanban toggle, 5 kanban groupings (status/responsible/priority/project/stage). Drag-and-drop is gated
:manage(dragging a card between "responsible" lanes calls the same assignee-add route the task form uses). - Task status configuration — no dedicated page, but a real settings dialog (gear icon,
platform:staffing:configureonly) covering the full status registry: create/edit/reorder/delete. The 5 seeded system statuses can't be hard-deleted, only deactivated. - Task detail page — every field is inline-editable, Jira-style. Status/stage/department/responsible/helpers/price need
:manage; description/priority/due date/progress are open to:manageor:manage_ownon your own task (creator, or assigned as RESPONSIBLE/HELPER — including via a linked HR Pool entry, not just a direct staff assignment). Comments follow the same:manage/:manage_ownsplit. Deleting a task is:manage-only. - Fixed, 2026-08-04: attaching a file to a comment or the description used to require blanket
:managewith no:manage_owncarve-out at all — a:manage_ownuser could write and send a comment, but the attach button 403'd every time, even though the UI showed it as available. The upload endpoint (POST /admin/staffing/attachments) is generic (also used by Project Vision) and doesn't know which entity a file is for by default; the fix threads an optionaltaskIdthrough the upload call so the server can apply the samemanage_own+ creator/assignee check the rest of the task already uses, instead of requiring blanket:managewhenever a task is actually identifiable.
HR Pool (/team/hr-pool)
In plain terms: the pipeline for people who want to work with P4P but aren't staff (yet, or ever) — a public application form feeds a review queue; approving someone lets you invite them onto a project as a contractor, all without necessarily making them platform staff.
- Public application (
/hr-pool/apply, no login, noplatform:staffing:*gate at all — Turnstile + rate-limited) — a marketing-style landing page with a form (name/email/phone/messenger/type/interest areas/skills/notes). Creates anHRPoolEntrywith no linkedUser. - Review queue (
/team/hr-pool, staff-only) — status stat tiles, filterable table. "⋯" per row: move through review states (new → in review → approved/deactivated,:manage), "Invite" (only onceAPPROVEDand not already linked to a user,:manage— sends a platform staff invitation), delete (only if the entry has no assignment history). - Entry detail — same actions, plus Edit (applicant type, interest areas, skills — the classification fields,
:manage) and an internal review-notes log (:manageto add a note; distinct from the applicant's own self-reported notes field). - Confirmed gap (2026-08-04): two backend-ready paths have no reachable frontend today —
HrPoolInvitationsController's public accept routes (GET/POST /hr-pool-invitations/:token[/accept]) have no matching page underapps/portal/pages/hr-pool-invitations/— unlike the structurally identical/invitations/[token]and/platform-staff-invitations/[token]pages, which do exist. The portal's public-route allowlist already anticipates this path (middleware/auth.global.ts), but the page itself was never built — an "Invite" sent from the HR Pool queue currently has nowhere real to land.- The self-serve "list me as an available resource" opt-in (
POST hr-pool/opt-in, open to any logged-in platform user, not just staff) has no frontend trigger anywhere in the portal — only the public application form and the internal review queue exist today.
Edit an entry / add a review note — field guide
The Edit dialog only covers how this applicant is classified — their name, email, phone, and preferred messenger were set once by the applicant themself and can't be changed here at all (they don't appear on the form). Unlike most other edit dialogs in this module, Save isn't disabled until you make a change — it's enabled the moment Applicant type and at least one Interest area are filled, so clicking it with nothing actually edited just re-saves the same values.
- Applicant type (required) — Individual / Agency / Company / Service provider.
- Interest areas (required, multi-select) — Consulting / Marketing / Development / Design / QA / AI / Events / Other. At least one is required; picking Other reveals a free-text box below it, but — despite appearing right under a required field — that box itself stays optional, up to 200 characters.
- Skills (optional) — free text, up to 500 characters.
- Review notes (separate panel below the entry's own info, not part of the Edit dialog) — a growable log, not a single field: each note is a plain text box, 1–4,000 characters, posted with its own Send button (disabled while empty) and permanently appended with your name and timestamp — notes can't be edited or deleted once sent. Visible only to staff with access to this page, never to the applicant.
Dashboard & Activity
/team/dashboard — real data throughout: a greeting, a "My payouts" banner (shown only when you have pending payout offers to accept/decline — see the forthcoming Finance module doc for the full payout workflow), six uniform section cards mirroring the sidebar (Tasks/Projects/Team/Departments/ HR Pool/Activity), recent activity, and quick-action links.
/team/activity — the unfiltered, cross-section feed (every event kind across tasks/projects/departments/HR pool entries/members), with actor/project/department/period filters. Live-updating.
Shared Documents (/team/shared-documents)
In plain terms: a shared file library for P4P staff — not a staffing feature at all under the hood.
Reuses the platform's generic file-storage capability (org:files:read/org:files:manage) against one fixed, synthetic organization (system-org-shared-documents) rather than anything staffing-specific. The page is still gated platform:staffing:read (so only P4P staff reach it at all), but the upload/delete/restore actions underneath are governed by org:files:*, checked against that synthetic org's own membership. Files/Trash tabs, 30-day soft-delete retention.
Testing this module
scripts/e2e/menu/staffing.mjs (12 checks) runs this module's core flows end to end and sets lastVerified above. Found and fixed along the way: the manage_own file-attachment 403 documented in Tasks above, and a missing system-org-shared-documents organization row (recreated + folded into packages/database/prisma/seed.ts so it survives future resets) that was returning a misleading "quota exceeded" error for every attachment/Shared Documents upload. Not covered by automation, still worth a manual pass — see docs/MANUAL_TESTING.md: department-leader-only access to a department's Finances/Roles tabs, the real public HR Pool application form (Turnstile-gated, so the automated suite seeds an entry via direct DB insert instead of submitting the form), the task-status configuration dialog, kanban drag-and-drop, and read/manage/manage_own permission-gating on each page's mutating controls.