Business Admin · Business Guide 13 numbered sections — reference by ADM-XXX for debugging or enhancement
ADM-001

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.

RoleTypePrefixCapabilities
super_adminPlatform/api/platform/Manage all businesses, plans, audit events
adminBusiness/api/admin/Full access to own business data; assign roles to staff
staffBusiness/api/admin/Permission-gated access; cannot manage roles
employeeEmployee portal/api/employee/Tasks, clock, calendar, site visits, gallery
contractorContractor portal/api/contractor/Daily logs, expenses, materials, vouchers, ledger
clientClient 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.

ADM-002

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.

1POST /api/login with username + password
2Auth guard verifies credentials → returns Sanctum bearer token
3Response includes user_type: if admin or staff, route to Admin portal
4All subsequent requests include Authorization: Bearer {token}
5rejectUnlessBusinessPortal() validates user_type on every /api/admin/* route
6businessIdOrFail() resolves business_id from auth user → scope all queries
7rejectUnlessPermission(slug) checks Spatie-style permission for staff users
⚠ Admin users bypass per-permission checks and have full access to all business resources. Staff users must be assigned the required permission slug to access each resource.
Concern MethodPurpose
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
ADM-003

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.

11 — Locations: Create offices/sites — POST /api/admin/locations. Projects and staff are linked to a location_id.
22 — Trade Types: Skill categories for subcontractors — POST /api/admin/trade-types.
33 — Document Types: Categories of project documents (drawings, certificates, contracts) — POST /api/admin/document-types.
44 — Phase Library: Reusable phase definitions with default durations — POST /api/admin/phase-library. phase_key must be a unique slug.
55 — Subcontractor Categories: Groupings for subcontractor companies — POST /api/admin/subcontractor-categories.
66 — Project Templates: Named sets of phase_keys to bootstrap new projects — POST /api/admin/project-templates.
77 — Staff: Link user accounts to StaffProfile records with job_title, department, location_id.
88 — Subcontractors: Register subcontractor companies. Can optionally link to a user account via user_id.
99 — Ready: Now create projects and link to locations, clients, and phases.
// phase_key constraint: must be unique per business
phase_key = Str::slug(name) // e.g. "Electrical Works" -> "electrical-works"
// Stored in phase_library.phase_key; ProjectPhase references this key
ADM-004

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.

StatusMeaningAllowed next status
draftBeing configured, not yet activeactive
activeWork in progresson_hold, completed, cancelled
on_holdTemporarily pausedactive, cancelled
completedAll phases done; ready for sign-off(terminal)
cancelledAbandoned(terminal)

Project relationships:

Related entityLinked viaNotes
ProjectPhaseproject_idOne project has many phases; progress_percent tracks completion
Proposalproject_idMany proposals per project (bidding rounds)
Voucherproject_idSubcontractor payment claims per week
LedgerEntryproject_idDouble-entry records of all money in/out
VariationOrderproject_idScope changes with cost impact
DayworkOrderproject_idHourly/day rate works outside original scope
CalendarEventproject_idMilestones, inspections, site visits
MaterialStockproject_idInventory tracked per project
DailyLogproject_idDaily site progress reports
ProjectDocumentproject_idLinked to document_type_id for categorisation
GalleryItemproject_idSite photos, before/after imagery
// Project financial summary formula
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
ADM-005

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.

1Project created → admin creates Proposal with status: draft
2Proposal sent to client for review → status: submitted
3Client requests changes → current_round incremented, new quoted_amount
4Client approves → status: approved, project contract_value updated to match
5Work begins → project status moves to active
Proposal statusMeaning
draftBeing prepared, not yet sent to client
submittedSent to client for review
approvedClient accepted; triggers contract_value update
rejectedClient declined; start new round or close project
revisedSuperseded by a new round
// Proposal round tracking
current_round starts at 1
On each revision: current_round++, new quoted_amount set
ProposalRound records archive each round: { round_number, amount, status, notes }
ADM-006

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 statusMeaning
pendingNot started yet
in_progressWork under way; progress_percent 1-99
completedAll work done; progress_percent 100
skippedExcluded from this project instance

The project's current_phase_key field is updated manually or automatically when a phase reaches 100%.

// Overall project progress formula (weighted equal phases)
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.

ADM-007

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

1Subcontractor submits weekly work claim → admin creates Voucher with amount + week_ending
2Admin verifies claim → Voucher status moves draft → submitted → approved
3Payment authorised → admin creates LedgerEntry with entry_type: payment, credit: amount, reference: voucher_no
4Ledger entry double-entry: debit = 0 (money out), credit = amount (liability recorded)
InstrumentPurposeKey fields
VoucherWeekly subcontractor payment claimvoucher_no, voucher_type, amount, week_ending, status
LedgerEntryFormal double-entry record of cash movemententry_type, debit, credit, party_type, party_id, reference
VariationOrderExtra scope beyond original contractvo_number, amount, status (pending/approved/rejected)
DayworkOrderLabour/equipment on a day-rate basisdwo_number, amount, work_date, status
// Running balance per party
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")
⚠ A Voucher and a LedgerEntry are separate records. Creating a voucher does NOT automatically create a ledger entry. The admin must explicitly post the ledger entry once payment is confirmed.
ADM-008

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 fieldNotes
user_idRequired; links to platform user account
location_idWhich office/site the staff member is based at
job_titleFree text; used in directory and reports
departmentGroup classification (Engineering, Finance, etc.)
statusactive / 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 fieldNotes
user_idOptional; if set, the linked user can log in to Contractor portal
company_nameRequired; legal/trading name of the subcontracting firm
trade_typeMust match a trade_type.name in the business trade types list
contact_email / contact_phonePrimary contact for the company
statusactive / inactive
// Linking subcontractor to Contractor portal
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
ADM-009

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 valuesUsed for
inspectionOfficial sign-off inspection by engineer or authority
meetingProgress meeting, client meeting, etc.
milestoneKey deliverable or handover
site_visitUnscheduled or scheduled site observation
otherAnything not covered above
1Admin creates event with title, project_id, starts_at, event_type
2Optionally assigns to a specific user (assigned_user_id)
3Employee portal shows events assigned to that user
4Client portal can be shown milestone events if visibility rules allow
5ends_at is optional; single-point events have null ends_at
ADM-010

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 stageAction
UploadPOST /api/uploads → returns staged file path
Create recordPOST /api/admin/... with file_path from staging + document_type_id
Approval queueFilter documents WHERE status="pending" AND document_type.requires_approval=true
Approvedstatus updated to "approved"; visible in client portal
Rejectedstatus 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.

// GET /api/admin/signature-settings returns:
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
ADM-011

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.

1GET /api/admin/permissions → list all available permission slugs
2POST /api/admin/roles with name + permission_ids[] → creates custom role
3Assign role to staff user via user.role_id update
4On API call: rejectUnlessPermission(slug) checks if staff user's role includes the slug
5Admin users bypass all permission checks
⚠ System roles (admin, staff) cannot be overwritten or deleted. Custom roles can be updated but their slug is auto-generated from name and cannot be changed after creation.
// Permission check logic in AuthorizesEngineeringAccess
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)
ADM-012

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 slugWhat it grantsEndpoints covered
dashboard.viewView the admin dashboard KPIs/api/admin/dashboard
projects.viewRead access to projects and all sub-routes/api/admin/projects, /api/admin/projects/{id}/*
projects.manageCreate, update, delete projects/api/admin/projects (POST/PUT/DELETE)
proposals.viewRead proposals/api/admin/proposals (GET)
proposals.manageCreate/update/delete proposals/api/admin/proposals (POST/PUT/DELETE)
phases.viewRead project phases/api/admin/phases (GET)
phases.manageCreate/update/delete phases/api/admin/phases (POST/PUT/DELETE)
locations.viewRead locations/api/admin/locations (GET)
locations.manageCreate/update/delete locations/api/admin/locations (POST/PUT/DELETE)
staff.viewRead staff profiles/api/admin/staff (GET)
staff.manageCreate/update/delete staff profiles/api/admin/staff (POST/PUT/DELETE)
subcontractors.viewRead subcontractor records/api/admin/subcontractors (GET)
subcontractors.manageCreate/update/delete subcontractors/api/admin/subcontractors (POST/PUT/DELETE)
vouchers.viewRead payment vouchers/api/admin/vouchers (GET)
vouchers.manageCreate/update/delete vouchers/api/admin/vouchers (POST/PUT/DELETE)
ledgers.viewRead ledger entries/api/admin/ledgers (GET)
ledgers.manageCreate/update/delete ledger entries/api/admin/ledgers (POST/PUT/DELETE)
variation_orders.viewRead variation orders/api/admin/variation-orders (GET)
variation_orders.manageCreate/update/delete variation orders/api/admin/variation-orders (POST/PUT/DELETE)
daywork_orders.viewRead daywork orders/api/admin/daywork-orders (GET)
daywork_orders.manageCreate/update/delete daywork orders/api/admin/daywork-orders (POST/PUT/DELETE)
calendar.viewRead calendar events/api/admin/calendar (GET)
calendar.manageCreate/update/delete calendar events/api/admin/calendar (POST/PUT/DELETE)
phase_library.viewRead phase library/api/admin/phase-library (GET)
phase_library.manageCreate/update/delete phase library items/api/admin/phase-library (POST/PUT/DELETE)
document_types.viewRead document types/api/admin/document-types (GET)
document_types.manageCreate/update/delete document types/api/admin/document-types (POST/PUT/DELETE)
trade_types.viewRead trade types/api/admin/trade-types (GET)
trade_types.manageCreate/update/delete trade types/api/admin/trade-types (POST/PUT/DELETE)
subcontractor_categories.viewRead subcontractor categories/api/admin/subcontractor-categories (GET)
subcontractor_categories.manageCreate/update/delete subcontractor categories/api/admin/subcontractor-categories (POST/PUT/DELETE)
project_templates.viewRead project templates/api/admin/project-templates (GET)
project_templates.manageCreate/update/delete project templates/api/admin/project-templates (POST/PUT/DELETE)
settings.viewView signature settings/api/admin/signature-settings (GET)
settings.manageUpdate signature settings/api/admin/signature-settings (PUT)
manage_rolesCreate and view roles/api/admin/roles (GET/POST)
ADM-013

Error Reference

HTTP codeError field valueCause
401UnauthenticatedToken missing, expired, or invalid
403Forbiddenuser_type not admin/staff, or missing permission slug
404Not foundRecord ID does not exist or belongs to another business
422Validation failedRequest body fails validation; data contains field-level errors
500Server errorUnexpected exception; check Laravel logs

Validation error shape (422):

{ "success": false, "msg": "Validation failed", "data": { "errors": { "name": ["The name field is required."], "project_id": ["The selected project id is invalid."] } } }