-
Notifications
You must be signed in to change notification settings - Fork 15
Catalogue Request Approvals Developer Guide
How catalogue-request approvals work, and why the shape is the way it is. Shipped as #928 (the gate + approver inbox + auto-raise) and #929 (the requester's dashboard view).
The user-facing page is Catalogue Request Approvals.
Colour key: ποΈ schema Β· βοΈ engine Β· π API Β· π₯οΈ UI Β· π¨ CSS Β· π i18n Β· π docs
| π¨ | File | What it does |
|---|---|---|
| ποΈ | database/freeitsm.sql |
forms.requires_approval / approver_id; form_submissions.approval_status / approver_id / approval_decided_by_id / approval_decided_datetime / approval_comment; the four FKs to analysts
|
| ποΈ | includes/db_verify_schema.php |
the same columns (columns only β FKs live in freeitsm.sql) |
| βοΈ | includes/catalogue_approvals.php |
the engine. catalogueApprovalDecide(), catalogueCreateTicketFromSubmission(), catalogueApprovalsList(), catalogueApprovalGate(), catalogueSubmissionBodyHtml()
|
| βοΈ | includes/services/forms.php |
FormsService::submitForm() gates the submission; saveForm() stores the per-item settings |
| π | api/forms/catalogue_approvals.php |
the inbox list (?filter=mine|all|decided) |
| π | api/forms/decide_catalogue_approval.php |
approve / reject one request |
| π | api/forms/save_form.php |
unchanged β passes requires_approval / approver_id straight through to the service |
| π | api/forms/get_forms.php |
returns the approval columns + approver_name for the list |
| π | api/self-service/get_dashboard.php |
returns the requester's requests[] with their state (#929) |
| π₯οΈ | forms/index.php |
the π‘οΈ shield button, the Approval settings modal, the amber pill |
| π₯οΈ | forms/approvals.php |
the approver inbox (cloned from change-management/approvals.php) |
| π₯οΈ | forms/includes/header.php |
the Approvals nav item |
| π₯οΈ | self-service/index.php |
the Your requests dashboard section (#929) |
| π |
lang/en/forms.php + lang/pt-BR/forms.php
|
the approval block, same commit |
| π |
lang/en/self-service.php + lang/pt-BR/self-service.php
|
the dashboard req_* keys (#929) |
| π |
CHANGELOG.local.md, this wiki |
#928 / #929 |
There is no new table: a catalogue item is a forms row and a request is a form_submissions row, so the approval state hangs off columns on those, mirroring how is_portal_visible was added.
The classic version routes a request to the requester's line manager. Portal users (users) have no manager relationship β only tenant_id (their company) β so v1 reuses Change Management's single-approver idea instead: one analyst signs a catalogue item off.
The Change approval code is a good template but nothing is shareable: it's welded to the changes table and to analysts, with status-name string literals. So this is a parallel, smaller implementation of the approver + inbox pattern from Change Management.
FormsService::submitForm() decides at submit time whether to gate:
$gateApproverId = ($portalUserId !== null && !empty($form['requires_approval']) && !empty($form['approver_id']))
? (int) $form['approver_id'] : null;
$approvalStatus = $gateApproverId !== null ? 'pending' : 'not_required';Three deliberate rules live in that one line:
-
Portal only (
$portalUserId !== null). The feature auto-raises a ticket for the requester; an analyst filling a form internally has none, so it's never gated. -
Unconfigured β gated.
requires_approvalon with noapprover_idfalls through tonot_requiredβ a form must never strand a request with nobody able to clear it. -
The approver is snapshotted onto
form_submissions.approver_idhere, not read from the form at decision time. Editing the catalogue item later re-routes future requests, never ones already waiting.
π A gated submission fires
catalogue_request.submitted, NOTform.submitted. An admin's create-ticket workflow rule onform.submittedwould otherwise raise the ticket immediately and jump the gate. The outcome events arecatalogue_request.approved/.rejected. All three are best-effort (guarded byclass_exists('WorkflowEngine')and try/catch) β notification is a bonus, never the mechanism.
catalogueApprovalDecide($conn, $actorId, $submissionId, $decision, $comment):
- Loads the submission; refuses if it isn't
pending. - Refuses unless the actor is
approver_idβ orsessionIsAdmin(). - On approve, inside one transaction: raise the ticket, then stamp
approval_status='approved',approval_decided_by_id, timestamp, comment andticket_id. On reject: the same minus the ticket.
catalogueCreateTicketFromSubmission() deliberately mirrors api/self-service/create_ticket.php β the correct portal path:
- requester resolved by
users.id(never an email string β that's the bug inWorkflowEngine::action_create_ticket()); - company from the requester's own
tenant_id, falling to NULL/Default exactly as the portal's new-ticket path does; - status
Open, the install's default priority; - body built by
catalogueSubmissionBodyHtml()β the answers as a fully-escaped table, so a customer's field values can't inject markup into the analyst's reading pane.
π This is also where promote-to-ticket finally landed.
form_submissions.ticket_idhad been a reserved "not yet actioned" column that nothing wrote. The approve path is its first writer.
-
Config (
forms/index.php): the shield button opens a self-contained modal (#approvalModal). Active analysts are fetched once fromapi/tickets/get_analysts.php. Save posts only{id, requires_approval, approver_id}β a partial update, the same contract as the portaltogglePortal, so nothing else about the form is touched. -
Approver inbox (
forms/approvals.php): cloned fromchange-management/approvals.phpβ filter sidebar (mine/all/decided) + card list, each card showing the answers and Approve/Reject with an optional note.showToastcomes from the shared header, so the page doesn't loadtoast.jsitself. -
Requester view (
self-service/index.php, #929): a Your requests section, hidden until the dashboard payload carries any.api/self-service/get_dashboard.phpreads them in a guarded query β a pre-upgrade instance without the approval columns degrades to no requests rather than a broken dashboard.
-
php -lacross every changed file. -
Engine end-to-end against the dev DB: seed a pending request β
catalogueApprovalsListshows it βcatalogueApprovalDecide(approve)raises a ticket, stampsticket_id, setsapproved; requester (user_id), company and the answers-in-body all correct; re-approve and wrong-approver both refused with their specific messages. - Both analyst pages rendered with a forged analyst session (HTTP 200, no PHP errors), inline JS parsed clean in headless Chrome.
- #929:
get_dashboard.phpreturns the three states (pending / approved+ticket_number / rejected) via a forged portal session; the dashboard renders clean.
-
Manager-based routing β the headline next step. Needs
users.manager_id(or equivalent) and a way to populate it (LDAP/SSO/manual), then gate on the requester's manager instead of a fixed analyst. That relationship is its own mini-project. -
A multi-approver board β lift Change Management's
voteCab(enum + membership check + double-vote block + all/majority threshold) into acatalogue_request_approverstable. -
Show the rejection reason to the requester β the note is stored (
approval_comment); surfacing it on the portal is a product decision (flip the approver's note from internal to requester-visible). -
Attachments β forms have no file fields today; if they gain uploads, carry them onto the ticket in
catalogueCreateTicketFromSubmission()(the pattern is increate_ticket.php). -
A "raise ticket" button for NON-gated submissions β
forms/submissions.phpstill has none; the approve path only covers gated ones.
- Catalogue Request Approvals β the analyst-facing page
- Portal Request Catalogue β how a form becomes a catalogue item
- Self-Service Developer Guide β portal internals and the rules when extending it
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- 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
- β³ 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)