System Overview
Engineering Services is a multi-tenant SaaS platform for construction businesses. Each tenant is a Business record with its own isolated data. The platform has five user-facing portals separated by user_type, each with its own route prefix and permission model.
Portal Architecture
| user_type | Portal | Route prefix | Capabilities summary |
|---|---|---|---|
| super_admin | Platform Admin | /api/platform/ | Manage all businesses, subscription plans, audit logs across all tenants |
| admin | Admin | /api/admin/ | Full access to own business — projects, people, finance, config. Bypasses all permission checks. |
| staff | Admin | /api/admin/ | Same routes as admin but gated by role permissions. Must be assigned a custom role. |
| employee | Employee | /api/employee/ | Tasks, punch clock, site visits, calendar view, project gallery |
| contractor | Contractor | /api/contractor/ | Daily logs, expenses, materials, wage vouchers, ledger view, e-signature |
| client | Client | /api/client/ | Project progress, document centre, gallery, support tickets, payment ledger |
Tenant Isolation
Every model that belongs to a business has a business_id column. The AuthorizesEngineeringAccess concern calls businessIdOrFail() which reads auth()->user()->business_id and scopes all queries via a forBusiness($id) local scope. This means a staff user from Business A can never read, write, or even enumerate records belonging to Business B — the scoping happens at the ORM level before any controller logic runs.
forBusiness() scope is on the model, not the controller. Even if a controller bug skipped the permission check, the ORM scope would still prevent cross-tenant data access.Authentication & Business Portal Access
All portals use the same POST /api/login endpoint. After login the app reads user_type from the response and routes accordingly. The bearer token is a Laravel Sanctum personal access token — stateless, no cookies.
Login & Routing User Flow
username, not email.POST /api/login — Auth guard verifies credentials, issues a Sanctum personal access token, returns user record including user_type, business_id, and name.user_type: if admin or staff → navigate to Admin portal. Other values route to their respective portals.Authorization: Bearer {token} header. No session cookie is used./api/admin/* request, rejectUnlessBusinessPortal() checks user_type IN (admin, staff). Returns 403 if the token belongs to an employee, client, or contractor.businessIdOrFail() resolves business_id from auth user. Injects it into every ORM query via the forBusiness() scope.staff users: rejectUnlessPermission(slug) checks if the user's role includes the required permission slug. admin users bypass this check entirely.| Concern method | When it runs | Failure response |
|---|---|---|
rejectUnlessBusinessPortal() | Every /api/admin/* route, first check | 403 — user_type not admin or staff |
businessIdOrFail() | After portal check; scopes all queries | 403 — user has no business_id |
rejectUnlessPermission(slug) | Resource-level, after portal check | 403 — staff missing this permission slug |
rejectUnlessSuperAdmin() | /api/platform/* only — not used in Admin module | 403 |
admin user has all permissions implicitly and is never blocked by rejectUnlessPermission(). A staff user with no role assigned will be denied on every protected endpoint. Always assign a role immediately after creating a staff account.Password Reset Flow
POST /api/password/send — User submits username/email. API sends OTP to registered contact.POST /api/password/verify — User submits OTP. API validates it and returns a short-lived reset token.POST /api/password/reset — User submits new password + reset token. API updates the password and invalidates all existing Sanctum tokens.Setup Flow & Dependency Order
Before a business can create projects or assign staff, several reference tables must be populated. Skipping this order causes foreign key failures and empty dropdowns in the mobile app. The diagram below shows which records depend on others.
location_id. Example: "Karachi Office", "Lahore Site B". Fields: name (req), address, contact_person, contact_phone, is_active.name (req), code, is_active.requires_approval flag controls whether uploaded documents go into an approval queue before becoming visible to clients. Fields: name (req), category, requires_approval (bool), is_active.name (req), code, is_active.phase_key (slug, e.g. foundation), a name, and a default_duration_days used to auto-calculate planned end dates when applying a template. Fields: phase_key (req, unique slug), category (req, one of: civil_structural | excavation | mep_rough_in | mep_final | architectural | finishing | handover | post_handover), name (req), default_duration_days, sort_order, is_active.sort_order controls sequence.phase_keys[] that can be applied when creating a new project to auto-populate its phase list. Example: "Residential Standard" = [foundation, structural, roofing, plumbing, electrical, finishing]. Fields: name (req), description, phase_keys (array of strings), is_active.user_type=staff. Fields: user_id (req), location_id, job_title, department, status.user_id) to grant Contractor portal access. Without a linked user, the subcontractor is a data record only — usable in vouchers and ledger but unable to log in. Fields: company_name (req), user_id (optional), trade_type, contact_email, contact_phone, status.Project Lifecycle
A Project is the primary business entity. All financial records, phases, staff assignments, documents, calendar events, and subcontractor work are linked to a project_id. Understanding the status lifecycle is critical because some transitions are irreversible.
Status State Machine
| Status | Meaning | Allowed next | Terminal? |
|---|---|---|---|
| proposal | Default status on creation. Being quoted/negotiated with the client — no active work or finance yet. | active | No |
| active | Proposal accepted; work in progress; phases, vouchers, ledger all active | on_hold, completed, cancelled | No |
| on_hold | Temporarily paused; no new work expected | active, cancelled | No |
| completed | All phases done; client handover complete. No further mutations expected. | — | Yes |
| cancelled | Abandoned. Financial records preserved for accounting. | — | Yes |
Project Entity Relationships
All of the following records are linked to a project via project_id. When building the Project Detail screen, these sub-routes are used:
| Sub-entity | Route | Purpose | Key fields |
|---|---|---|---|
| ProjectPhase | GET /api/admin/projects/{id}/phases | Ordered list of phases, each with progress_percent and status | phase_key, name, status, progress_percent, planned_start, planned_end, sort_order |
| Proposal | GET /api/admin/projects/{id}/proposals | All bidding rounds for this project | title, status, quoted_amount, current_round |
| Voucher | GET /api/admin/projects/{id}/vouchers | All subcontractor payment claims on this project | voucher_no, subcontractor_id, amount, week_ending, status |
| LedgerEntry | GET /api/admin/projects/{id}/ledgers | All formal financial transactions, ordered by entry_date desc | entry_type, debit, credit, party_type, party_id, reference, entry_date |
| Revisions | GET /api/admin/projects/{id}/revisions | Contract revisions (revised value) and drawing revisions (version-controlled drawings) | revision_number, status, contract_value / drawing_code, file_path |
| MaterialStock | GET /api/admin/projects/{id}/materials | Inventory of materials on-site for this project | item_name, quantity, unit, received_date |
| DailyLog | GET /api/admin/projects/{id}/logs | Contractor daily site reports, ordered by log_date desc | log_date, work_summary, workers_count, project_id |
Project Financial Summary (Dashboard Card Formula)
Creating a Project — User Flow
POST /api/admin/projects — provide name, location_id, client_user_id, contract_value, start_date, expected_end_date, address. If status is omitted the column defaults to proposal — the correct value while the deal is still being quoted.GET /api/admin/project-templates → pick template → POST /api/admin/phases for each phase_key in the template. Use default_duration_days from Phase Library to calculate planned_end = planned_start + default_duration_days. Alternatively, pass template_id in the project creation body (POST /api/admin/projects) to have the API auto-populate phases in one call.POST /api/admin/proposals with project_id, title, quoted_amount, status: draft.status: in_review. Client reviews (see ADM-005).status: accepted and update project contract_value to match quoted_amount. Move project status to active.progress_percent as work progresses. Record subcontractor vouchers and post matching ledger entries. Create calendar events for inspections/milestones.status: completed, set actual_end_date. Run final ledger reconciliation.Proposals & Bidding Flow
A Proposal is a formal price quotation attached to a project. The system supports multiple bidding rounds on the same project — each revision is tracked with a current_round counter and historical rounds are preserved via ProposalRound records. Multiple proposals can exist per project to support re-quoting after scope changes.
proposal_rounds (full CRUD at /api/admin/proposal-rounds). Team shows staff members assigned to prepare and negotiate this proposal via proposal_members (/api/admin/proposal-members — create/delete). Notes shows internal team notes not visible to the client via proposal_notes (/api/admin/proposal-notes — full CRUD). Messages is a logged communication record of inbound/outbound messages with the client via proposal_messages (/api/admin/proposal-messages — create/list). A status-timeline stepper at the top of the detail page mirrors the 7 canonical proposal stages.Proposal Status Transitions
Proposal.status is a free string, but the Admin Portal's status picker is driven by the seeded proposal_stages lookup (GET /api/common/proposal-stages) — always use these 7 canonical values so filtering and reporting stay meaningful:
| Status | sort_order | Meaning | Who acts |
|---|---|---|---|
| draft | 1 | Prepared internally, not yet sent to client | Admin |
| scheduled | 2 | A client walkthrough/presentation has been scheduled to present the proposal | Admin |
| in_review | 3 | Sent to client, awaiting their decision | Admin updates; client reviews externally |
| revision_requested | 4 | Client asked for changes (price, scope, materials). Triggers a new ProposalRound. | Admin records outcome |
| accepted | 5 | Client agreed to the final terms. Project contract_value should be updated to match quoted_amount. | Admin records outcome |
| deposit_paid | 6 | Client has paid the initial deposit/advance — post a matching LedgerEntry (entry_type: advance) | Admin records outcome |
| converted | 7 | Proposal is fully converted into active execution — terminal state | Admin records outcome |
in_review or revision_requested) and the project itself is moved to cancelled if the deal falls through entirely (see ADM-004).ProposalRound — Per-Round History (Full CRUD)
ProposalRound records the amount, status, and notes of each individual bidding round on a proposal — the audit trail behind the parent Proposal's single current_round/quoted_amount fields. Standard CRUD is exposed at /api/admin/proposal-rounds (permissions: proposals.view / proposals.manage — same slugs as the parent Proposal resource, no separate permission).
| Field | Type | Notes |
|---|---|---|
proposal_id | integer (FK) | Required on create |
proposal_title | string (derived) | Denormalized from proposal.title via eager-loaded relation — read-only, API-computed, never accept on write |
round_number | integer | Required, ≥1. Should match the parent Proposal's current_round once this round is the live one. |
amount | decimal | The quoted figure for this specific round |
status | string | Round-level vocabulary: pending (default) → accepted | superseded. Distinct from Proposal.status above — don't confuse round-level and proposal-level state. |
notes | text (nullable) | Free-text — what changed this round, why |
submitted_at | datetime (nullable) | When this round's figure was shared with the client |
Multi-Round Bidding User Flow
POST /api/admin/proposals — Create Proposal with current_round: 1, quoted_amount: 2500000, status: draft. POST /api/admin/proposal-rounds — Round 1, amount: 2500000, status: pending.PUT /api/admin/proposals/{id} — Set status: in_review. Share proposal document with client outside the system (PDF, email, etc.).PUT /api/admin/proposals/{id} — Set status: revision_requested. PUT /api/admin/proposal-rounds/{round1_id} — Round 1 status: superseded. Then PUT /api/admin/proposals/{id} — current_round: 2, quoted_amount: 2350000. POST /api/admin/proposal-rounds — Round 2, amount: 2350000, status: pending.PUT /api/admin/proposal-rounds/{round2_id} — status: accepted. PUT /api/admin/proposals/{id} — status: accepted. Then PUT /api/admin/projects/{project_id} — Update contract_value: 2350000 and status: active.status: deposit_paid, post a LedgerEntry (entry_type: advance). Once execution formally begins → proposal status: converted (terminal).PUT /api/admin/projects/{id} with the new contract_value. This separation allows the admin to override the contract value independently if needed (e.g. approved in-principle but price confirmed later).Phase Management & Progress Tracking
Phases are the execution backbone of a project. Each phase has its own status lifecycle, a progress_percent (0–100), and optional planned date range. Phases are ordered by sort_order and displayed sequentially in the mobile app's Project Detail → Phases tab.
Phase Status Transitions
| Field | Type | Rules |
|---|---|---|
phase_key | string slug | Must match a phase_library.phase_key for this business. Cannot be changed after creation. |
progress_percent | integer 0–100 | 0 when pending; 1–99 when in_progress; 100 forces status to completed. |
status | enum | Set manually or auto-derived: progress 100 → completed; progress 1–99 → in_progress. |
planned_start | date | When work is expected to begin. Auto-calculated from template: project.start_date + sum of previous phases' default_duration_days. |
planned_end | date | planned_start + phase_library.default_duration_days for this phase_key. |
sort_order | integer | Display order in app. Phases rendered ascending by sort_order. |
Overall Project Progress Formula
Applying a Template — Step-by-Step
GET /api/admin/project-templates — fetch templates list. User selects one on the Create Project form.phase_key in template.phase_keys (in sort order), call GET /api/admin/phase-library to retrieve name, default_duration_days for each key.POST /api/admin/phases for each phase with project_id, phase_key, name, planned_start, planned_end, sort_order (0-indexed increment), status: "pending", progress_percent: 0.current_phase_key to the first phase_key (or update it as phases complete).Financial Instruments
The Admin module has four interlocking financial objects: Vouchers, Ledger Entries, Variation Orders, and Daywork Orders. Each serves a distinct purpose. Getting them confused leads to double-counting and wrong budget figures.
Instrument Summary
| Instrument | What it represents | Creates a ledger entry? | Key required fields |
|---|---|---|---|
| Voucher | A subcontractor's weekly work claim — "I did this work, pay me PKR X" | No — manual step | voucher_no (req), subcontractor_id, project_id, amount, week_ending, status |
| LedgerEntry | The formal financial record of actual cash movement or obligation | Is itself a ledger entry | entry_type, debit OR credit, party_type, party_id, project_id, reference, entry_date |
| VariationOrder | Extra scope beyond the original contract, with a cost impact | No — creates VO exposure only | vo_number (req), title (req), project_id (req), amount, status |
| DayworkOrder | Labour or equipment engaged on a day/hour rate, outside original scope | No — creates DWO exposure only | dwo_number (req), title (req), project_id (req), amount, work_date, status |
Voucher Lifecycle
POST /api/admin/vouchers — Create voucher with voucher_no (e.g. VCH-2026-0008), subcontractor_id, project_id, amount, week_ending date, status: draft. voucher_type is typically subcontractor.status: submitted.status: approved.POST /api/admin/ledgers with entry_type: payment, credit: {voucher_amount}, debit: 0, reference: {voucher_no}, party_type: subcontractor, party_id: {subcontractor_id}.Ledger Entry Types & Double-Entry Convention
The ledger uses a simplified double-entry model where debit records money received/recovered into the business and credit records money paid out or owed by the business.
| entry_type | Direction | debit | credit | Example |
|---|---|---|---|---|
| payment | Money out | 0 | amount | Paying subcontractor for completed work |
| advance | Money out (advance) | 0 | amount | Advance payment before work starts |
| deduction | Money recovered | amount | 0 | Retention, penalty, or damage recovery |
| refund | Money returned | amount | 0 | Subcontractor returns unused advance |
| client_receipt | Money in from client | amount | 0 | Client pays invoice for completed phase |
Variation Order (V.O.) Lifecycle
A Variation Order represents extra work beyond the original contract scope — e.g. client requests an extra floor or design change. VOs increase the project's financial exposure.
POST /api/admin/variation-orders — Create VO with vo_number (e.g. VO-2026-0003), title, description, amount, project_id, status: pending.PUT /api/admin/variation-orders/{id} with status: approved. The approved VO amount is now added to the project's total liability. Update contract_value on the project accordingly.Daywork Order (D.W.O.) Lifecycle
A Daywork Order covers labour or equipment engaged on a time-and-materials or day-rate basis — typically for unforeseen work discovered during a project. Different from a VO: a VO is a scope change; a DWO is a time-based claim for unplanned labour.
| DWO field | Required | Notes |
|---|---|---|
dwo_number | Yes | Unique reference, e.g. DWO-2026-0001 |
title | Yes | Brief description of day work, e.g. "Emergency drainage clearing" |
description | No | Full detail of labour and equipment used |
work_date | No | Date the daywork was performed |
amount | No | Agreed day rate × days, or lump sum |
status | No | draft → submitted → approved |
Full Financial Position Formula
People Management
The Admin module manages two distinct types of people: Staff (internal employees who use the Admin portal) and Subcontractors (external companies who optionally use the Contractor portal). Both are business-scoped and rely on underlying platform user accounts.
Staff — Internal Employees
A Staff record is a StaffProfile that links a platform user account (user_type = staff) to a business. Once linked, the user can log in and access the Admin portal with the permissions their assigned role grants.
| Field | Required | Notes |
|---|---|---|
user_id | Yes | Must reference an existing platform user with user_type = staff. The platform admin creates user accounts; the business admin links them via StaffProfile. |
location_id | No | Which office/site the staff member is based at. Shown in Staff directory and useful for filtering. |
job_title | No | Free text — "Site Engineer", "Finance Officer", "Project Manager" |
department | No | Group classification — "Engineering", "Finance", "Admin". Used for filtering in Staff list. |
status | No | active / inactive / suspended. Inactive staff cannot log in. |
Staff Onboarding Flow
user_type: staff, username, and password via Platform Admin portal.POST /api/admin/staff — Link the new user_id to this business with job_title, department, location_id.PUT /api/admin/staff/{id} updating the user's role — or create a role first if none exists (see ADM-011).POST /api/login with their username. The app reads user_type: staff and routes to the Admin portal. The staff member sees only what their role permissions allow.Subcontractors — External Companies
Subcontractor records represent external firms. They are referenced in Vouchers (subcontractor_id) and LedgerEntries (party_id when party_type = subcontractor). Optionally, a subcontractor can be linked to a platform user account to grant Contractor portal access.
| Field | Required | Notes |
|---|---|---|
company_name | Yes | Legal/trading name of the subcontracting firm |
user_id | No | If set: the linked user gets access to the Contractor portal, where they can view their own vouchers, ledger statement, and daily log history. |
trade_type | No | Must match a trade_type.name in this business's Trade Types list (see ADM-003) |
contact_email | No | Primary contact for the company — for external communication only |
contact_phone | No | Phone number for site coordination |
status | No | active / inactive |
Contractor Portal Access Flow
user_type: contractor for the subcontractor's on-site representative.PUT /api/admin/subcontractors/{id} — Set user_id to the newly created contractor user's ID.POST /api/login. The app reads user_type: contractor and routes to the Contractor portal. They can see only records where their subcontractor_id matches.Calendar & Scheduling
Calendar events are the scheduling backbone across all portals. They link to projects and optionally to specific users. The Admin creates events; Employees see events assigned to them; Clients can see milestone events when portal visibility rules allow.
Event Fields
| Field | Required | Notes |
|---|---|---|
title | Yes | Short description: "Foundation inspection", "Client progress review" |
starts_at | Yes | ISO datetime. Used for dashboard KPI: upcoming_event_count = events WHERE starts_at >= now() |
ends_at | No | Optional. Single-point events (inspections, meetings) have no ends_at. Multi-day events use both. |
project_id | No | Links event to a project. Allows filtering by project in both Admin and Employee portals. |
assigned_user_id | No | The specific staff member or employee responsible. If set, event appears in Employee portal calendar. |
event_type | No | inspection / meeting / milestone / site_visit / other |
description | No | Full detail — agenda, participants, notes |
Event Types & Cross-Portal Visibility
| event_type | Typical use | Admin sees | Employee sees | Client sees |
|---|---|---|---|---|
| inspection | Official engineer or authority inspection | Yes | If assigned | No |
| meeting | Progress or coordination meeting | Yes | If assigned | No |
| milestone | Phase completion or handover | Yes | If assigned | Yes — visible in Progress Chart |
| site_visit | Admin or client site observation | Yes | If assigned | No |
| other | Any uncategorised event | Yes | If assigned | No |
Calendar Event User Flow
POST /api/admin/calendar — Create event with title, project_id, starts_at, event_type. Optionally assign to a staff user via assigned_user_id.GET /api/employee/calendar — returns events where assigned_user_id = auth user. Event appears in their calendar with project context.upcoming_event_count recalculates on each dashboard load as COUNT(events WHERE starts_at >= now()).starts_at >= now(). It is not filtered by assigned_user — it is a business-wide figure.Document Control & Signature Settings
Document Upload & Approval Workflow
Project documents are typed via document_type_id (see ADM-003). Document Types with requires_approval: true trigger an approval workflow before the document becomes visible to the client portal.
POST /api/uploads — Upload the file to the staging endpoint. Returns a file_path (temporary staged URL or storage path).POST /api/admin/... with file_path from step 1 and document_type_id. Document is stored with status: pending if type.requires_approval = true; otherwise status: approved immediately.status = 'pending'. Inspect document, verify it is the correct version and type.PUT document with status: approved. Document is now visible in the Client portal's Document Centre.PUT document with status: rejected. Uploader should be notified externally and re-upload the corrected document.| Document status | Client portal visible? | Action |
|---|---|---|
| pending | No | Awaiting admin approval |
| approved | Yes | Visible in Document Centre |
| rejected | No | Needs re-upload |
Signature Settings
Each business has a single SignatureSetting record used on generated PDF documents (vouchers, certificates). The record is upserted — if it doesn't exist, the first PUT creates it; subsequent PUTs update it.
| Field | Purpose |
|---|---|
signature_image_path | URL/path to PNG of the authorised signatory's signature. Rendered on PDFs. |
signer_name | Full name displayed under the signature on documents |
signer_title | Job title displayed under the name (e.g. "Managing Director", "Site Engineer") |
Roles & Permissions
The Roles system allows the business admin to define permission sets and assign them to staff members. The admin user_type has all permissions by default. staff users must be explicitly given a role or they will be denied on every protected endpoint.
System vs Custom Roles
| Role | Type | Who has it | Editable? | Permissions |
|---|---|---|---|---|
| admin | System | Users with user_type = admin | No | All — implicit bypass of all permission checks |
| staff | System base | Default for user_type = staff with no custom role | No | None — staff with only the base role are denied everywhere |
| Custom roles | Business-defined | Assigned to individual staff members | Yes — can edit permissions, not the slug | Any subset of available permission slugs |
Creating and Assigning a Role — User Flow
GET /api/admin/permissions — Fetch the full list of available permission slugs (see ADM-012). Review what capabilities are needed for this role type.POST /api/admin/roles with name (e.g. "Site Supervisor"). The slug is auto-generated from the name (site-supervisor). Do not include permissions here — assign them in the next step.POST /api/admin/roles/{id}/permissions — Sync permissions to this role. Provide permissions: ["projects.view", "phases.view", "calendar.view", "calendar.manage"] as an array. This is a SYNC operation — it replaces the full permission set, not appends.PUT /api/admin/staff/{id} with the role_id of the custom role. The staff member immediately gains the new permissions on their next API call.POST /api/admin/roles/{id}/permissions again with the full desired permission array. Changes take effect immediately — no logout required.Recommended Role Templates
| Role name | Typical for | Suggested permissions |
|---|---|---|
| Site Supervisor | Field engineers, supervisors | projects.view, phases.view, phases.manage, calendar.view, calendar.manage, staff.view |
| Finance Officer | Accountants, finance staff | projects.view, vouchers.view, vouchers.manage, ledgers.view, ledgers.manage, variation_orders.view, daywork_orders.view |
| Project Manager | Project managers | projects.view, projects.manage, proposals.view, proposals.manage, phases.view, phases.manage, calendar.view, calendar.manage, staff.view, subcontractors.view |
| HR Admin | HR and people management | staff.view, staff.manage, subcontractors.view, subcontractors.manage, locations.view |
| Read-Only | Directors, auditors | dashboard.view, projects.view, proposals.view, phases.view, vouchers.view, ledgers.view, variation_orders.view, calendar.view, staff.view |
Permission Matrix
Full list of all permission slugs. Admin users have all implicitly. Staff must be granted them via a custom role.
| Slug | Grants | Endpoints |
|---|---|---|
| dashboard.view | View KPI dashboard | GET /api/admin/dashboard |
| projects.view | Read projects and all sub-routes | GET /api/admin/projects, /api/admin/projects/{id}/* |
| projects.manage | Create, update, delete projects | POST/PUT/DELETE /api/admin/projects |
| proposals.view | Read proposals | GET /api/admin/proposals |
| proposals.manage | Create/update/delete proposals | POST/PUT/DELETE /api/admin/proposals |
| phases.view | Read project phases | GET /api/admin/phases |
| phases.manage | Create/update/delete phases | POST/PUT/DELETE /api/admin/phases |
| locations.view | Read locations | GET /api/admin/locations |
| locations.manage | Create/update/delete locations | POST/PUT/DELETE /api/admin/locations |
| staff.view | Read staff profiles | GET /api/admin/staff |
| staff.manage | Create/update/delete staff | POST/PUT/DELETE /api/admin/staff |
| subcontractors.view | Read subcontractor records | GET /api/admin/subcontractors |
| subcontractors.manage | Create/update/delete subcontractors | POST/PUT/DELETE /api/admin/subcontractors |
| vouchers.view | Read payment vouchers | GET /api/admin/vouchers |
| vouchers.manage | Create/update/delete vouchers | POST/PUT/DELETE /api/admin/vouchers |
| ledgers.view | Read ledger entries | GET /api/admin/ledgers |
| ledgers.manage | Create/update/delete ledger entries | POST/PUT/DELETE /api/admin/ledgers |
| variation_orders.view | Read VOs | GET /api/admin/variation-orders |
| variation_orders.manage | Create/update/delete VOs | POST/PUT/DELETE /api/admin/variation-orders |
| daywork_orders.view | Read DWOs | GET /api/admin/daywork-orders |
| daywork_orders.manage | Create/update/delete DWOs | POST/PUT/DELETE /api/admin/daywork-orders |
| calendar.view | Read calendar events | GET /api/admin/calendar |
| calendar.manage | Create/update/delete events | POST/PUT/DELETE /api/admin/calendar |
| phase_library.view | Read phase library | GET /api/admin/phase-library |
| phase_library.manage | Create/update/delete phase library | POST/PUT/DELETE /api/admin/phase-library |
| document_types.view | Read document types | GET /api/admin/document-types |
| document_types.manage | Create/update/delete document types | POST/PUT/DELETE /api/admin/document-types |
| trade_types.view | Read trade types | GET /api/admin/trade-types |
| trade_types.manage | Create/update/delete trade types | POST/PUT/DELETE /api/admin/trade-types |
| subcontractor_categories.view | Read sub. categories | GET /api/admin/subcontractor-categories |
| subcontractor_categories.manage | Create/update/delete | POST/PUT/DELETE /api/admin/subcontractor-categories |
| project_templates.view | Read project templates | GET /api/admin/project-templates |
| project_templates.manage | Create/update/delete templates | POST/PUT/DELETE /api/admin/project-templates |
| settings.view | View signature settings | GET /api/admin/signature-settings |
| settings.manage | Update signature settings | PUT /api/admin/signature-settings |
| manage_roles | Create roles and sync permissions | GET/POST /api/admin/roles, POST /api/admin/roles/{id}/permissions |
End-to-End Project Journey
This walkthrough traces a complete construction project end to end — from business onboarding and the initial proposal, through drawing sketches, negotiation, phased execution, and financial close, all the way to the final client walkthrough and key handover. Every step names the exact status value and API call involved, so it can be cross-checked against ADM-004, ADM-005, and the mockups.
Scenario: Residential Block A — PKR 2,500,000 contract
POST /api/admin/projects — name "Residential Block A", location Karachi, client_user_id, contract_value 2,500,000, start_date 2026-07-01. Status is omitted and defaults to proposal — no contract exists yet.DrawingRevision record is logged for drawing_code STR-001, revision_number 1, status submitted. Client reviews the sketch, requests a column-spacing change; a second internal revision is prepared. Once signed off, the record is updated to revision_number 2, status approved, file_path pointing at the final PDF. This history is visible read-only via GET /api/admin/projects/{id}/revisions (drawings array) — see the warning below on write access.status: completed plus the signed completion certificate from step 15 and, optionally, final walkthrough photos posted to GalleryItem (visibility: client). Project is now archived; financial and document records are retained.DrawingRevision and ContractRevision records (step 9) are currently created at the data layer only — GET /api/admin/projects/{id}/revisions is the sole endpoint, and it is read-only. Treat the drawing narrative above as the intended business workflow; a write endpoint is a known gap, not something to call today.Error Reference
| HTTP | Meaning | Common cause | Fix |
|---|---|---|---|
| 401 | Unauthenticated | Token missing, expired, or revoked | Re-login, obtain a new token |
| 403 — portal | Wrong user_type | Contractor/client trying to call /api/admin/* | Ensure user_type = admin or staff |
| 403 — permission | Staff missing slug | Staff user has no role or role lacks the required slug | Assign or update role via ADM-011 |
| 404 | Not found | Record ID doesn't exist or belongs to a different business_id | Verify the ID; check you're using the correct environment (localhost vs live) |
| 422 | Validation failed | Missing required field, wrong type, or constraint violation | Read the data.errors object for field-level messages |
| 500 | Server error | Unexpected exception in controller or model | Check Laravel logs; report with request ID |
Notifications & Audit Logs
The t_app_notifications table is a shared in-app notification store. Every row is scoped to a user_id, so each portal user sees only their own rows. Notifications are created by AppNotificationService and written by business operations (project creation, phase updates, voucher approvals, etc.).
Two Separate Concerns
| Purpose | Controller | Who can access | Endpoint pattern |
|---|---|---|---|
| Bell dropdown | AdminNotificationController | Admin and staff | GET /api/admin/notifications — max 20 unread, no paginationPATCH /api/admin/notifications/read-all |
| Audit log screen | AdminAuditLogController | Admin only — staff get 403 | GET /api/admin/audit-logs (paginated, filters)+ show / markRead / markAllRead / destroy |
Notification Shape (t_app_notifications row)
| Field | Type | Notes |
|---|---|---|
id | int | Primary key |
user_id | int FK → t_users | Recipient — always the authenticated user's own rows |
type | string(100) | Machine-readable event type, e.g. project_created, phase_updated, voucher_approved |
title | string(255) | Short human-readable title shown in bell and audit list |
body | text | Full notification message |
data | JSON / null | Contextual payload (e.g. {"project_id": 5}) for deep-link navigation |
is_read | boolean | Default false; indexed together with user_id |
created_at | timestamp | When the notification was created |
Bell Endpoint Rules
- Returns ≤ 20 most recent rows where
is_read = falsefor the authenticated user. - No
paginationkey in the response — intentional, this is a lightweight dropdown feed. - Available to both
adminandstaffuser types.
Audit Log Screen Rules (Admin Only)
AdminAccess::hasFullPermissions($user->user_type)gate — staff are rejected with 403.- Supports filters:
type,is_read(0/1/true/false), and full-textsearchacross title, body, type, and numeric id. - Paginated — default 15 per page. Ordered newest first (
orderByDesc('id')). - Scoped to
WHERE user_id = Auth::id()— admin sees only their own notification history, not all business users. - Hard delete is permanent — no soft delete. Only delete confirmed noise records.
Writing Notifications
Use AppNotificationService::createForAdmins(type, title, body, data) to fan out a notification to all admin + staff users in the system. For future portal-specific events, add createForEmployee(), createForClient(), createForContractor() static methods following the same pattern.
Common Lookups Overview
Common lookups provide non-paginated key-value lists for selecting entities in dropdowns across the mobile and web applications. All lookups return the standard envelope {success: true, msg: string, data: Array} where each row contains at least id (always as a string) and name (normalized string, never null).
Scoping Rules
- [BIZ]: Dynamically filtered by the authenticated user's
business_id. If the user has no business, returns an empty array. - [PLATFORM]: Query execution across platform-level data (e.g. subscription plans). No tenant restriction.
- [GLOBAL]: Static reference lookup data populated via seeds. Accessible by all tenants.
BIZ Scoped Lookup - Projects
Endpoint: GET /api/common/projects. Scoped to the authenticated business. Supports filtering by status and name text search. Returns id, name, status, and location name.
BIZ Scoped Lookup - Locations
Endpoint: GET /api/common/locations. Scoped to the authenticated business. Supports name search. Returns id, name, and address.
BIZ Scoped Lookup - Staff
Endpoint: GET /api/common/staff. Scoped to the authenticated business. Supports name search. Returns id, name (combining display_name/username), and trade_type.
BIZ Scoped Lookup - Subcontractors
Endpoint: GET /api/common/subcontractors. Scoped to the authenticated business. Supports category_id filter and name search. Returns id, name, category name, and trade type.
BIZ Scoped Lookup - Subcontractor Categories
Endpoint: GET /api/common/subcontractor-categories. Scoped to the authenticated business. Returns id and name.
BIZ Scoped Lookup - Project Templates
Endpoint: GET /api/common/project-templates. Scoped to the authenticated business. Returns id and name.
BIZ Scoped Lookup - Project Phases
Endpoint: GET /api/common/phases. Requires project_id param. Returns all phases registered under the project, including phase_key and status.
BIZ Scoped Lookup - Material Stocks
Endpoint: GET /api/common/materials. Requires project_id param. Returns material stocks lookup showing units.
BIZ Scoped Lookup - Roles
Endpoint: GET /api/common/roles. Scoped to the business_id or global roles. Returns id and name.
PLATFORM Lookup - Plan Tiers
Endpoint: GET /api/common/plan-tiers. Lists all distinct, active subscription plan names.
PLATFORM Lookup - Subscription Plans
Endpoint: GET /api/common/subscription-plans. Lists active subscription plans including price suffix (e.g. "Basic ($49/mo)").
GLOBAL Lookup - Project Types
Endpoint: GET /api/common/project-types. Seeded options: Residential, Commercial, Renovation, Industrial.
GLOBAL Lookup - Expense Categories
Endpoint: GET /api/common/expense-categories. Seeded options: Labor, Equipment, Material, Misc.
GLOBAL Lookup - Advance Sources
Endpoint: GET /api/common/advance-sources. Seeded options: Client, Company.
GLOBAL Lookup - Daywork Expense Policies
Endpoint: GET /api/common/daywork-expense-policies. Seeded options: Client pays all, Client pays wages only, Client pays wages & company covers expenses, Client pays wages & split 50/50.
GLOBAL Lookup - Proposal Packages
Endpoint: GET /api/common/proposal-packages. Seeded options: Basic, Standard, Pro.
GLOBAL Lookup - Proposal Stages
Endpoint: GET /api/common/proposal-stages. Seeded options: Draft, Scheduled, In Review, Revision Requested, Accepted, Deposit Paid, Converted.
GLOBAL Lookup - Voucher Statuses
Endpoint: GET /api/common/voucher-statuses. Seeded options: Draft, Submitted, Approved, Paid, Rejected.
GLOBAL Lookup - Phase Statuses
Endpoint: GET /api/common/phase-statuses. Seeded options: Not Started, In Progress, Completed, On Hold.
GLOBAL Lookup - Task Statuses
Endpoint: GET /api/common/task-statuses. Seeded options: In Progress, Completed, Skipped.
GLOBAL Lookup - Theme Colors
Endpoint: GET /api/common/theme-colors. Returns theme colors (Amber, Harbor, Sage, Graphite, Concrete, Copper, Slate) including hex_value.
GLOBAL Lookup - Theme Modes
Endpoint: GET /api/common/theme-modes. Seeded options: Dark Mode, Light Mode, System Preference.
GLOBAL Lookup - Languages
Endpoint: GET /api/common/languages. Seeded options: English (code: en), Urdu (code: ur).