System Overview
The Admin module is the operational heart of each tenant business. An admin or staff user logs in and operates entirely within their own business scope. No cross-tenant data access is possible — every query is filtered by the business_id resolved from the authenticated user's record.
| Role | Type | Prefix | Capabilities |
|---|---|---|---|
| super_admin | Platform | /api/platform/ | Manage all businesses, plans, audit events |
| admin | Business | /api/admin/ | Full access to own business data; assign roles to staff |
| staff | Business | /api/admin/ | Permission-gated access; cannot manage roles |
| employee | Employee portal | /api/employee/ | Tasks, clock, calendar, site visits, gallery |
| contractor | Contractor portal | /api/contractor/ | Daily logs, expenses, materials, vouchers, ledger |
| client | Client portal | /api/client/ | Project progress, documents, gallery, support |
The admin controller uses AuthorizesEngineeringAccess concern which provides rejectUnlessBusinessPortal() — this checks user_type IN (admin, staff) — and rejectUnlessPermission() for per-resource checks.
Authentication & Business Portal Access
Admin and staff users authenticate via the same POST /api/login endpoint as all other roles. After login, the user_type field in the response determines which portal the mobile/web app should route to.
user_type: if admin or staff, route to Admin portalAuthorization: Bearer {token}rejectUnlessBusinessPortal() validates user_type on every /api/admin/* routebusinessIdOrFail() resolves business_id from auth user → scope all queriesrejectUnlessPermission(slug) checks Spatie-style permission for staff users| Concern Method | Purpose |
|---|---|
| rejectUnlessBusinessPortal() | Ensures user_type is admin or staff; 403 otherwise |
| businessIdOrFail() | Reads business_id from auth user; 403 if missing |
| rejectUnlessPermission(slug) | Admin bypasses; staff must have the slug via role |
| rejectUnlessSuperAdmin() | Used in /api/platform/* only; not used in admin module |
Setup Flow & Dependency Order
Before a business can run projects, reference data must be configured. The correct setup order avoids foreign key issues and ensures dropdowns are populated in the mobile app.
POST /api/admin/locations. Projects and staff are linked to a location_id.POST /api/admin/trade-types.POST /api/admin/document-types.POST /api/admin/phase-library. phase_key must be a unique slug.POST /api/admin/subcontractor-categories.POST /api/admin/project-templates.phase_key = Str::slug(name) // e.g. "Electrical Works" -> "electrical-works"
// Stored in phase_library.phase_key; ProjectPhase references this key
Project Lifecycle
A project is the primary entity in the Admin module. It moves through a status lifecycle from draft through completion. All financial, scheduling, and people records are linked to a project_id.
| Status | Meaning | Allowed next status |
|---|---|---|
| draft | Being configured, not yet active | active |
| active | Work in progress | on_hold, completed, cancelled |
| on_hold | Temporarily paused | active, cancelled |
| completed | All phases done; ready for sign-off | (terminal) |
| cancelled | Abandoned | (terminal) |
Project relationships:
| Related entity | Linked via | Notes |
|---|---|---|
| ProjectPhase | project_id | One project has many phases; progress_percent tracks completion |
| Proposal | project_id | Many proposals per project (bidding rounds) |
| Voucher | project_id | Subcontractor payment claims per week |
| LedgerEntry | project_id | Double-entry records of all money in/out |
| VariationOrder | project_id | Scope changes with cost impact |
| DayworkOrder | project_id | Hourly/day rate works outside original scope |
| CalendarEvent | project_id | Milestones, inspections, site visits |
| MaterialStock | project_id | Inventory tracked per project |
| DailyLog | project_id | Daily site progress reports |
| ProjectDocument | project_id | Linked to document_type_id for categorisation |
| GalleryItem | project_id | Site photos, before/after imagery |
budget_consumed = SUM(ledger_entries WHERE entry_type="payment" AND project_id=N)
vo_exposure = SUM(variation_orders WHERE project_id=N AND status IN (pending,approved))
dwo_exposure = SUM(daywork_orders WHERE project_id=N AND status IN (draft,submitted))
total_liability = budget_consumed + vo_exposure + dwo_exposure
remaining = contract_value - total_liability
Proposals & Bidding Flow
A Proposal is a price quotation attached to a project. The current_round field tracks how many revision rounds have occurred. Multiple proposals can exist per project, supporting re-quoting after scope changes.
status: draftstatus: submittedcurrent_round incremented, new quoted_amountstatus: approved, project contract_value updated to matchactive| Proposal status | Meaning |
|---|---|
| draft | Being prepared, not yet sent to client |
| submitted | Sent to client for review |
| approved | Client accepted; triggers contract_value update |
| rejected | Client declined; start new round or close project |
| revised | Superseded by a new round |
current_round starts at 1
On each revision: current_round++, new quoted_amount set
ProposalRound records archive each round: { round_number, amount, status, notes }
Phase Management & Progress Tracking
Phases are the building blocks of a project timeline. Each phase has a phase_key that references the Phase Library, a progress_percent (0–100), and lifecycle status.
| Phase status | Meaning |
|---|---|
| pending | Not started yet |
| in_progress | Work under way; progress_percent 1-99 |
| completed | All work done; progress_percent 100 |
| skipped | Excluded from this project instance |
The project's current_phase_key field is updated manually or automatically when a phase reaches 100%.
project_progress = SUM(phase.progress_percent) / COUNT(phases WHERE status != "skipped")
// To bulk-load phases from a template:
// 1. GET /api/admin/project-templates/{id} -> returns phase_keys[]
// 2. For each key, POST /api/admin/phases with project_id + phase_key + name from library
Phase Library: Stores default_duration_days which apps use to pre-fill planned_start/planned_end when a phase is added from a template. The library is business-scoped — businesses build their own phase vocabulary.
Financial Instruments
The Admin module has four interlocking financial objects. Understanding their relationships prevents double-counting and ensures the ledger reflects reality.
Voucher → Ledger flow
draft → submitted → approvedentry_type: payment, credit: amount, reference: voucher_nodebit = 0 (money out), credit = amount (liability recorded)| Instrument | Purpose | Key fields |
|---|---|---|
| Voucher | Weekly subcontractor payment claim | voucher_no, voucher_type, amount, week_ending, status |
| LedgerEntry | Formal double-entry record of cash movement | entry_type, debit, credit, party_type, party_id, reference |
| VariationOrder | Extra scope beyond original contract | vo_number, amount, status (pending/approved/rejected) |
| DayworkOrder | Labour/equipment on a day-rate basis | dwo_number, amount, work_date, status |
balance = SUM(debit) - SUM(credit) WHERE party_type="subcontractor" AND party_id=N
// Positive balance = owed to subcontractor; negative = overpaid
// Variation Order financial exposure
approved_vo_total = SUM(amount WHERE project_id=N AND status="approved")
pending_vo_total = SUM(amount WHERE project_id=N AND status="pending")
People Management
The Admin module manages two types of people: Staff (employees on the admin's payroll) and Subcontractors (external companies). Both are business-scoped.
Staff
Staff records live in StaffProfile and link to a user_id in the users table. The user gets user_type=staff and can log in to the Admin portal with their assigned permissions.
| Staff field | Notes |
|---|---|
| user_id | Required; links to platform user account |
| location_id | Which office/site the staff member is based at |
| job_title | Free text; used in directory and reports |
| department | Group classification (Engineering, Finance, etc.) |
| status | active / inactive / suspended |
Subcontractors
Subcontractors represent external companies. They can optionally have a linked user_id for Contractor portal access. Without a user_id they exist as data records only (for vouchers and ledger references).
| Subcontractor field | Notes |
|---|---|
| user_id | Optional; if set, the linked user can log in to Contractor portal |
| company_name | Required; legal/trading name of the subcontracting firm |
| trade_type | Must match a trade_type.name in the business trade types list |
| contact_email / contact_phone | Primary contact for the company |
| status | active / inactive |
IF subcontractor.user_id IS NOT NULL:
user.user_type = "contractor"
contractor API routes become available to that user
contractor sees only vouchers/ledger linked to their subcontractor_id
Calendar & Scheduling
Calendar events are the scheduling backbone. They link to a project and optionally to an assigned user. The upcoming_event_count on the dashboard reflects events with starts_at > now().
| event_type values | Used for |
|---|---|
| inspection | Official sign-off inspection by engineer or authority |
| meeting | Progress meeting, client meeting, etc. |
| milestone | Key deliverable or handover |
| site_visit | Unscheduled or scheduled site observation |
| other | Anything not covered above |
Document Control & Signature Settings
Project documents are categorised by document_type_id, linking to the Document Types reference table. Documents with requires_approval: true must be reviewed before being visible to clients.
| Document flow stage | Action |
|---|---|
| Upload | POST /api/uploads → returns staged file path |
| Create record | POST /api/admin/... with file_path from staging + document_type_id |
| Approval queue | Filter documents WHERE status="pending" AND document_type.requires_approval=true |
| Approved | status updated to "approved"; visible in client portal |
| Rejected | status updated to "rejected"; uploader notified |
Signature Settings
The SignatureSetting record stores the digital signature used on vouchers and certificates. One record per business (user_id = 0 for the business default). Apps render this signature image on generated PDF documents.
signature_image_path // URL path to signature PNG
signer_name // Display name on documents
signer_title // Job title below signature
// PUT /api/admin/signature-settings upserts:
// if record exists: UPDATE; if not: INSERT with business_id + user_id=0
Roles & Permissions
The Roles system allows the business admin to define custom permission sets and assign them to staff members. The admin role has all permissions by default and cannot be restricted.
IF user.user_type === "admin": PASS (no permission check)
ELSE IF user.user_type === "staff":
permissions = user.role.permissions.pluck("slug")
IF slug NOT IN permissions: abort(403)
Permission Matrix
Full list of all permission slugs in the Admin module. Admin users have all permissions implicitly. Staff users must be assigned a role that includes the required slug.
| Permission slug | What it grants | Endpoints covered |
|---|---|---|
| dashboard.view | View the admin dashboard KPIs | /api/admin/dashboard |
| projects.view | Read access to projects and all sub-routes | /api/admin/projects, /api/admin/projects/{id}/* |
| projects.manage | Create, update, delete projects | /api/admin/projects (POST/PUT/DELETE) |
| proposals.view | Read proposals | /api/admin/proposals (GET) |
| proposals.manage | Create/update/delete proposals | /api/admin/proposals (POST/PUT/DELETE) |
| phases.view | Read project phases | /api/admin/phases (GET) |
| phases.manage | Create/update/delete phases | /api/admin/phases (POST/PUT/DELETE) |
| locations.view | Read locations | /api/admin/locations (GET) |
| locations.manage | Create/update/delete locations | /api/admin/locations (POST/PUT/DELETE) |
| staff.view | Read staff profiles | /api/admin/staff (GET) |
| staff.manage | Create/update/delete staff profiles | /api/admin/staff (POST/PUT/DELETE) |
| subcontractors.view | Read subcontractor records | /api/admin/subcontractors (GET) |
| subcontractors.manage | Create/update/delete subcontractors | /api/admin/subcontractors (POST/PUT/DELETE) |
| vouchers.view | Read payment vouchers | /api/admin/vouchers (GET) |
| vouchers.manage | Create/update/delete vouchers | /api/admin/vouchers (POST/PUT/DELETE) |
| ledgers.view | Read ledger entries | /api/admin/ledgers (GET) |
| ledgers.manage | Create/update/delete ledger entries | /api/admin/ledgers (POST/PUT/DELETE) |
| variation_orders.view | Read variation orders | /api/admin/variation-orders (GET) |
| variation_orders.manage | Create/update/delete variation orders | /api/admin/variation-orders (POST/PUT/DELETE) |
| daywork_orders.view | Read daywork orders | /api/admin/daywork-orders (GET) |
| daywork_orders.manage | Create/update/delete daywork orders | /api/admin/daywork-orders (POST/PUT/DELETE) |
| calendar.view | Read calendar events | /api/admin/calendar (GET) |
| calendar.manage | Create/update/delete calendar events | /api/admin/calendar (POST/PUT/DELETE) |
| phase_library.view | Read phase library | /api/admin/phase-library (GET) |
| phase_library.manage | Create/update/delete phase library items | /api/admin/phase-library (POST/PUT/DELETE) |
| document_types.view | Read document types | /api/admin/document-types (GET) |
| document_types.manage | Create/update/delete document types | /api/admin/document-types (POST/PUT/DELETE) |
| trade_types.view | Read trade types | /api/admin/trade-types (GET) |
| trade_types.manage | Create/update/delete trade types | /api/admin/trade-types (POST/PUT/DELETE) |
| subcontractor_categories.view | Read subcontractor categories | /api/admin/subcontractor-categories (GET) |
| subcontractor_categories.manage | Create/update/delete subcontractor categories | /api/admin/subcontractor-categories (POST/PUT/DELETE) |
| project_templates.view | Read project templates | /api/admin/project-templates (GET) |
| project_templates.manage | Create/update/delete project templates | /api/admin/project-templates (POST/PUT/DELETE) |
| settings.view | View signature settings | /api/admin/signature-settings (GET) |
| settings.manage | Update signature settings | /api/admin/signature-settings (PUT) |
| manage_roles | Create and view roles | /api/admin/roles (GET/POST) |
Error Reference
| HTTP code | Error field value | Cause |
|---|---|---|
| 401 | Unauthenticated | Token missing, expired, or invalid |
| 403 | Forbidden | user_type not admin/staff, or missing permission slug |
| 404 | Not found | Record ID does not exist or belongs to another business |
| 422 | Validation failed | Request body fails validation; data contains field-level errors |
| 500 | Server error | Unexpected exception; check Laravel logs |
Validation error shape (422):