-
-
Notifications
You must be signed in to change notification settings - Fork 27
People Developer Guide
User guide: People Β· Issue: #153 step 2 Β· Plan: roadmap
A read-only module that assembles one person (a users row) or one company (a tenants row) from every module that already holds data about them. No tables of its own β nothing is stored, copied or synchronised.
| File | Role |
|---|---|
includes/people.php |
The data layer: lists, personDetail(), companyDetail(), and one function per section |
people/index.php |
People list (search + filters, fetches api/people/list.php) |
people/companies.php |
Companies list (server-rendered) |
people/person.php, people/company.php
|
The record pages (server-rendered from one personDetail() / companyDetail() call) |
people/suppliers.php, people/supplier.php, people/contact.php
|
Suppliers list (server-rendered, filtered in the browser) and the supplier / supplier-contact pages, from peopleSupplierRows(), supplierDetail(), supplierContactDetail() (3.1.0) |
people/includes/header.php, render.php
|
Module header; shared section cards, <head>, helpers (ppl*) |
api/people/list.php |
GET ?q=&company=&status=active|leavers|all β up to 300 rows |
assets/css/people.css |
All styling, including the header-nav rule and the accent (--ppl-accent, local to the file so theme.css did not need re-versioning) |
lang/en/people.php |
The people namespace |
Registered in: getModuleRegistry() (module access), includes/waffle-menu.php (waffle + βK module list), includes/module-colors.php, the landing page, common.modules.people, entityLink('person'|'company'), RECENT_TRAIL_MODULES, api/system/global_search.php and assets/js/command-palette.js (types person, company, and since 3.1.0 supplier, supplier_contact).
Every section passes both, and both belong to the module that owns the data:
-
Module β
analystCanAccessModule($conn, $aid, '<module>'). A section the reader may not see is absent from$sections, not empty: "no tickets" and "you may not see tickets" are different answers, and the renderer never has to know about permissions. -
Company β the page needs
analystCanAccessUser()/analystCanAccessTenant(); each section then applies its module's own rule:
| Section | Module | Query | Company rule |
|---|---|---|---|
| tickets | tickets |
tickets.user_id / company match, deleted_datetime IS NULL
|
allAccessibleTenantsFilter(t.tenant_id) |
| assets | assets |
users_assets.user_id / assets.tenant_id
|
allAccessibleTenantsFilter(a.tenant_id) |
| contracts | contracts |
party_type='customer' and customer_user_id / contractCustomerTenantExpr()
|
contractVisibilitySql() |
| domains | domains |
domains.customer_user_id / domains.tenant_id
|
allAccessibleTenantsFilter(d.tenant_id) |
| courses | lms |
person: lmsMyCourses(LmsLearner::user()); company: lms_progress grouped by course |
LMS has no company scope |
| forms | forms |
form_submissions.submitted_by_user_id |
forms have no company; the ticket link is shown only if analystCanAccessTicket()
|
The manager and each report are people too: shown only if the reader could open their page.
π Accessible companies, not the active one. A person page is reached by id from anywhere β a contract, a domain, βK. Filtering to the header's active company would show a real person with "no tickets" whenever the picker pointed elsewhere. Every row shown is one the analyst may already open in its own module.
A person with no company belongs to the Default company (peopleCompanyMatch() adds OR col IS NULL for Default), as everywhere else.
Shown, not moved. A supplier is a row in suppliers, a supplier contact a row in contacts β both owned and edited by Contracts. The roadmap's step 3 first imagined moving contacts into users; that would have made every supplier contact a potential portal sign-in, requester and directory-sync target, and needed a migration on every install. Giving them pages over the tables they already live in got the "one place to look" without any of that.
| Rule | |
|---|---|
| Module gate |
peopleCanSeeSuppliers() = People and Contracts. supplierDetail() / supplierContactDetail() return null without it (the page then reads as not found); suppliers.php calls requireModuleAccess('contracts'); the header hides the tab; βK and the recent trail apply the same pair. |
| Company | Suppliers and contacts have no company β install-wide, as in Contracts. Each section still applies its own module's rule: contracts via contractVisibilitySql(), assets and domains via peopleScope() (accessible companies). |
| Sections | supplier: contacts (always), contracts (supplier contracts β the page already needs Contracts), assets (assets.supplier_id, gated by Assets), domains (gated by Domains). contact: domains. |
| Domain roles |
peopleSupplierDomains($who) with ['supplier' => id] or ['contact' => id] selects one is_<role> flag per condition β supplier: registrar_supplier_id, customer_supplier_id, and tech_contact_id IN (its contacts); contact: tech_contact_id, customer_contact_id β and returns roles per row. pplSectionDomains() adds an As column only when rows carry roles, so person and company pages are unchanged. |
| Labels |
pplStats() takes a $labels map and pplSectionContracts() / pplSectionAssets() / pplSectionDomains() a title, because on a supplier "Contracts as customer" would be wrong. pplSectionContracts(β¦, false) drops the With column. |
| Entity types |
entityLink('supplier' | 'supplier_contact'), RECENT_TRAIL_MODULES (both people, gated on Contracts in recentTrailLabels()). |
$c and was overwritten by the waffle (below) β a fatal on the first load. The pages use $sup and $ct.
- A function in
includes/people.phptaking$who = ['user' => id]or['company' => id], returning['total' => β¦, 'rows' => [β¦]], with that module's company rule. - Call it from
personDetail()/companyDetail()behind$can('<module>'). - A renderer in
people/includes/render.php, added topplSections()andpplStats(). - A hidden + positive-control pair in
tests/people-scope.php. - If the companies page should show its count, add a grouped query to
peopleCompanyCards()(below).
peopleCompanyCards() is peopleCompanies() plus figures for each card. It runs one grouped query per figure rather than companyDetail() per company, and each query follows the company page's own rules so a card and the page it opens cannot disagree:
- a module's figure is
null(not shown) unless the analyst can open that module -0would be a claim; -
peopleScope()limits it to accessible companies; -
COALESCE(tenant_id, <default>)counts a record with no company under Default, aspeopleCompanyMatch()does; -
People counts everyone, leavers included, because the company page's People figure counts its list, which shows leavers.
peopleCompanies()itself (used by the list page's Company drop-down) still counts current people only.
Proved by fetching every company's page and comparing its figures with its card.
people/help.php is the house guide layout (help.css), the Domains guide's shape: a $sections array of id => paragraph count, words in lang/en/people.php under help.<id>.*, with **bold** allowed in paragraphs. help.css is passed to pplHead()'s third argument, which links page stylesheets before mobile.css so the mobile layer keeps winning its ties.
includes/waffle-menu.php runs in the page's global scope and uses $c, $key, $row, $stmt and $conn. A page variable called $c was silently overwritten by the waffle's colour loop β the company page lost its name and showed PHP warnings. Use descriptive names ($company) on module pages.
people/includes/header.php loops its nav as [$href, $label, $icon]. The guide's first draft had a closure called $icon; after the header it was a string of SVG paths, and calling it was a fatal error half-way down the page. It is $pplHelpIcon now. Any variable a page defines before include 'includes/header.php' and uses after it must not share a name with either include's locals.
inbox.css makes <body> overflow: hidden, so a page that relies on the document scrolling cannot be scrolled at all. .ppl-page therefore scrolls itself (height: calc(100vh - 62px); overflow-y: auto), like .ppl-main-list on the list page; on a phone LAYER 42a hands it the remainder of the flex body instead.
php tests/people-scope.php β 26 checks inside an always-rolled-back transaction: a restricted analyst cannot list, open or follow a person or company outside their companies (with positive controls), sees only the reports and manager they may see, and an analyst with only People + Tickets gets exactly one section. It also covers the Assets β Users fix (#2095) found while building this, and (3.1.0) that a supplier's and a contact's domains keep to the analyst's companies β with positive controls and the roles reported β and that People without Contracts opens neither page. The restricted analyst is now picked by SQL alone and given every module inside the transaction, because the module check caches per analyst for the life of the process.
See also: People Β· Customer contracts β Developer Guide Β· Multi-tenancy β Developer Guide
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96