Skip to content

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 not matching the usual /org/:slug/* pattern, it now genuinely is an organization's workspace underneath — P4P Internal's — not a Platform-tier feature: see Architectural tiers. Until 2026-08-18 this module was gated by Platform RBAC (platform:staffing:*); docs/staffing/ADR-002-org-rbac-migration.md moved it onto Organization RBAC, scoped to the P4P Internal organization — the same Role/ Permission/TenantGuard+PermissionsGuard mechanism every other organization uses, now under workspace:staffing:*. A PlatformStaffMembership row is still the separate, unrelated gate on top (an HR/roster fact — "is this person recognized P4P staff at all" — unchanged by the migration): you need both to see anything here, but they answer different questions.

All routes share middleware: 'staffing-access' + requiresPermission: 'workspace:staffing:read' (page-level gate; individual mutating actions require :manage, see below) and layout: 'team' — except /account/hr-pool (see Your HR Pool profile), a plain layout: 'account' page with no workspace:staffing:* gate at all, built for the one audience the rest of this module by definition excludes. 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.

Search… Ctrl K in the header (a magnifier on a phone), or Ctrl+K (⌘K on a Mac) anywhere in Team Management, opens the search window. Type at least 2 characters: it finds tasks, projects, people, departments and HR pool candidates by name (people also by email, candidates also by skills) — up to 5 of each, grouped. ↑ ↓ move, Enter opens, Esc closes. Before you type, it shows what you last opened from it. It searches names and titles only, not descriptions or comments, and shows only what you could already open from the lists; archived projects are left out.

Departments ​

Create a department — Staffing → Departments → New department → Name, pick a Leader → Create department. See field guide for what each field means.

Edit a department — open the department → the ⋯ menu (top right) → Edit department. Same fields as creating one.

Delete a department — open the department → ⋯ → Delete. Blocked while it still has active sub-departments — see Sub-departments. This only archives it.

Bring back a deleted department — Staffing → Departments list → Archive tab → the row's Restore button. Members, roles, and project attachments come back exactly as they were.

Create/edit a department — field guide ​
  • Name — required, 2–100 characters, shown everywhere the department appears (lists, breadcrumbs, cards).
  • Leader — required; search and pick a staff member. They get direct access to this department's own finance book just by being its leader — nothing else to set up.
  • Description — optional, up to 2,000 characters, shown on the department's Overview tab.
  • Parent department — optional; leave as "None" unless this is meant to be a sub-department. Nesting only goes one level deep, so a sub-department can't have sub-departments of its own.

Members ​

Add someone to a department — open the department → Members tab → Add member → pick staff members → Add (N). See field guide.

Describe what someone actually does in the department — department → Members tab → the row's ⋯ → Edit functions. See field guide.

Remove someone from a department — department → Members tab → the row's ⋯ → Remove from department.

Members tab — field guide ​
  • Add member — search and pick one or more staff members not already in this department → Add (N).
  • Role (read-only) — each member's actual Platform Role (e.g. "P4P Admin"). To change what someone can do, grant or revoke their Platform Role from Platform Administration → Users instead — this tab just displays it.
  • Functions — a free-text note on what someone actually does in this department (click a row's Functions cell, or ⋯ → Edit functions), separate from their Role above. Up to 2,000 characters; the table shows one line, click to expand and read the rest. The department's own leader can edit this even without broader management permissions.

Projects (department's Projects tab) ​

Attach a project to a department — department → Projects tab → Attach project → pick one → confirm. See also Projects → Participants for doing the same thing from the project's own side (there it's a bulk-add of the department's people).

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 (or the same option from the ⋯ menu) — it's the same form as creating any department, with Parent department already filled in. Not offered once a department already has a parent, since nesting only goes one level deep.

Employees ​

Adding someone here isn't a staffing action — it's the platform-staff invitation flow described in 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. This only affects their internal-staffing status, not their account — see Platform Administration → Users for suspending an account entirely.

Describe what someone does on a project — open their profile → the Projects tab → Set function / Edit function on a project row. This is the per-project sibling of the per-department Functions note above, and edits the same participant note the project's own Participants tab shows.

Projects ​

Create a project — Work → Projects → New project → fill it in → Create. See field guide.

Edit a project — either from the Work → 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.

Delete a project — Work → Projects list → the row's ⋯ → Delete. Permanently deletes everything in it — departments, stages, tasks, and participants.

Create/edit a project — field guide ​
  • Name — required, 1–100 characters.
  • Description — optional, up to 2,000 characters, shown in project lists and cards.
  • Department — optional; the team this project belongs to (its home in the org chart). Pick a live department or leave it as "No department". Shown on the project's Overview and used for grouping — distinct from the derived "also involved" departments (whichever teams the assigned participants happen to belong to), which the Overview shows separately. When you pick one while creating a project, a checkbox offers to seed the participant list with that department's current members (a one-time snapshot).
  • Status (edit only) — Planned / Active / Completed / Archived. Every new project starts as Planned.
  • Progress (edit only) — a 0–100 number for the project's progress bar. You set this by hand — it doesn't calculate itself from task or stage completion.
  • Owner — optional; search and pick a staff member, or leave it unset to default to you. The owner can attach participants to their own project even without broader project-management permissions — see Participants below.
  • Vision — optional, up to 2,000 characters, for why this project matters and what "done" looks like. Supports rich text (bold, lists, links) and lives on the project's Overview tab, where you can also edit it directly.
Project templates ​

A project template is a ready-made project the team starts from — its description, vision, stages in order and all its tasks (with labels and subtasks). People, dates, statuses, budget, comments and files are never part of it.

  • Make one: open a project → ⋯ (Overview tab) → Save as template, and give it a name. Every task is saved, finished ones too.
  • Change one: fix the project, then save it again under the same name — the dialog warns that the template will be replaced.
  • Use one: in New project, pick it under From template. The description and vision are filled in; the line under the picker says how many stages and tasks come with it. When you press Create, they are created, every task starting afresh.
  • Manage templates: on the Projects page, the templates button next to + (on a phone, ⋯ → Project templates) — see what each holds, rename or delete. People who can create projects manage them. Deleting a template never changes projects already made from it.

Project discussion ​

Write to a project — project → Discussion tab → type in the box at the bottom → Send (Enter sends, Shift+Enter starts a new line). Rich text is supported: bold, lists, links.

Edit or delete your own post — hover the post → the pencil or the bin. Only the author sees those, and a manager can't edit somebody else's words. An edited post is marked "edited"; a deleted one leaves a line in History so the thread doesn't silently change shape.

Read the event log instead — same tab → History. That is the automatic record (tasks created, people added, stages moved); the discussion is what people wrote. They used to be one stream and were split apart because reading them together was noise.

What belongs here

Anything about the project that is not about one task: the client moved a deadline, here is the brief, we decided to drop the second landing page. A question about a single task belongs in that task's own comments, where the people working on it will see it.

Participants ​

There is no separate "Departments" tab (removed 2026-08-30). Everyone on a project is an explicit participant; a department only shows up as a grouping if some of its people are on the project.

See who's on the project — project → Participants tab. A Staff / HR pool toggle at the top; for Staff, a further By department (a tree) / Flat list switch.

Add a whole department at once — project → Participants tab → + → Add a department's members → pick one → Add members. Every current member is added as a participant. The project's own Department (if set) comes pre-ticked. It's a one-time snapshot — someone who joins that department later won't appear on their own. (Same thing on offer as a checkbox in the create-project form when you set an owning department.)

Add one person — project → Participants tab → + → Add a participant → Staff or HR pool tab → pick a person → confirm. See field guide.

Remove one person — the row's ⋯ → Remove from project.

Remove several at once — tick the rows (or a department group's header checkbox to tick everyone in it) → Remove selected in the bar that appears → confirm. They stay in their departments; only their link to this project is removed.

Edit a participant's note — the row's ⋯ → Edit. You can also set the same "what they do" note from the other side: a person's employee card → Projects tab → Set function / Edit function on the project row (workspace:staffing:projects:manage). A project the person only has tasks in — never formally added as a participant — has no note to set.

Add a participant — field guide ​
  • Source (add only) — Staff for an active P4P staff member, or HR pool for an external contractor.
  • Person (add only) — required. The Staff list is active P4P staff with Team Management access (workspace:staffing:read) not already on the project; the HR pool list is Approved HR Pool entries only.
  • What they do on this project — one optional free-text note (role, tasks, area of responsibility). Not a "role" field — the person's real Team Management role is a separate thing and lives on their employee card.

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 2,000 characters.
  • Start date / Due date — both optional; the two watch each other, so you can't pick a start after the due date or vice versa.
  • Status (edit only) — Planned / In progress / Completed. Every new stage starts as Planned, and you set this by hand — the stage list separately flags it if the status and the tasks underneath disagree (e.g. marked complete while tasks are still open), but doesn't fix it for you.
  • Owner — optional, purely informational, same idea as a project's own Owner field.

Sprints (project's Sprints tab) ​

A sprint is a time box for part of a project's work — usually one or two weeks — with an optional goal. New sprint proposes the next number and the two weeks after the last sprint; a project's sprints can't share a day. Only one sprint runs at a time: Start sprint on a planned one (while another runs, the button says why it can't).

The running sprint shows its goal, dates, days left, how many tasks and hours are done, and its tasks as a small board (To do / In progress / Done). Below: the planned sprints, and the backlog — the project's tasks in no sprint that aren't done. Tick backlog tasks and Move to sprint to plan them. A task's sprint can also be set in its Details → Sprint (people who may change the task's due date may change this too). Complete sprint keeps the done tasks with it and asks where the unfinished ones go: to the next planned sprint, or back to the backlog. Only a planned sprint can be deleted; its tasks go back to the backlog. On the Tasks page, the Sprint column and filter show the same.

Timeline (project's Timeline tab) ​

The project's plan on a time scale, read-only. Each stage is a thin bar over its own dates, with its tasks under it (earliest first, subtasks indented), then the tasks without a stage. A task with a start and a due date is a bar coloured by its status; one with only a due date is a diamond; an unfinished task past its due date is outlined in red. Arrows run from a task to the one waiting for it (dependencies); a link icon means a dependency on a task in another project — point at it to see which. The red vertical line is today. Week / Month / Quarter changes the scale, and the choice is remembered. Click a name or a bar to open the task. Tasks with no dates are listed under the chart — give them a start or a due date (task page → Details, or Edit task) to place them. A task can't start after its due date.

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 — Work → Tasks (the cross-project board) → New task → title → Select a project → Select a member → Create task. The assignee list here is the whole company, not just the chosen project's current participants — if the person you pick (including yourself) isn't attached to that project yet, the dialog says so and asks you to confirm before it adds them alongside creating the task. See field guide.

Change a task's status/assignee/etc. — Status and Progress are editable straight from the list (the Status and Progress columns — status via a dropdown, progress via a slider clamped to the current status's band); Status can also be changed from the Edit task dialog — a row's ⋯ menu → Edit, on the per-project Tasks tab and the cross-project Tasks list alike (:manage only; the cross-project list loads that task's own project context on the fly to reuse the same dialog). Every other field is click-to-edit on the task page. See field guide.

Reconfigure the task status list — Work → Tasks → gear icon → Configure columns → add/rename/reorder/deactivate statuses. See field guide.

Delete a task — open the task → the ⋯ menu → Delete, or a row's ⋯ → Delete on the per-project Tasks tab or the cross-project Tasks list (:manage only).

Change many tasks at once — Work → Tasks in the List view → tick the rows (or the header box for the whole page) → the bar above the table offers Status, Priority, Due date, Labels and, with :manage, Responsible and Delete. Each asks what to set and applies it to every selected task. A task you may not change — or, for Responsible, one whose project the person is not on — is skipped, and the result lists which ones and why; the rest are changed. Selection covers the page on screen, up to 100 tasks. Not on phones yet.

Say which task waits for which — open the task → Dependencies (under the description) → + → choose This task waits for… or This task blocks… → find the other task by title (any project) → Add dependency. A task still waiting on unfinished tasks shows an hourglass (Waiting) on its page, in the task list and on board cards. You can still move it to In progress or Done — you are only warned. Only someone who may change the waiting task can add or remove what it waits for; a task can’t wait for itself or, through a chain, for a task that waits for it. The hourglass is not the Blocked status — that one you still set by hand.

Create/edit a task — field guide ​

Two entry points open this form, and they behave slightly differently:

  • "New task" from the cross-project board (and from an employee's own card) — its own dialog, with a Project field, creation only, no editing. Assignee/Helper candidates are the whole company; picking someone not yet on the project asks for confirmation and adds them there as part of creating the task.
  • The dialog inside a project (the project's own Tasks tab — the same dialog handles both creating and editing an existing task there) — the project is already known, so that field is hidden. Assignee/Helper candidates are only people already attached to that project: staff from its attached departments, staff attached individually, and HR Pool contractors already attached to that specific project.

Every other field is the same in both. Status is the only one offered on edit only — a new task always starts at the default status. Making a task a subtask works differently too — see the note at the end.

  • Title — required, 1–150 characters.
  • Project — required (cross-project dialog only); every other field on this form (Stage, Department, and the Assignee/Helper candidate list) depends on which project is chosen and resets whenever you change it.
  • Priority — Urgent / High / Medium / Low. Defaults to Medium.
  • Stage — optional; appears once a project is picked, offering that project's own stages.
  • Department — optional; must be a department the project has people from.
  • Description — optional, up to 2,000 characters.
  • Assignee — required, the person responsible for the task, and this field is never truly left empty:
    • at creation it defaults to whoever is creating the task, as long as they're themselves eligible;
    • on edit, if no one is assigned yet, it defaults to the task's author — again only if they're eligible; the field can't be cleared to blank, only changed to someone else.
    • The one case it stays blank is when neither the creator nor the author is eligible for this project at all (e.g. an old task with no recorded author) — then you pick manually, and Save stays disabled until you do.
  • Time estimate, h — optional; planned effort in hours, a decimal number (e.g. 1.5 = 1 hour 30 minutes). Purely informational, doesn't drive anything else — see actual time spent under Time tracking below.
  • Due date — optional.
  • Repeats — optional; see Repeating tasks below. Not offered for a subtask.
  • Helpers — optional; any number of additional people in a supporting role, from the same candidate list as Assignee.
  • Status (edit only) — any status from the dictionary (the same ones that are the kanban board columns). Changing it re-derives the task's progress from the new status's category. Status is also editable inline in the list — the dropdown in the Status column, wherever you may write that task.
  • Cost / Currency — a plain number for internal budget bookkeeping — it's a note, not a real ledger entry (compare Task payouts, which is).
  • Making a task a subtask — a separate action from everything above: the task's own ⋯ menu → Add subtask, done once at creation and not reassignable afterward. Subtasks only go one level deep — a subtask can't itself have subtasks.
Automations ​

Automations (Tasks page → ⋯ → Automations, for people who can configure statuses and labels) are rules that change tasks for you: When something happens to a task — it is created, its status becomes a chosen one, or a responsible person is assigned — if it matches the conditions you chose (projects, labels, priorities; leave them empty for any task), then the actions run in order: assign a responsible person, add a label, set the priority, change the status, add a watcher, or create a task from a template in the same project.

A rule runs right after the change, in the name of whoever last saved it, and the task's history says Automation «…» next to what it did. Changes made by a rule never start other rules. Each rule shows how often it ran, when last, and whether the last run failed; Runs lists the latest runs with the reason for any failure (for example, the person is no longer on the project). The switch turns a rule off without deleting it.

Task templates ​

A template is a ready-made task the whole team can start from — for example "Weekly report" or "Onboarding a new colleague". It holds the task's title, description, priority, time estimate, labels and a list of subtasks. It never holds people or dates: those are chosen for each task.

  • Use one: in New task (on the board or inside a project) pick it under From template. The title, description, priority and estimate are filled in — change anything you like. The line under the picker says what else the task gets: when you press Create, its labels are added and its subtasks are created. A subtask can't be made from a template.
  • Save a task as a template: open the task → ⋯ → Save as template, and give it a name. Its title, description, priority, estimate, labels and subtask titles are saved.
  • Manage templates: on the Tasks page, ⋯ → Task templates — create, edit or delete them. Anyone who can see tasks can use templates; people who can create tasks can manage them. Two templates can't have the same name. Changing or deleting a template never changes tasks already made from it.
Custom task fields ​

Custom fields are fields a task does not have by default, such as "Client", "Story points" or "Approved by client". They are shared by the whole team. There are five kinds: text, number, date, dropdown (one choice) and yes/no.

  • Set up fields: on the tasks page, ⋯ → Task fields. Add, edit or delete a field and change the order with the arrows. For each field choose all projects or only some projects. A field's kind cannot change once it is created. Whoever configures the team can set up fields.
  • Fill in a field: on the task, Details → the Fields section, click the pencil next to the field. Whoever can change the task can fill in its fields. Every change shows in the task's History.
  • Table and filter: every field has its own column in the tasks table (new columns start hidden — turn them on with the columns button). Dropdown and yes/no fields can be picked in Filters; that filter is kept in saved filters.
  • What gets cleared: removing a dropdown option clears it from the tasks that chose it. Taking a project away from a field clears that field's values on the project's tasks. Deleting a field clears all its values — the dialog says first how many tasks have one.
Repeating tasks ​

Repeats makes a task come back on a schedule: every day, every week on the days you pick, every two weeks, or every month on the same date — with an optional Until day. Set it when creating the task, in Edit task, or on the task page → Details → Repeats. Whoever may change the task may change its repeat.

On each repeat day, early in the morning, a new copy of the task appears — whether or not the previous one is finished. It has the same title, description, priority, stage, department, estimate, assignee, helpers and labels as the newest copy, so a change you make to the latest copy carries forward. Comments, time, cost, files and subtasks are not copied. The copy's dates move with it: a task from Monday to Wednesday repeated weekly becomes Monday to Wednesday of the next week; a task with only a due date gets the repeat day as its due date. The first copy comes after the task's own start or due date.

A repeating task shows a ⟲ icon on the board and in the tables — point at it to see the rule. Under Repeats in Details you see the day of the next copy. Changing the repeat affects only the copies still to come. Choosing Does not repeat stops it; the copies already made stay as ordinary tasks. A subtask can't repeat — make its parent task repeat instead.

Time tracking on a task ​

On the task's own page, in the Details panel, right under Time estimate — a Time spent field: the sum of every hour logged against the task. The + button next to it only shows for whoever is themselves the assignee or a helper on that task — it opens a small "hours + date" form and adds an entry to the running total. Individual entries aren't listed anywhere in the UI yet — only the total — viewing, editing, or deleting one specific entry is API-only for now.

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. The 5 built-in statuses (To Do, In Progress, In Review, Blocked, Done) can't be renamed; custom ones can, in any language.
  • Color — one of 6 swatches, purely visual.
  • Category — TODO / In progress / Done. This one isn't cosmetic — it drives a task's Progress %. Every non-Done status gets an equal band of the 0–100 range in status order; moving a task into any Done-category status pins its progress straight to 100.
  • Reorder (↑/↓ arrows) — changes both the kanban column order and the Category progress bands above, since both follow this same order.
  • Active toggle (built-in statuses only) — the reversible alternative to deleting one: switching it off hides it everywhere but keeps its history. The default status (what new tasks start in — currently To Do) can't be switched off until you make another one default. If a status still has tasks on it, turning it off asks where to move them first.
  • Delete (custom statuses only) — disabled if it's the only one left, and asks where to move its tasks first if it has any.
  • New status name / color / category (bottom row) — adds a new custom status at the end. The Add button stays disabled until you type a name.

Dashboard & Activity ​

Dashboard (sidebar: Dashboard) — read-only, nothing to configure. Its "My payouts" banner is covered in Finance.

Activity (sidebar: Activity) — also read-only, with its own filters above the list: type, action, actor, project, department, and period. Action groups similar events together (Created, Updated, Deleted, and so on) rather than listing every specific event kind separately.

HR Pool ​

Move an HR Pool applicant through review — Staffing → HR Pool → the row's ⋯ menu → Take in review, then Approve, then Invite to platform (sends a platform staff invitation). A fresh Submitted entry can also be Approved directly, skipping Take in review.

Edit an entry's classification — open the entry → ⋯ menu → Edit. See field guide.

Add a review note — open the entry → Review notes → write one. Separate from the applicant's own self-reported notes, and visible to staff only. See field guide.

Delete an HR Pool entry — the row's ⋯ menu → Delete. Only possible for an entry with no project or task history.

Account HR Pool ​

Check your own projects and tasks — My Account → HR Pool Profile (shown only if you have a linked HR Pool entry). There's nothing to do on this page — it just shows what's already been decided elsewhere, and updates on its own as staff attach you to a project or task.

Notifications ​

What this module tells people about, and who gets told. The rule everywhere is the same: you hear about it only if it concerns you, and never about your own actions. Full explanation in Notifications.

Tasks ​

Assigned to a task — the assignee is told, in the bell and by email. Same when they are removed from one, or when a task they work on is deleted (group My tasks).

A comment is added — everyone with a stake in the task: its assignees and its creator, minus whoever wrote it (group Comments). Several comments on one task arrive as a single entry with a count, not one per message.

The status changes — same circle, but quieter: no email unless you ask for one (group Status of my tasks). Editing a description, a due date or a priority tells nobody — on an active task those are constant, and notifying on them is how a bell stops being worth opening.

A payout is offered or answered — see Payouts.

Many tasks changed at once — a bulk change from the task list sends one notification per person ("the status of your tasks changed to Done — tasks: 12"), not one per task. Someone touched by only one of the tasks gets that task's usual notification instead.

A task you follow can start — when the last unfinished task it was waiting for is done, its watchers are told (group Status of my tasks).

Project discussion ​

Someone posts in a project's discussion — the project's participants and its owner, minus whoever wrote it (group Comments). Several posts in one project arrive as a single entry with a count, the same way a task's comments do. The notification carries the project's name and never the text of the post — a bell is read over people's shoulders.

Project participants ​

Added to or removed from a project — the person themselves, in the bell and by email (group Projects and departments). Nobody else on the project is notified; the change is visible in the Activity feed for anyone who wants it.

Works the same whether the participant was attached directly or through their HR pool entry — a pool entry with a linked account is a real person with a real bell.

Department members ​

Added to or removed from a department — the member themselves (group Projects and departments). Where someone belongs decides what they are expected to work on, so being moved without being told is how people end up accountable for things they never saw.

HR pool ​

A new application arrives — everyone who can process applications (workspace:staffing:hr_pool:manage), whether it came from the public form or someone opted themselves in (group Administration). This is the one notification whose audience is a permission rather than a relationship to the record — an application nobody is told about waits until someone thinks to go looking.

Somebody who opts themselves in is not notified about their own application, even if they also hold the permission.

Account HR Pool ​

This page itself notifies nobody. Being attached to a project or a task tells you through the bell — see Project participants and Tasks — this page just also happens to update live the moment that notification arrives, instead of waiting for a reload.

Dashboard & Activity ​

Neither Dashboard nor Activity notifies anyone. Both are the pull half of a pair: you open one of them and see everything that happened in the team. The bell is the other, push half — it reaches you on its own, but only with what concerns you.

That split is the whole reason the bell is worth opening. If it also carried every task anyone created, it would become something you scroll past. When you want the full picture, you come to Dashboard or Activity; when something needs you specifically, it comes to you via the bell.

Which of the things happening there reach you personally is decided by your notification settings.

Departments ​

This page itself notifies nobody. Adding or removing someone happens on a department's Members tab and tells that person — see Department members.

Projects ​

This page itself notifies nobody. Attaching or detaching someone happens on a project's Participants tab and tells that person — see Project participants.

The permission model in one table ​

Split 2026-08-14 from a single flat platform:staffing:manage into one permission per domain, so a role could, for example, delegate "manage tasks" without also handing out "delete departments." Everywhere below, the shorthand `:manage` means that section's own domain permission — departments:manage in Departments, projects:manage in Projects, tasks:manage in Tasks, hr_pool:manage in HR Pool — not one shared key:

PermissionGrants
workspace:staffing:readView everything — every list/detail page's baseline gate
workspace:staffing:departments:manageCreate/edit/delete a department, its avatar, and add/remove any member
workspace:staffing:members:add_ownAdd a member to your own department only (Department.leaderId) — the one department action the leader carve-out extends to; see Departments → Members
workspace:staffing:projects:manageCreate/edit/delete a project, its cover, department links, stages, and assignments
workspace:staffing:assignments:add_ownAdd a participant to your own project only (Project.ownerId) — see Projects → Participants
workspace:staffing:tasks:manageCreate/delete a task and assign/unassign any task's assignees; manage any task regardless of who created it
workspace:staffing:tasks:manage_ownOn 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 TaskAccessService.resolveTaskWriteAccess() against the specific task
workspace:staffing:hr_pool:manageReview/update/delete HR pool entries, invite to convert, add notes
workspace:staffing:configureThe task-status registry only (colors/order/system-status activation) — a global, shared setting, deliberately split from day-to-day :manage
workspace:staffing:restoreRestore archived departments, projects, and other soft-deleted staffing records

Both _own permissions follow the same shape: the permission itself isn't row-scoped, it's the service-layer check (Department.leaderId/Project.ownerId against the actor) that narrows it to "your own" — a real, revocable, UI-grantable permission rather than a hardcoded bypass, same convention docs/iam/PERMISSIONS_CATALOG.md uses for tasks:manage_own. Holding the blanket :manage permission already covers everything the _own variant does — _own exists for someone who should be able to grow their own department/project without also being handed the platform- wide version.

A few routes intentionally carry no @RequirePlatformPermission decorator at all — department finance-book entries use a data-driven "is this actor the department's leader" raw service-level check instead, and editing a member's function (title + description) uses the dedicated DepartmentLeaderOrStaffingManageGuard for the same reason: 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). That guard briefly disappeared 2026-08-14 along with the whole DepartmentRole system it also used to back (department role-management routes), then came back the next day scoped down to just this field once it was kept as its own thing (see Members above).

Departments (/team/departments) ​

In plain terms: the org chart. A department has a leader, members (each with their real platform role and an optional per-department function), attached projects, and — if the leader chooses to keep one — its own finance book.

  • List/tree view on desktop; a card grid on mobile (no toggle). "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, Sub-departments — each covered in its own section below.
  • The shell's "⋯" menu also offers New sub-department (2026-08-14) — the same form as the Sub-departments tab's own button, with this department pre-selected and locked as parent (that field stays editable from a plain "Add department" or in edit mode — it's only locked when the parent was implied by how the dialog was opened). Hidden for a department that already has a parent — nesting is capped at 1 level.
  • Deleting a department (:manage) is blocked server-side while it still has active sub-departments — the confirmation toast now shows that reason specifically rather than a generic error (2026-08-14 fix; it silently showed the generic one before, regardless of the actual cause).
  • Archive/restore (workspace:staffing:restore, 2026-08-13) — deleting a department only soft-deletes it (members, roles, and project attachments are untouched and reappear as-is on restore); the list page gains Active/Archive tabs, same pattern as Shared Documents's own Active/Trash split, but only for someone who actually holds this permission — browsing what is archived is not useful without also being able to act on it. Deliberately narrower than :manage and excluded from P4P Admin's default grant: undoing someone else's delete without their knowledge is closer to "who watches the watchers" territory than day-to-day department work. Restoring notifies every workspace:staffing:read holder.

Members (department's Members tab) ​

Add (:manage, or the department's own leader via members:add_own — added 2026-08-14), remove (:manage only — no leader carve-out here, adding and removing are deliberately asymmetric), edit a member's function (leader or :manage, via DepartmentLeaderOrStaffingManageGuard — no permission string, same reasoning and same guard as before 2026-08-14, reinstated the next day for this narrower field once DepartmentRole itself was removed).

Each member's "Role" column is their real PlatformRole(s) — read-only here, sourced the same way Platform Administration → Users shows them. Removed 2026-08-14: this used to be DepartmentRole, a per-department job-title catalog (leader or :manage could create/edit/assign it, e.g. "Recruiter") that carried no actual permissions — indistinguishable from a real role in the UI while doing nothing structurally, which the user found actively misleading rather than merely decorative.

The member's Functions column is a different thing that survived that removal — kept explicitly at the user's request, since a per-person "what do they actually do here" note is a genuinely legitimate need distinct from a role label, and was never itself the problem. It moved from DepartmentRole (shared by everyone holding that role) to DepartmentMember directly (one person, one department, one note) — reachable via PATCH .../members/:userId, the same route/guard DepartmentRole assignment used to share, now narrowed to only this field. Originally (2026-08-15) a single free-text field (duties); split the same day into DepartmentMember.functionTitle (short, shown in the column and truncated on the employee's own card) and DepartmentMember.functionDescription (longer, shown on row-expand here and in a hover tooltip on the employee's own card) — mirroring the old DepartmentRole's own name + duties shape, but with neither field carrying any permission.

Projects (department's Projects tab) ​

The projects this department has people on. "Attach project" bulk-adds this department's current members to the picked project (:manage) — the same one-time snapshot as the project side (see Projects → Participants); "detach" removes them all again.

Per project the table shows: status, how many of this department's own people are involved, its own Tasks breakdown (todo / in progress / done) and Budget (sum of this department's task prices, per currency — only with workspace:finance:read), involved departments, timeline, progress. This per-project cut moved here 2026-08-30 when the project's own Departments tab was removed. Full department finances still live on the Finances tab.

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.

Org chart (/team/org-chart) ​

In plain terms: the departments drawn as a tree — the company at the top, its departments below, their sub-departments under them.

  • Everyone with workspace:staffing:read sees it. It is view only — change the structure in the department forms.
  • Each box shows the department, its leader and how many people it has. People opens the members with their job titles; the second button folds the sub-departments. Expand all / Collapse all do it for every box.
  • Not in a department lists the active staff who belong to no department.
  • A person's name opens their employee card.
  • Archived departments and people who left are not shown. On a phone the chart is an indented list.

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.

Searching, filtering, sorting and paging the roster happen on the server; the department filter lists the departments that still have someone under the search and the status filter, with how many.

  • List: search + department/status filters. "Add member" is gated by platform:users:invite (the same permission — and the same bulk invite dialog, one email-per-row now — as /platform/staff; delegated 2026-08-13, see Platform Administration → Staff invitations, was hardcoded SUPER_ADMIN/P4P Owner before that).
  • Detail tabs: Overview (contact info, skills/interests), Departments (every department this person belongs to, with their Function in each — added 2026-08-15, split out of Overview's own Departments card once Function grew a real description worth showing in full), 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.
  • Overview tab specifics (2026-08-15, closing three gaps the user found): a role with PlatformRole.description set renders with a dotted underline and shows that description in a hover tooltip, everywhere a role name is shown (this page, the roster, a department's Members tab). Skills/specialty is self-reported (User.skills/interestAreas) with no admin override — editing someone else's card shows a plain hint that it's self-reported; on your own card, a hover-revealed pencil icon (same convention as a task comment's own edit/delete, Card gets class="group" + opacity-0 group-hover:opacity-100) links to /account/profile for both this card and the Personal info card next to it — an admin viewing someone else's card never sees that icon.
  • Departments tab: each entry shows the person's function title right under the department name, and — unlike the department's own Members tab (title in the column, description only on row-expand) or Overview's old compact card (description only in a hover tooltip) — the description renders directly underneath, no interaction needed. A dedicated tab has the room; the tradeoff that motivated hiding it elsewhere doesn't apply here. See Members above for what a function actually is. Same email-login-links-to- own-account-when-it's-your-own-row exception as the roster (PersonLink.vue) also applies to this page's own header, added the same day: the login under the person's name links to /platform/users/:id normally, but to /account/profile — and always, regardless of platform:users:read — when it's your own card.
  • Projects tab: the What they do column (grid or list view) is the person's participant note on that project. With workspace:staffing:projects:manage each row carries a Set function / Edit function control — the same add/edit affordance the Departments tab has for the per-department function (2026-09-02), writing the same ProjectAssignment.note the project's own Participants tab edits. A project the person is tied to only through tasks (no participant row) shows a plain "—" and no control — there's nothing to attach the note to.

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.

Searching, filtering and paging the projects list happen on the server, newest first; the department filter lists the departments taking part in a project the search and the status leave, with how many.

  • List: grid/list toggle, status/department filters. "New project" (:manage).
  • Detail tabs: Overview (progress stats, per-stage progress, recent tasks, involved departments, an inline-editable rich-text "Vision" field), Participants, Stages, Tasks, Discussion (the project's own thread plus its event log), and Finances (see Finance → Project finance tab) — each covered in its own section below.
  • A task's department must be one the project actually has people from (validated server-side) — see Participants.

Participants (project's Participants tab) ​

ProjectDepartment (a live department↔project link) was removed 2026-08-30 (docs/staffing/ADR-001-staffing-pm-tool.md Decision 1). A project's participants are only explicit ProjectAssignment rows now; a department is shown as a grouping iff ≥1 of its members is assigned. The dedicated "Departments" tab is gone — everything lives here.

  • Top toggle: Staff (STAFF-sourced rows) / HR pool (HR_POOL).
  • Staff can be shown by department (a tree — one table, a folder row per department + a "No department" group, collapsible) or as a flat list. Each row carries a free-text What they do note. The Account column (pool-only / has a login / ex-staff) shows only in HR pool — in Staff it would say the same thing on every row.
  • Attach a department (POST .../projects/:id/departments/:deptId, :manage) — a one-time bulk-add of that department's current members; each added person gets a "you were added" notification. No live link.
  • Detach a department (DELETE ...) — removes every current member of that department from the project in one call.
  • Add / remove one person — POST / DELETE .../assignments (:manage, or the project's own owner via assignments:add_own for adds). Bulk-remove ticks rows and deletes each assignment.

Discussion (project's Discussion tab) ​

The project's own thread — ProjectComment, the same shape as a task's TaskComment one level up (docs/staffing/ADR-001-staffing-pm-tool.md Decision 23). Two views in one tab: Discussion (what people wrote) and History (the TeamEvent log for this project).

  • Reading is workspace:staffing:read, like everything else on the card.
  • Posting needs workspace:staffing:projects:manage, or a personal stake in this project: being one of its participants, or its owner. Without that the composer is replaced by a line saying who may write — never silently missing.
  • Editing and deleting are author-only, enforced server-side. Neither a project manager nor an admin can rewrite someone else's post.
  • Posts are plain rich text (bold, lists, links). No file attachments yet — a task's comments have them because a task owns an upload scope; a project doesn't (yet).
  • Every post, edit and delete is written to the audit log and the activity feed (project.comment_created / _updated / _deleted); a deleted post leaves a one-line preview in history, since TeamEvent.payload is denormalized on purpose.

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. Creating, editing, or deleting a stage notifies every project participant (2026-08-13, project_stage.created/updated/deleted — Project activity group).

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.

In the table view, searching, filtering, sorting and paging happen on the server; the tiles count what the filters leave, and the filter lists show how many tasks each option has. The board comes from the server too: each column shows its real number of tasks and the first 20 cards, with Show more for the rest.

  • 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).
  • Old finished work is hidden by default — tasks completed more than 14 days ago are not loaded, in both the list and the kanban (Jira's "hide completed issues older than"). A line under the stat tiles says so, with Show all / Hide older than 14 days to switch (remembered for a week). "Completed" is when the task last entered a Done-category status (Task.completedAt) — editing an old finished task does not bring it back. A subtask follows its parent, so a card's "2 of 5" count stays true. The stat tiles count only what is loaded.
  • Labels — the Labels field in a task's Details. Pick existing labels or type a new name and choose Create (anyone who can change the task may). Labels show on board cards and table rows, and the board's filters include Labels. Renaming, recolouring and deleting a label — which changes every task that has it — is in the board's ⋯ → Labels, for people with workspace:staffing:configure.
  • Timer — on a task you are assigned to, ▶ next to Time spent starts a timer. It shows in the header on every page (on a phone as a timer icon) and survives a reload or another device. ■ stops it and records the time as an ordinary entry on the day it started; under a minute records nothing. One timer per person: starting another stops and records the running one. A timer left on over 12 hours asks you to confirm or correct the hours. The header timer's menu can also discard it without recording.
  • Export to CSV — ⋯ → Export to CSV saves the tasks the board is showing, with its filters and search, as a spreadsheet file. Prices and payouts are never in it. The time report has its own Export to CSV: every logged entry for the report's dates and project — date, person, task, minutes, hours, note — for a timesheet or an invoice.
  • Saved filters — at the top of the board's filter menu. Choose filters, type a name and press Save; click a saved filter later to apply it again. They are yours only and follow you to any device. Saving under a name you already use replaces that filter.
  • @mentions — type @ in a comment, a task description or a project discussion post and pick a person from the list (search by name or email). They are notified — only the first time a text mentions them, so editing it does not notify them again — and start watching the task. Only people who can see Team Management are offered.
  • Watching — the Watch / Watching button in a task's header decides whether you are notified about its comments and changes. You start watching automatically when you create the task, are assigned to it, or comment on it. Stopping sticks: commenting or being assigned later does not subscribe you again — only pressing Watch does. "You were assigned / removed" notifications still reach you either way.
  • Live updates — another person's change re-reads only the task it touched, not the whole board; a full reload happens only after a reconnect or a very large burst of changes.
  • Task status configuration — no dedicated page, but a real settings dialog (gear icon, workspace:staffing:configure only) 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. Title/stage/department/responsible/helpers/price need :manage; status/description/priority/due date/progress are open to :manage or:manage_own on your own task (creator, or assigned as RESPONSIBLE/HELPER — including via a linked HR Pool entry, not just a direct staff assignment). Status and progress, on the same :manage/:manage_own split, are also editable straight from the task list (a dropdown + a slider in those columns), not only here; the kanban board's own drag-between-status is a separate, still :manage-only path (dragging a card calls the same route this select does, but the board itself stays gated tighter). Writing a new comment follows the same :manage/:manage_own split (the composer is hidden, replaced by a note about who may write, otherwise); editing or deleting a comment you already wrote is gated purely on being its author client-side, deliberately with no separate :manage/ canWriteOwn re-check (the actual enforcement is server-side, TaskAccessService.resolveTaskWriteAccess, same as everywhere else on this page). Deleting a task is :manage-only — and so is the list row's ⋯ menu (Edit / Delete) on both the per-project Tasks tab and this cross-project list; a :manage_own user edits from the task page instead. The cross-project list has no project context loaded, so Edit there fetches that task's own stages + departments on the fly and hands off to the same TaskFormDialog the per-project tab uses.
  • Fixed, 2026-08-04: attaching a file to a comment or the description used to require blanket :manage with no :manage_own carve-out at all — a :manage_own user 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 optional taskId through the upload call so the server can apply the same manage_own + creator/assignee check the rest of the task already uses, instead of requiring blanket :manage whenever a task is actually identifiable.

Time off (/team/time-off) ​

Staffing → Time off. Press Request time off, choose the type (vacation, sick leave, other), the first and last day and, if you like, a note. The dialog shows how many working days that is.

  • Sick leave counts at once — no approval; your department leader and managers are told.
  • Vacation and other wait for a decision by the leader of any department you belong to, or by someone with workspace:staffing:time_off:manage (Team Manager, Team Admin). Nobody decides their own request. You are notified of the decision; a rejection shows its reason on your request.
  • You can cancel a pending request, or an approved one that has not started yet (the people who approve are told).
  • To approve (only for those who decide) lists waiting requests and who else is away over the same days.
  • Who is away shows everyone's approved time off from today on — dates only. The type and the note are seen only by the person and by those who decide.
  • Approved days are taken off that person's norm on the workload page, show in the team calendar as "away", and count as busy when someone schedules a meeting.

Workload (/team/reports/workload) ​

Work → Workload shows who is overloaded and who has room. Rows are people, columns are this week and the next five, plus No due date. Each cell is "planned hours / norm":

  • Norm is the person's weekly norm (set in the time report's norm settings). This week counts only the working days left, from today. Days of staff leave and approved time off have no norm — a week with none left says On leave.
  • Planned hours are what is left of each open task — its estimate minus the time already logged — shared equally between its assignees and spread over the working days until its due date. Overdue work counts in this week.
  • A task without an estimate adds no hours; it is counted under the hours instead ("+2 not estimated"). Give tasks an estimate to make the chart accurate.
  • Green is under 80% of the norm, amber 80–100%, red over the norm. Click a cell to see its tasks and open one.

The filter narrows it to one department. Visible with workspace:staffing:reports:read (the same people who see the time report).

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, no workspace:staffing:* gate at all — Turnstile + rate-limited) — a marketing-style landing page with a form (name/email/phone/messenger/type/interest areas/skills, a rich-text Additional information field, and an optional resume — one PDF / DOC / DOCX / TXT file, up to 5 MB). Creates an HRPoolEntry with no linked User.
  • Review queue (/team/hr-pool, staff-only) — status stat tiles, filterable table. Searching, filtering, sorting and paging happen on the server, so a large pool stays fast; the status tiles count what the search and the other filters leave. Interest areas and skills are not sortable, and type and source sort by their code. "⋯" per row: move through review states, :manage — not a strict linear pipeline: "Take in review" only offers from SUBMITTED, but "Approve" offers from either SUBMITTED orUNDER_REVIEW (a fresh submission can be approved directly, skipping review entirely — see the how-to above), and "Deactivate" offers from any status except already INACTIVE. "Invite" (only once APPROVED and 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 (:manage to add a note; distinct from the applicant's own self-reported notes field).
  • Confirmed gap (2026-08-04), partially closed (2026-08-16): two backend-ready paths had no reachable frontend —
    1. HrPoolInvitationsController's public accept routes had no matching page — fixed: /hr-pool-invitations/:token now exists, mirroring /platform-staff-invitations/:token. An "Invite" sent from the HR Pool queue (or the entry detail page's own Invite to platform button, promoted out of the "⋯" menu the same day) has somewhere real to land.
    2. 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) still 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.

Your HR Pool profile (/account/hr-pool) ​

In plain terms: the one place a contractor — someone with a linked HR Pool entry but no platform staff role — can see anything about their own engagement at all. The rest of /team is closed to them entirely, since every page there requires workspace:staffing:read, which by definition they don't hold.

  • No workspace:staffing:* permission gate at all — just being signed in with a linked entry is enough (GET /hr-pool/me and friends check identity, nothing else). Reached from a tile on the workspace landing page and an item in the account sidebar, both shown only when GET /me reports a linked entry (hrPoolEntryId) — and hidden again once you're also platform staff, since the real /team/hr-pool/:id already covers everything this page shows.
  • Your entry — status, areas of interest, skills. Read-only; there's no edit form here yet.
  • Projects — every project you're attached to, with your role on each.
  • Tasks — every task assigned to you, with its status and due date.
  • Live-updating (2026-08-16): getting attached to a project or a task shows up here without a reload — subscribed to your own personal channel (userRoom, identity-gated, the same one the bell itself uses), refreshing on the very notification.created signal that also ticks the bell. See Project participants.

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 Finance for the full payout workflow), up to eight uniform section cards — Tasks/Projects/Team/Departments/HR Pool/Calendar/ Finance/Shared Documents, each shown only if you hold that card's own permission (the first five need workspace:staffing:read; Calendar needs workspace:calendar:read; Finance needs workspace:finance:read — a narrower gate than the rest, same as its sidebar entry; Shared Documents needs files:read — see IAM, it isn't really a Staffing permission either) — recent activity, and quick-action links (each also individually permission-gated; "New employee" additionally needs to be a super admin or the organization's owner). There is no Activity card — the "Recent activity" panel below the section cards already shows the same feed, so a card restating the same count would say nothing the panel doesn't.

/team/activity — the unfiltered, cross-section feed (every event kind across tasks/projects/departments/HR pool entries/members/calendar events), with type/action/actor/project/department/period filters. Live-updating.

Shared Documents used to have its own section here — moved to IAM → Shared Documents (2026-08-20): it was never really a Staffing feature under the hood, just routed under Team Management's URL space, so it's documented alongside the rest of the file-storage capability instead. The page itself briefly moved out to /org/p4p-internal/shared-documents over the same period, then back to /team/shared-documents (2026-08-23) once that turned out to cost more in findability than the routing purity was worth — see the IAM section for the full reasoning. It's one click away in this module's own sidebar (layouts/team.vue, gated files:read, not workspace:staffing:read) and from /team/dashboard's own section card and quick action above.

Testing this module ​

scripts/e2e/menu/staffing.mjs (68 checks, rerunnable anytime — see scripts/e2e/README.md for the full per-check breakdown) runs this module's core flows end to end: Dashboard (section cards/quick actions for a full-access super admin — the narrower-access case is a known gap, needs a custom platform role), the full Departments family (create/edit/delete, a department's own Roles and Sub-departments tabs, its Activity tab, and both directions of the Departments↔Projects attach/detach), the full Projects family (create/edit/delete, the Participants tab's Staff/HR-pool groups, Stages create/reorder/delete, the Tasks tab's List view, and the Overview tab's Vision field), a cross-project task creation flow plus the cross-project Tasks page's own List view (search, the project filter, and reset), the task detail page's own core inline-edit fields (Title, Priority — one representative field from each of the :manage/canWriteOwn permission tiers), its Subtasks card (add a real subtask — exercising parentTaskId, never live-tested before — change its status inline, delete it), its Comments (send, edit, delete — a real comment, through the same rich-text composer used elsewhere on this page; overlaps with the deeper, standalone task-comment-edit-delete.mjs — see scripts/e2e/README.md for the accepted, documented redundancy), and its History tab (switches to it and confirms real events already render, from the Title/Priority edits just above), and HR Pool (seeded via direct DB insert — the public application form itself is Turnstile-gated, not automatable, see the note below — moved through review states: take in review, approve, add a review note on the entry's own page, invite to platform, delete). The public application form's own logic (everything except the Turnstile widget and the actual live submission) is unit-tested instead, apps/portal/pages/hr-pool/apply.test.ts (6 tests, 2026-08-12): the honeypot silently pretending success without ever calling the API, the full submit payload (interestAreaOther included only when OTHER is among the selected interest areas; phone/preferredMessenger sent as undefined rather than '' when left blank, so the backend doesn't store an empty string), a rate-limited response showing the cooldown message and starting the shared resend-cooldown timer, and a generic API error surfacing the server's own message. NuxtTurnstile is a local stub exposing a reset() spy, confirming handleFormSubmit's own turnstileRef.value?.reset() fires after every submit attempt (success or failure — a Turnstile token is single-use), and Employees (/team/members, previously entirely uncovered live — round 1 covered the list page/shell/Overview: the roster search finds a real staff member and the employee card loads real data; round 2 added the Tasks tab showing the real task assigned to this member earlier in the same run; round 3 added Schedule and Activity both loading without erroring; round 4 closes out unit coverage for the Candidate application tab, with no new live check — see the note below for why). The Projects and Candidate application tabs stay live-untested, along with "Send on leave" and "Revoke staff" — see the same note. Board layout and drag-and-drop on the Tasks page are covered separately, live, by realtime-board-and-task.mjs, not duplicated here. This closes out live coverage for all 4 blocks of the task detail page's round split — the feed's reveal-older-items pagination and the Attachments panel's dedup/scroll-to-source logic stay unit-tested only (triggering either live needs 15+ comments, too slow for this suite).

/team/activity (the global, unfiltered cross-section feed — every /team event with no scoping filter, unlike every other Activity surface in this module) has two separate strands of live coverage, checked first before adding anything new: the "N new" live-update pill mechanism on this exact page is already thoroughly proven by realtime-new-items-pill.mjs (both halves of the rule — rows merge in silently at the top, are held and offered as a count once scrolled into history) and realtime-two-sessions.mjs; a new file, scripts/e2e/menu/activity.mjs (7 checks, 2026-08-12), covers what neither of those touches — the page's own filters and search: the feed loads with real events, a nonsense search query empties it to the "no results" state and clearing it restores the feed, the type filter's option list renders real values, and choosing/resetting a filter enables/disables the panel's Reset button (the only on-screen signal a filter is active, since the trigger itself is an icon with no visible label). Companion unit coverage, apps/portal/pages/team/activity.test.ts (10 tests) — search across all 5 slots the feed can match on (actor/subject/name/project/department), the type/action/actor/project/department/period filters each narrowing the list correctly, activeFilterCount deliberately excluding search, and — the first page in this module to actually exercise it — useTeamActivity()'s own cursor-based loadMore pagination (hasMore turning off once a short page comes back), which every OTHER Activity surface's tests never touch since none of them render a "load more" state.

Shared Documents (the one shared "P4P Internal" file space, at /team/shared- documents — see IAM → Shared Documents) had zero coverage, unit or live, before this round. A new file, scripts/e2e/menu/shared-documents.mjs (8 checks, 2026-08-12), is the first script anywhere in this suite to drive a real file upload live — the hidden <input type="file"> behind FileUpload's dropzone takes page.setInputFiles directly, bypassing its own drag-and-drop/click-to-open UI (that component's own concern, already covered by its own ui-kit tests). One continuous lifecycle reusing the same uploaded file rather than creating three: upload a real .txt file (confirmed via a real 201), find it via search, soft-delete it, confirm it lands in Trash, restore it, confirm it's back in the active list, then soft-delete again and permanently purge it — fully self-cleaning. One real gotcha found while writing the "gone" checks: the delete toast reads "{name}" deleted, which literally contains the file's own name, so a page-wide text search can't tell "still in the table" from "mentioned in a toast that hasn't faded yet" apart — every presence/absence check is scoped to an actual table row (page.locator('tr', { hasText: fileName })) instead. Companion unit coverage, apps/portal/pages/org/[slug]/shared-documents.test.ts — block A (list, search, the active/trash toggle, canManage gating, download, soft-delete, restore, permanent delete; 11 tests) and block B (the upload flow itself, same round, 12 more tests — 23 total): image-file selection getting an immediate blob preview vs. a non-previewable file getting none yet (PDF/video previews are real browser work — a dynamically-imported pdf.js decode and an offscreen <video> canvas grab — deliberately not exercised here, the same "not worth mocking a real decode pipeline" call made elsewhere in this initiative), removing a selected file revoking its blob URL, confirmUpload's real XMLHttpRequest construction with the correct Authorization/X-Organization-Id headers and a real upload.onprogress event updating the item's percent, a 201 clearing the item and reloading the list, a non-401 error status showing the server's own message (falling back to a generic one for a non-JSON body), a network-level xhr.onerror, a 401 refreshing the token via the Nuxt-global $fetch — not the api-client mock, since a refresh runs before there's a valid token to attach at all — and retrying with the new one, a failed refresh leaving the item errored rather than retrying forever, onRetry re-sending the same pending file, and onRemove making a later onRetry for that id a no-op. First page in this initiative to need XMLHttpRequest/$fetch mocking at all — a local MockXHR fake plus a stubbed $fetch, neither promoted to the shared test setup yet (single use so far). The live upload check above already proves the happy path end to end; block B's coverage is deliberately the EDGE cases a live run has no clean way to force (a real 401, a mid-upload network failure) — the same "live proves the happy path, unit proves the edges" split this initiative already uses elsewhere (e.g. calendar.mjs skips a live drag-move in favor of onMoveEvent's unit-tested rollback).

Every tab under /team/departments/[id]/* and /team/projects/[id]/* also has its own unit test file (apps/portal/pages/team/{departments,projects}/**/*.test.ts) covering permission gates, filter/dedup logic, and mismatch warnings in detail. apps/portal/pages/team/dashboard.test.ts (28 unit tests) does the same for the Dashboard: permission-gated visibility per card and per quick action, the Calendar card counting from occurrences rather than an event's own startsAt (a recurring series' first instance, not this week's), and the Finance card's mismatch reconciliation. apps/portal/pages/team/tasks/ index.test.ts (14 unit tests) covers the cross-project Tasks page's List view the same way — cascading filter options (a project filter narrows the department/stage/responsible option lists to it), the two filter- invalidation watchers (an upstream filter clearing a now-stranded downstream one), the due/priority filters, the ?project= one-shot drill-down from a project's own Tasks tab (which also switches the view to kanban, unlike the per-project tab's own ?stage= drill-down), the stat tiles' derived counts, and the board's subtask rollup; activity.vue's redirect-only route is covered in the same file. apps/portal/pages/team/tasks/[id].test.ts (38 unit tests) covers the task detail page's entire 4-block split — every inline-edit field's PATCH body (both directions of Stage/Department/Price's clear-to-null semantics, the explicit-null vs. no-op-undefined nuance on Due date, the Responsible/Helpers assignee POST/DELETE calls), the full :manage-vs-canWriteOwn permission matrix — Status is the one field that looks like it should be :manage-only (grouped with Stage/Department/ Responsible/Helpers/Price everywhere else in this doc) but is actually canWriteOwn, same tier as Description/Priority/Due date/Progress; only the kanban board's own drag-to-change-status stays :manage-only — the progress-slider debounce, task delete, the Subtasks card's own visibility rules, the asymmetry between the main task's canWriteOwn-gated Status select and a subtask row's stricter :manage-only one, Comments (the canComment gate on the composer, a failed send preserving the draft for a retry via useChatSubmit's own contract, and editing/deleting a comment being gated purely on being its author — isMyComment — independent of :manage/canWriteOwn entirely, confirmed across all three permission combinations), and History/feed infrastructure (History tab switching and the task.comment_created exclusion, feedVisibleCount/revealOlderFeed/ feedHasMore, the Attachments panel's cross-source file-id dedup, and a real bug found while writing this coverage: goToAttachmentSource widens feedVisibleCount to reveal an older comment's attachment, but if the click also has to switch tabs (e.g. starting from History), the page's own watch(feedTab, ...) — which resets the window on every tab switch — fires at the exact await nextTick() the function itself awaits next, silently undoing the widening; only manifests cross-tab, confirmed alongside a passing same-tab control case; not yet fixed, reported per the standing "report gaps found while testing" instruction). apps/portal/ pages/team/hr-pool/index.test.ts (13 unit tests) and .../[id].test.ts (8 unit tests) together cover HR Pool's full review-state machine — it is not a strict linear pipeline: "Take in review" only offers from SUBMITTED, "Approve" offers from SUBMITTED or UNDER_REVIEW (a fresh submission can be approved directly), "Deactivate" offers from any status except already INACTIVE, "Invite" only once APPROVED and not already linked to a user, and "Delete" only once the entry has no engagement history — plus the review-notes composer's canComment-style gate on the entry's own page. apps/portal/pages/team/members/{index,[id], [id]/index,[id]/projects,[id]/tasks,[id]/schedule,[id]/activity, [id]/candidate-application}.test.ts (round 1 of a 4-round split covered list/shell/Overview; round 2 added Projects+Tasks; round 3 added Schedule+Activity; round 4 adds Candidate application, closing out /team/members and this whole module entirely) covers the roster's search/department/status filters, departmentsLabel/rolesLabel joining every value rather than just the first, "Add member" needing platform:staffing:manage AND (isSuperAdmin or isOwner — mirrors the real invite endpoint's SuperAdminOrOwnerGuard) at the time this round was written — since superseded, the gate is now a single platform:users:invite check with no combined staffing/superadmin condition, see Employees above — the shell's tab routing and its "more actions" menu (Send on leave / Return from leave under platform:users:manage, Revoke staff under isSuperAdmin/isOwner), the Overview tab's personal-info fields and interest-area labelling, the Projects tab's search/grid-vs-list view mode (persisted to localStorage, survives a fresh mount) and its two distinct empty states, the Tasks tab's search/status filter, its own sort order (open tasks before DONE ones, nearest due date first within each group), and overdue highlighting (a DONE task past its due date is deliberately NOT flagged), the Schedule tab's visible/blocked-time split, blocked-time visibility (TEAM shows the real title, anything else reads as generic "Busy", a recurring block adds a "repeats weekly" hint), and the busy/visible-event dedup (a confirmed meeting's own interval must not also draw a duplicate opaque "busy" chip in the same slot), the Activity tab's actor-scoped feed — this is the first page in the whole initiative to touch the Calendar module's own composables (useCalendarEvents/useEventTooltip/toCalendarItems), ahead of a future dedicated /team/calendar round that will reuse the same stubbing approach — and the Candidate application tab's contact fields, interestAreaLabel's OTHER-with-custom-detail branch, the notes card's own presence gate, and the whole tab rendering nothing at all for a member with no application on file. Deliberately NOT given a live e2e check this round — staffing.mjs's own HR Pool flow (testHrPool) invites an applicant to platform staff and deletes the HR Pool entry right after, but nobody ever accepts that invitation, so no real TeamMember with a populated candidateApplication exists anywhere in this suite's current run; adding one would mean accepting a second throwaway invite via a new browser context (the same shape as acceptStaffInvite) purely to exercise an already-thoroughly-unit-tested read-only view — a disproportionate setup cost for this round, matching the same judgment call that skipped live "Duplicate" (round 27) and "Transfer ownership" (round 26).

Found, not fixed, escalated this round (test-infra, not app code): staffing.mjs has never revoked the throwaway "E2E Staffing Member" platform-staff account it invites each run — unlike Departments/Projects (each fixed in an earlier round), nothing cleans this one up, so the roster has accumulated 10+ orphans. Previously framed as DB clutter; this round found it's worse — the roster's own name search landed on a STALE run's member instead of the current one, making the new Tasks-tab check flake (that member's own tab correctly said "No tasks yet.", just for the wrong person). Worked around by searching on staffEmail (unique, timestamped) instead of the shared display name, but the orphan accumulation itself is still unaddressed and will trip up any future name-based lookup here the same way. Flagged rather than fixed unilaterally.

Real bugs found and fixed along the way (2026-08-04 through 2026-08-11): the manage_own file-attachment 403 documented in Tasks above; 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; a project's Participants tab counting people once (its own tab badge) but listing one row per department×person pairing (its own table) — fixed by grouping the table to one row per person. Still open: a real, previously-undetected bug in reka-ui's AlertDialogAction (just DialogClose with no defaultPrevented gate) that silently defeats the "keep the dialog open, show the server error inline" pattern on several delete/remove confirms across this module and IAM — the department Roles tab's own repro of this no longer exists (the whole tab was removed 2026-08-14, see Members above), but the same class of bug is still reproducible on this module's other delete-confirm dialogs; not fixed yet, since fixing it means touching prod code across every site, a separate piece of work from testing.

Not covered by automation, still worth a manual pass — see docs/MANUAL_TESTING.md: department-leader-only access to a department's Finances tab, 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 not already covered above.