Business Admin · Business Guide 14 sections — reference by ADM-XXX code
ADM-001

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

Platform Admin /api/platform/* super_admin Admin /api/admin/* admin / staff Employee /api/employee/* employee Client /api/client/* client Contractor /api/contractor/* contractor Business DB (tenant-scoped) business_id on every query
user_typePortalRoute prefixCapabilities summary
super_adminPlatform Admin/api/platform/Manage all businesses, subscription plans, audit logs across all tenants
adminAdmin/api/admin/Full access to own business — projects, people, finance, config. Bypasses all permission checks.
staffAdmin/api/admin/Same routes as admin but gated by role permissions. Must be assigned a custom role.
employeeEmployee/api/employee/Tasks, punch clock, site visits, calendar view, project gallery
contractorContractor/api/contractor/Daily logs, expenses, materials, wage vouchers, ledger view, e-signature
clientClient/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.

ⓘ The 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.
ADM-002

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

1AppUser enters username + password on login screen and taps Sign In. Note: field is username, not email.
2APIPOST /api/login — Auth guard verifies credentials, issues a Sanctum personal access token, returns user record including user_type, business_id, and name.
3AppStores token in secure storage. Reads user_type: if admin or staff → navigate to Admin portal. Other values route to their respective portals.
4AppEvery subsequent request adds Authorization: Bearer {token} header. No session cookie is used.
5APIOn each /api/admin/* request, rejectUnlessBusinessPortal() checks user_type IN (admin, staff). Returns 403 if the token belongs to an employee, client, or contractor.
6APIbusinessIdOrFail() resolves business_id from auth user. Injects it into every ORM query via the forBusiness() scope.
7APIFor staff users: rejectUnlessPermission(slug) checks if the user's role includes the required permission slug. admin users bypass this check entirely.
Concern methodWhen it runsFailure response
rejectUnlessBusinessPortal()Every /api/admin/* route, first check403 — user_type not admin or staff
businessIdOrFail()After portal check; scopes all queries403 — user has no business_id
rejectUnlessPermission(slug)Resource-level, after portal check403 — staff missing this permission slug
rejectUnlessSuperAdmin()/api/platform/* only — not used in Admin module403
admin vs staff: An 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

1AppPOST /api/password/send — User submits username/email. API sends OTP to registered contact.
2AppPOST /api/password/verify — User submits OTP. API validates it and returns a short-lived reset token.
3AppPOST /api/password/reset — User submits new password + reset token. API updates the password and invalidates all existing Sanctum tokens.
ADM-003

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.

Step 1 Step 2-3 Step 4-6 Step 7-8 Step 9 — Go Locations /admin/locations Trade Types /admin/trade-types Document Types /admin/document-types Sub. Categories /admin/sub-categories Phase Library /admin/phase-library Proj. Templates /admin/proj-templates Staff Profiles /admin/staff Subcontractors /admin/subcontractors Projects /admin/projects
1Locations — Create office/site locations first. Every staff member and project must reference a location_id. Example: "Karachi Office", "Lahore Site B". Fields: name (req), address, contact_person, contact_phone, is_active.
2Trade Types — Skill categories for subcontractors (e.g. "Civil Works", "Electrical", "Plumbing"). Subcontractor records reference a trade_type by name. Fields: name (req), code, is_active.
3Document Types — Define the categories of documents that can be attached to projects (e.g. "As-Built Drawing", "Completion Certificate", "Inspection Report"). The 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.
4Subcontractor Categories — Company-level groupings for subcontracting firms (e.g. "Tier 1 Partner", "Specialist Sub"). Independent of Trade Types — a company can be in category "Tier 1" and have trade type "Electrical". Fields: name (req), code, is_active.
5Phase Library — The master list of reusable phase definitions for this business. Each entry has a unique 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.
Category is required. The category groups phases in the Phase Library list view and enables filtering. It does not affect API logic or phase ordering — sort_order controls sequence.
6Project Templates — Named bundles of 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.
7Staff Profiles — Link platform user accounts (user_type=staff) to the business. Requires a pre-existing user account with user_type=staff. Fields: user_id (req), location_id, job_title, department, status.
8Subcontractors — Register external companies. Optionally link to a user account (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.
9Ready — All reference data is in place. Create projects, assign phases from templates, link to clients, and begin financial tracking.
// phase_key format rule phase_key = Str::slug(name) // "Electrical Works" → "electrical-works" // Must be unique per business — used as foreign key in ProjectPhase // Cannot be changed after ProjectPhase records reference it // Convention: use lowercase hyphens, max 64 chars
Phase Library before Templates. Project Templates reference phase_key strings. If you create a template before the matching Phase Library entries exist, the template will store the keys but the mobile app dropdown will show no phase names when it tries to resolve them. Always populate Phase Library first.
ADM-004

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

proposal bidding stage active work in progress phases updated pause on_hold temporarily paused resume completed terminal → cancelled terminal →
StatusMeaningAllowed nextTerminal?
proposalDefault status on creation. Being quoted/negotiated with the client — no active work or finance yet.activeNo
activeProposal accepted; work in progress; phases, vouchers, ledger all activeon_hold, completed, cancelledNo
on_holdTemporarily paused; no new work expectedactive, cancelledNo
completedAll phases done; client handover complete. No further mutations expected.Yes
cancelledAbandoned. 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-entityRoutePurposeKey fields
ProjectPhaseGET /api/admin/projects/{id}/phasesOrdered list of phases, each with progress_percent and statusphase_key, name, status, progress_percent, planned_start, planned_end, sort_order
ProposalGET /api/admin/projects/{id}/proposalsAll bidding rounds for this projecttitle, status, quoted_amount, current_round
VoucherGET /api/admin/projects/{id}/vouchersAll subcontractor payment claims on this projectvoucher_no, subcontractor_id, amount, week_ending, status
LedgerEntryGET /api/admin/projects/{id}/ledgersAll formal financial transactions, ordered by entry_date descentry_type, debit, credit, party_type, party_id, reference, entry_date
RevisionsGET /api/admin/projects/{id}/revisionsContract revisions (revised value) and drawing revisions (version-controlled drawings)revision_number, status, contract_value / drawing_code, file_path
MaterialStockGET /api/admin/projects/{id}/materialsInventory of materials on-site for this projectitem_name, quantity, unit, received_date
DailyLogGET /api/admin/projects/{id}/logsContractor daily site reports, ordered by log_date desclog_date, work_summary, workers_count, project_id

Project Financial Summary (Dashboard Card Formula)

// Shown on Project Detail → Overview tab budget_consumed = SUM(ledger_entries.credit WHERE project_id = N AND entry_type = 'payment') advance_paid = SUM(ledger_entries.credit WHERE project_id = N AND entry_type = 'advance') deductions = SUM(ledger_entries.debit WHERE project_id = N AND entry_type = 'deduction') approved_vo_total = SUM(variation_orders.amount WHERE project_id = N AND status = 'approved') pending_vo_total = SUM(variation_orders.amount WHERE project_id = N AND status = 'pending') dwo_total = SUM(daywork_orders.amount WHERE project_id = N AND status IN ('draft','submitted')) total_liability = budget_consumed + advance_paid - deductions + approved_vo_total remaining_budget = contract_value - total_liability total_exposure = total_liability + pending_vo_total + dwo_total
Worked Example — PRJ-004 Residential Block A
Contract valuePKR 2,500,000
Payments (ledger credits)PKR 380,000
Advance paidPKR 50,000
Deductions (ledger debits)PKR 15,000
Approved VOsPKR 75,000
Pending VOs (exposure)PKR 40,000
total_liability = 380k + 50k − 15k + 75kPKR 490,000
remaining_budget = 2,500,000 − 490,000PKR 2,010,000
total_exposure = 490k + 40kPKR 530,000

Creating a Project — User Flow

1AdminPOST /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.
2Admin(Optional) Apply a template: 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.
3AdminCreate an initial Proposal: POST /api/admin/proposals with project_id, title, quoted_amount, status: draft.
4AdminSend proposal to client → update status: in_review. Client reviews (see ADM-005).
5AdminOn client acceptance: update proposal status: accepted and update project contract_value to match quoted_amount. Move project status to active.
6AdminWork begins. Update phase progress_percent as work progresses. Record subcontractor vouchers and post matching ledger entries. Create calendar events for inspections/milestones.
7AdminAll phases reach 100% → update project status: completed, set actual_end_date. Run final ledger reconciliation.
ADM-005

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 Detail — five tabs: the Proposal Detail screen (Screen 12, App Guide) is organised into five tabs that together cover the full bidding workflow. Overview shows the core proposal fields (title, status, quoted_amount, current_round, project link) plus a read-only reference to the linked project's drawing/contract revisions. Rounds shows per-round bid history via 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:

draft scheduled in_review accepted deposit_paid converted✓ terminal
in_review revision_requested→ new ProposalRound, back to in_review
Statussort_orderMeaningWho acts
draft1Prepared internally, not yet sent to clientAdmin
scheduled2A client walkthrough/presentation has been scheduled to present the proposalAdmin
in_review3Sent to client, awaiting their decisionAdmin updates; client reviews externally
revision_requested4Client asked for changes (price, scope, materials). Triggers a new ProposalRound.Admin records outcome
accepted5Client agreed to the final terms. Project contract_value should be updated to match quoted_amount.Admin records outcome
deposit_paid6Client has paid the initial deposit/advance — post a matching LedgerEntry (entry_type: advance)Admin records outcome
converted7Proposal is fully converted into active execution — terminal stateAdmin records outcome
There is no seeded lookup for a "rejected" outcome — a declined proposal is simply left at its last real stage (usually 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).

FieldTypeNotes
proposal_idinteger (FK)Required on create
proposal_titlestring (derived)Denormalized from proposal.title via eager-loaded relation — read-only, API-computed, never accept on write
round_numberintegerRequired, ≥1. Should match the parent Proposal's current_round once this round is the live one.
amountdecimalThe quoted figure for this specific round
statusstringRound-level vocabulary: pending (default) → accepted | superseded. Distinct from Proposal.status above — don't confuse round-level and proposal-level state.
notestext (nullable)Free-text — what changed this round, why
submitted_atdatetime (nullable)When this round's figure was shared with the client

Multi-Round Bidding User Flow

1AdminPOST /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.
2AdminPUT /api/admin/proposals/{id} — Set status: in_review. Share proposal document with client outside the system (PDF, email, etc.).
3ClientClient requests changes — scope reduction, material change. Admin receives this request externally.
4AdminPUT /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.
5ClientClient accepts the round-2 figure.
6AdminPUT /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.
7AdminClient pays the deposit → proposal status: deposit_paid, post a LedgerEntry (entry_type: advance). Once execution formally begins → proposal status: converted (terminal).
// Round tracking rule current_round starts at 1; increment manually for each new revision round // When a client requests a revision: proposal.status = 'revision_requested' old_round.status = 'superseded' // PUT /api/admin/proposal-rounds/{old_id} // then, once the new figure is set: proposal.current_round += 1 proposal.quoted_amount = revised_amount new_round = POST /api/admin/proposal-rounds { proposal_id: same, round_number: proposal.current_round, amount: revised_amount, status: 'pending' } // On acceptance — update the round and the project to match: round.status = 'accepted' // PUT /api/admin/proposal-rounds/{id} proposal.status = 'accepted' project.contract_value = accepted_proposal.quoted_amount project.status = 'active'
Contract value is NOT automatically updated when a proposal is approved. The admin must explicitly 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).
ADM-006

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

pending in_progress completed
pending skipped(excluded from this project)
FieldTypeRules
phase_keystring slugMust match a phase_library.phase_key for this business. Cannot be changed after creation.
progress_percentinteger 0–1000 when pending; 1–99 when in_progress; 100 forces status to completed.
statusenumSet manually or auto-derived: progress 100 → completed; progress 1–99 → in_progress.
planned_startdateWhen work is expected to begin. Auto-calculated from template: project.start_date + sum of previous phases' default_duration_days.
planned_enddateplanned_start + phase_library.default_duration_days for this phase_key.
sort_orderintegerDisplay order in app. Phases rendered ascending by sort_order.

Overall Project Progress Formula

// Shown on Project Detail → Overview: phase progress bars + overall % active_phases = phases WHERE status != 'skipped' project_progress_% = SUM(phase.progress_percent) / COUNT(active_phases) // Template application — planned date auto-calculation: phase[0].planned_start = project.start_date phase[0].planned_end = planned_start + phase_library[phase_key].default_duration_days phase[1].planned_start = phase[0].planned_end + 1 day phase[1].planned_end = phase[1].planned_start + phase_library[phase[1].phase_key].default_duration_days // ... and so on for each phase in sort_order sequence
Worked Example — Template "Residential Standard" applied to PRJ-004 (start: 01 Jul 2026)
foundation (30 days)01 Jul – 30 Jul 2026 · 65% in_progress
structural (60 days)31 Jul – 28 Sep 2026 · 0% pending
roofing (21 days)29 Sep – 19 Oct 2026 · 0% pending
plumbing (30 days)20 Oct – 18 Nov 2026 · 0% pending
electrical (45 days)19 Nov – 02 Jan 2027 · 0% pending
finishing (20 days)03 Jan – 22 Jan 2027 · 0% pending
project_progress_% = (65+0+0+0+0+0) / 6≈ 10.8%

Applying a Template — Step-by-Step

1AppGET /api/admin/project-templates — fetch templates list. User selects one on the Create Project form.
2AppFor each phase_key in template.phase_keys (in sort order), call GET /api/admin/phase-library to retrieve name, default_duration_days for each key.
3AppCalculate planned_start and planned_end for each phase (sequential chaining above).
4AppFire 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.
5AdminSet project current_phase_key to the first phase_key (or update it as phases complete).
ADM-007

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

InstrumentWhat it representsCreates a ledger entry?Key required fields
VoucherA subcontractor's weekly work claim — "I did this work, pay me PKR X"No — manual stepvoucher_no (req), subcontractor_id, project_id, amount, week_ending, status
LedgerEntryThe formal financial record of actual cash movement or obligationIs itself a ledger entryentry_type, debit OR credit, party_type, party_id, project_id, reference, entry_date
VariationOrderExtra scope beyond the original contract, with a cost impactNo — creates VO exposure onlyvo_number (req), title (req), project_id (req), amount, status
DayworkOrderLabour or equipment engaged on a day/hour rate, outside original scopeNo — creates DWO exposure onlydwo_number (req), title (req), project_id (req), amount, work_date, status

Voucher Lifecycle

draft submitted approved
submitted rejected(create new voucher for corrected amount)
1AdminPOST /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.
2AdminReview claim against site logs and materials. If correct, update status: submitted.
3AdminFinance authorises payment → update status: approved.
4AdminManually create a LedgerEntry to record the payment: 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_typeDirectiondebitcreditExample
paymentMoney out0amountPaying subcontractor for completed work
advanceMoney out (advance)0amountAdvance payment before work starts
deductionMoney recoveredamount0Retention, penalty, or damage recovery
refundMoney returnedamount0Subcontractor returns unused advance
client_receiptMoney in from clientamount0Client pays invoice for completed phase
// Per-party running balance net_balance = SUM(credit) - SUM(debit) WHERE party_type = 'subcontractor' AND party_id = N // Positive = business owes subcontractor; negative = subcontractor owes business // Example — RapidSteel Contractors on PRJ-004: credits = 195,000 (payment VCH-006) + 50,000 (advance ADV-002) = 245,000 debits = 0 net_balance = 245,000 - 0 = PKR 245,000 owed to RapidSteel

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.

pending→ client approves → approved
pending→ client declines → rejected
1AdminPOST /api/admin/variation-orders — Create VO with vo_number (e.g. VO-2026-0003), title, description, amount, project_id, status: pending.
2AdminShare VO document with client for approval. The pending VO is tracked as financial exposure in the project summary.
3AdminClient approves → 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.
4AdminOnce the VO work is complete and payment confirmed, post a LedgerEntry referencing the VO number.

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.

draft submitted approved
DWO fieldRequiredNotes
dwo_numberYesUnique reference, e.g. DWO-2026-0001
titleYesBrief description of day work, e.g. "Emergency drainage clearing"
descriptionNoFull detail of labour and equipment used
work_dateNoDate the daywork was performed
amountNoAgreed day rate × days, or lump sum
statusNodraft → submitted → approved

Full Financial Position Formula

// Project financial position — complete picture // A. What has already been committed (certain costs): ledger_outflow = SUM(credit) - SUM(debit) on all ledger_entries for project approved_vo = SUM(variation_orders.amount WHERE status = 'approved') // B. Uncertain / pending exposure: pending_vo = SUM(variation_orders.amount WHERE status = 'pending') open_dwo = SUM(daywork_orders.amount WHERE status IN ('draft','submitted')) open_vouchers = SUM(vouchers.amount WHERE status IN ('draft','submitted')) // C. Summary: committed = ledger_outflow + approved_vo exposure = committed + pending_vo + open_dwo + open_vouchers budget_remaining = contract_value - committed worst_case = contract_value - exposure // if all pending items materialise
No automatic ledger entries. Approving a Voucher, VO, or DWO does NOT automatically create a LedgerEntry. The admin must post the LedgerEntry manually once actual payment is made. This separation exists so the system can track approved-but-not-yet-paid obligations separately from actual cash movements.
ADM-008

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.

FieldRequiredNotes
user_idYesMust reference an existing platform user with user_type = staff. The platform admin creates user accounts; the business admin links them via StaffProfile.
location_idNoWhich office/site the staff member is based at. Shown in Staff directory and useful for filtering.
job_titleNoFree text — "Site Engineer", "Finance Officer", "Project Manager"
departmentNoGroup classification — "Engineering", "Finance", "Admin". Used for filtering in Staff list.
statusNoactive / inactive / suspended. Inactive staff cannot log in.

Staff Onboarding Flow

1Platform AdminCreate user account with user_type: staff, username, and password via Platform Admin portal.
2AdminPOST /api/admin/staff — Link the new user_id to this business with job_title, department, location_id.
3AdminAssign a custom role to the staff member: PUT /api/admin/staff/{id} updating the user's role — or create a role first if none exists (see ADM-011).
4StaffStaff logs in via 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.

FieldRequiredNotes
company_nameYesLegal/trading name of the subcontracting firm
user_idNoIf set: the linked user gets access to the Contractor portal, where they can view their own vouchers, ledger statement, and daily log history.
trade_typeNoMust match a trade_type.name in this business's Trade Types list (see ADM-003)
contact_emailNoPrimary contact for the company — for external communication only
contact_phoneNoPhone number for site coordination
statusNoactive / inactive

Contractor Portal Access Flow

1Platform AdminCreate a platform user account with user_type: contractor for the subcontractor's on-site representative.
2AdminPUT /api/admin/subcontractors/{id} — Set user_id to the newly created contractor user's ID.
3ContractorThe linked user logs in via 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.
// Data visibility for a linked contractor user // Contractor portal scope: resolves subcontractor_id from auth user via: subcontractor = Subcontractor WHERE user_id = auth()->user()->id // Then scopes all queries: vouchers WHERE subcontractor_id = subcontractor.id ledgers WHERE party_type = 'subcontractor' AND party_id = subcontractor.id daily_logs WHERE project_id IN (projects linked to this business) // their own logs
ADM-009

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

FieldRequiredNotes
titleYesShort description: "Foundation inspection", "Client progress review"
starts_atYesISO datetime. Used for dashboard KPI: upcoming_event_count = events WHERE starts_at >= now()
ends_atNoOptional. Single-point events (inspections, meetings) have no ends_at. Multi-day events use both.
project_idNoLinks event to a project. Allows filtering by project in both Admin and Employee portals.
assigned_user_idNoThe specific staff member or employee responsible. If set, event appears in Employee portal calendar.
event_typeNoinspection / meeting / milestone / site_visit / other
descriptionNoFull detail — agenda, participants, notes

Event Types & Cross-Portal Visibility

event_typeTypical useAdmin seesEmployee seesClient sees
inspectionOfficial engineer or authority inspectionYesIf assignedNo
meetingProgress or coordination meetingYesIf assignedNo
milestonePhase completion or handoverYesIf assignedYes — visible in Progress Chart
site_visitAdmin or client site observationYesIf assignedNo
otherAny uncategorised eventYesIf assignedNo

Calendar Event User Flow

1AdminPOST /api/admin/calendar — Create event with title, project_id, starts_at, event_type. Optionally assign to a staff user via assigned_user_id.
2Employee AppEmployee fetches their calendar: GET /api/employee/calendar — returns events where assigned_user_id = auth user. Event appears in their calendar with project context.
3AdminFor milestone events linked to a project, the Client portal's Progress Chart tab displays these to the client as project timeline markers.
4AdminDashboard KPI auto-updates: upcoming_event_count recalculates on each dashboard load as COUNT(events WHERE starts_at >= now()).
Dashboard KPI definition: The dashboard's "Upcoming Events" card counts all calendar events for the business where starts_at >= now(). It is not filtered by assigned_user — it is a business-wide figure.
ADM-010

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.

1Admin/StaffPOST /api/uploads — Upload the file to the staging endpoint. Returns a file_path (temporary staged URL or storage path).
2Admin/StaffCreate the document record: 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.
3AdminReview queue: filter documents WHERE status = 'pending'. Inspect document, verify it is the correct version and type.
4AdminApprove → PUT document with status: approved. Document is now visible in the Client portal's Document Centre.
5AdminReject → PUT document with status: rejected. Uploader should be notified externally and re-upload the corrected document.
Document statusClient portal visible?Action
pendingNoAwaiting admin approval
approvedYesVisible in Document Centre
rejectedNoNeeds 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.

FieldPurpose
signature_image_pathURL/path to PNG of the authorised signatory's signature. Rendered on PDFs.
signer_nameFull name displayed under the signature on documents
signer_titleJob title displayed under the name (e.g. "Managing Director", "Site Engineer")
// GET /api/admin/signature-settings — returns current record (or 404 if not set) // PUT /api/admin/signature-settings — upsert logic: IF SignatureSetting WHERE business_id = N EXISTS: UPDATE { signature_image_path, signer_name, signer_title } ELSE: INSERT { business_id = N, user_id = 0, signature_image_path, signer_name, signer_title }
ADM-011

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

RoleTypeWho has itEditable?Permissions
adminSystemUsers with user_type = adminNoAll — implicit bypass of all permission checks
staffSystem baseDefault for user_type = staff with no custom roleNoNone — staff with only the base role are denied everywhere
Custom rolesBusiness-definedAssigned to individual staff membersYes — can edit permissions, not the slugAny subset of available permission slugs

Creating and Assigning a Role — User Flow

1AdminGET /api/admin/permissions — Fetch the full list of available permission slugs (see ADM-012). Review what capabilities are needed for this role type.
2AdminPOST /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.
3AdminPOST /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.
4AdminAssign role to a staff member: 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.
5AdminTo update permissions later: call POST /api/admin/roles/{id}/permissions again with the full desired permission array. Changes take effect immediately — no logout required.
// Permission check logic — AuthorizesEngineeringAccess concern IF auth_user.user_type === 'admin': PASS // no permission check performed ELSE IF auth_user.user_type === 'staff': role_permissions = auth_user.role.permissions.pluck('slug') IF requested_slug NOT IN role_permissions: RETURN 403 { "msg": "Forbidden", "error": "Insufficient permissions" } // The sync endpoint replaces — it does not append: // WRONG: sending ["phases.view"] will REMOVE all other permissions from the role // RIGHT: always send the full desired permission set in one call

Recommended Role Templates

Role nameTypical forSuggested permissions
Site SupervisorField engineers, supervisorsprojects.view, phases.view, phases.manage, calendar.view, calendar.manage, staff.view
Finance OfficerAccountants, finance staffprojects.view, vouchers.view, vouchers.manage, ledgers.view, ledgers.manage, variation_orders.view, daywork_orders.view
Project ManagerProject managersprojects.view, projects.manage, proposals.view, proposals.manage, phases.view, phases.manage, calendar.view, calendar.manage, staff.view, subcontractors.view
HR AdminHR and people managementstaff.view, staff.manage, subcontractors.view, subcontractors.manage, locations.view
Read-OnlyDirectors, auditorsdashboard.view, projects.view, proposals.view, phases.view, vouchers.view, ledgers.view, variation_orders.view, calendar.view, staff.view
ADM-012

Permission Matrix

Full list of all permission slugs. Admin users have all implicitly. Staff must be granted them via a custom role.

SlugGrantsEndpoints
dashboard.viewView KPI dashboardGET /api/admin/dashboard
projects.viewRead projects and all sub-routesGET /api/admin/projects, /api/admin/projects/{id}/*
projects.manageCreate, update, delete projectsPOST/PUT/DELETE /api/admin/projects
proposals.viewRead proposalsGET /api/admin/proposals
proposals.manageCreate/update/delete proposalsPOST/PUT/DELETE /api/admin/proposals
phases.viewRead project phasesGET /api/admin/phases
phases.manageCreate/update/delete phasesPOST/PUT/DELETE /api/admin/phases
locations.viewRead locationsGET /api/admin/locations
locations.manageCreate/update/delete locationsPOST/PUT/DELETE /api/admin/locations
staff.viewRead staff profilesGET /api/admin/staff
staff.manageCreate/update/delete staffPOST/PUT/DELETE /api/admin/staff
subcontractors.viewRead subcontractor recordsGET /api/admin/subcontractors
subcontractors.manageCreate/update/delete subcontractorsPOST/PUT/DELETE /api/admin/subcontractors
vouchers.viewRead payment vouchersGET /api/admin/vouchers
vouchers.manageCreate/update/delete vouchersPOST/PUT/DELETE /api/admin/vouchers
ledgers.viewRead ledger entriesGET /api/admin/ledgers
ledgers.manageCreate/update/delete ledger entriesPOST/PUT/DELETE /api/admin/ledgers
variation_orders.viewRead VOsGET /api/admin/variation-orders
variation_orders.manageCreate/update/delete VOsPOST/PUT/DELETE /api/admin/variation-orders
daywork_orders.viewRead DWOsGET /api/admin/daywork-orders
daywork_orders.manageCreate/update/delete DWOsPOST/PUT/DELETE /api/admin/daywork-orders
calendar.viewRead calendar eventsGET /api/admin/calendar
calendar.manageCreate/update/delete eventsPOST/PUT/DELETE /api/admin/calendar
phase_library.viewRead phase libraryGET /api/admin/phase-library
phase_library.manageCreate/update/delete phase libraryPOST/PUT/DELETE /api/admin/phase-library
document_types.viewRead document typesGET /api/admin/document-types
document_types.manageCreate/update/delete document typesPOST/PUT/DELETE /api/admin/document-types
trade_types.viewRead trade typesGET /api/admin/trade-types
trade_types.manageCreate/update/delete trade typesPOST/PUT/DELETE /api/admin/trade-types
subcontractor_categories.viewRead sub. categoriesGET /api/admin/subcontractor-categories
subcontractor_categories.manageCreate/update/deletePOST/PUT/DELETE /api/admin/subcontractor-categories
project_templates.viewRead project templatesGET /api/admin/project-templates
project_templates.manageCreate/update/delete templatesPOST/PUT/DELETE /api/admin/project-templates
settings.viewView signature settingsGET /api/admin/signature-settings
settings.manageUpdate signature settingsPUT /api/admin/signature-settings
manage_rolesCreate roles and sync permissionsGET/POST /api/admin/roles, POST /api/admin/roles/{id}/permissions
ADM-013

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

1AdminOnboarding: Setup locations (Karachi Office), trade types (Civil, Electrical), phase library (foundation→finishing), one project template "Residential Standard" with those 6 phase_keys.
2AdminStaff: Link 3 platform staff user accounts. Assign "Site Supervisor" role (projects.view, phases.manage, calendar.manage) to the on-site engineer.
3AdminSubcontractors: Register RapidSteel Contractors (trade: Civil) with a linked user_id → grants them Contractor portal access.
4AdminProject: 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.
5AdminPhases: Apply "Residential Standard" template — POST 6 phases with phase_keys and auto-calculated planned dates (foundation: 01 Jul – 30 Jul, etc.). Phases exist from day one so planning can start even before the contract is signed.
6AdminProposal Round 1: POST proposal — quoted_amount 2,600,000, status draft → in_review. Client reviews.
7AdminNegotiation: Client requests PKR 100k reduction. PUT proposal status: revision_requested. current_round: 2, quoted_amount: 2,500,000. Client accepts. PUT proposal status: accepted. PUT project contract_value: 2,500,000, status: active.
8AdminDeposit & conversion: Client pays the agreed deposit. PUT proposal status: deposit_paid, POST matching LedgerEntry (entry_type: advance, credit 250,000). Once mobilisation is confirmed, PUT proposal status: converted — the bidding phase is now closed.
9AdminDrawings & sketches: The architect prepares the structural drawing set. A 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.
10AdminCalendar: POST foundation inspection event (05 Jul, inspection type, assigned to Site Engineer user). POST foundation completion milestone (30 Jul, milestone type — visible to client).
11AdminPhase progress: Weekly updates — PUT /api/admin/phases/{id} with progress_percent: 25, 50, 65 as work advances. Dashboard KPI active_project_count = 1.
12AdminVouchers: End of week 2 — POST VCH-2026-0006 for RapidSteel, amount 195,000, week_ending 14 Jun. Review and approve. POST matching LedgerEntry: entry_type payment, credit 195,000, reference VCH-2026-0006.
13AdminVariation Order: Client requests extra drainage work. POST VO-2026-0003 — title "Additional drainage layer", amount 75,000, status pending. Client approves → PUT status approved. PUT project contract_value: 2,575,000. POST LedgerEntry for VO when paid.
14AdminPhase complete: Foundation reaches 100% → PUT phase status: completed. PUT project current_phase_key: structural. Repeat cycle for each phase (structure → finishing).
15AdminDocuments: Upload completion certificate (document_type: Completion Certificate, requires_approval: true). POST document → status: pending. Review and approve → client can download from portal.
16AdminFinancial close: All vouchers paid (ledger entries posted). Confirm no pending VOs or DWOs. remaining_budget = contract_value − committed_ledger_outflow.
17AdminFinal walkthrough & key handover: All 6 phases at 100%. Admin and client walk the finished site together against the approved drawing set (step 9). Snags are logged and cleared. PUT project status: completed, actual_end_date: 2027-01-22 — this transition is the system's record of the handover moment. There is no dedicated "keys" field; the physical handover is represented by 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.
Drawing/contract revision creation is not yet API-exposed. 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.
ADM-014

Error Reference

HTTPMeaningCommon causeFix
401UnauthenticatedToken missing, expired, or revokedRe-login, obtain a new token
403 — portalWrong user_typeContractor/client trying to call /api/admin/*Ensure user_type = admin or staff
403 — permissionStaff missing slugStaff user has no role or role lacks the required slugAssign or update role via ADM-011
404Not foundRecord ID doesn't exist or belongs to a different business_idVerify the ID; check you're using the correct environment (localhost vs live)
422Validation failedMissing required field, wrong type, or constraint violationRead the data.errors object for field-level messages
500Server errorUnexpected exception in controller or modelCheck Laravel logs; report with request ID
// 422 Validation error envelope: { "success": false, "msg": "Validation failed", "data": { "errors": { "name": ["The name field is required."], "project_id": ["The selected project id is invalid."], "phase_key": ["The phase key has already been taken."] } } }
ADM-015

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

PurposeControllerWho can accessEndpoint pattern
Bell dropdownAdminNotificationControllerAdmin and staffGET /api/admin/notifications — max 20 unread, no pagination
PATCH /api/admin/notifications/read-all
Audit log screenAdminAuditLogControllerAdmin only — staff get 403GET /api/admin/audit-logs (paginated, filters)
+ show / markRead / markAllRead / destroy

Notification Shape (t_app_notifications row)

FieldTypeNotes
idintPrimary key
user_idint FK → t_usersRecipient — always the authenticated user's own rows
typestring(100)Machine-readable event type, e.g. project_created, phase_updated, voucher_approved
titlestring(255)Short human-readable title shown in bell and audit list
bodytextFull notification message
dataJSON / nullContextual payload (e.g. {"project_id": 5}) for deep-link navigation
is_readbooleanDefault false; indexed together with user_id
created_attimestampWhen the notification was created

Bell Endpoint Rules

  • Returns ≤ 20 most recent rows where is_read = false for the authenticated user.
  • No pagination key in the response — intentional, this is a lightweight dropdown feed.
  • Available to both admin and staff user 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-text search across 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.

ADM-CL-001

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.
ADM-CL-002

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.

ADM-CL-003

BIZ Scoped Lookup - Locations

Endpoint: GET /api/common/locations. Scoped to the authenticated business. Supports name search. Returns id, name, and address.

ADM-CL-004

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.

ADM-CL-005

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.

ADM-CL-006

BIZ Scoped Lookup - Subcontractor Categories

Endpoint: GET /api/common/subcontractor-categories. Scoped to the authenticated business. Returns id and name.

ADM-CL-007

BIZ Scoped Lookup - Project Templates

Endpoint: GET /api/common/project-templates. Scoped to the authenticated business. Returns id and name.

ADM-CL-008

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.

ADM-CL-009

BIZ Scoped Lookup - Material Stocks

Endpoint: GET /api/common/materials. Requires project_id param. Returns material stocks lookup showing units.

ADM-CL-010

BIZ Scoped Lookup - Roles

Endpoint: GET /api/common/roles. Scoped to the business_id or global roles. Returns id and name.

ADM-CL-011

PLATFORM Lookup - Plan Tiers

Endpoint: GET /api/common/plan-tiers. Lists all distinct, active subscription plan names.

ADM-CL-012

PLATFORM Lookup - Subscription Plans

Endpoint: GET /api/common/subscription-plans. Lists active subscription plans including price suffix (e.g. "Basic ($49/mo)").

ADM-CL-013

GLOBAL Lookup - Project Types

Endpoint: GET /api/common/project-types. Seeded options: Residential, Commercial, Renovation, Industrial.

ADM-CL-014

GLOBAL Lookup - Expense Categories

Endpoint: GET /api/common/expense-categories. Seeded options: Labor, Equipment, Material, Misc.

ADM-CL-015

GLOBAL Lookup - Advance Sources

Endpoint: GET /api/common/advance-sources. Seeded options: Client, Company.

ADM-CL-016

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.

ADM-CL-017

GLOBAL Lookup - Proposal Packages

Endpoint: GET /api/common/proposal-packages. Seeded options: Basic, Standard, Pro.

ADM-CL-018

GLOBAL Lookup - Proposal Stages

Endpoint: GET /api/common/proposal-stages. Seeded options: Draft, Scheduled, In Review, Revision Requested, Accepted, Deposit Paid, Converted.

ADM-CL-019

GLOBAL Lookup - Voucher Statuses

Endpoint: GET /api/common/voucher-statuses. Seeded options: Draft, Submitted, Approved, Paid, Rejected.

ADM-CL-020

GLOBAL Lookup - Phase Statuses

Endpoint: GET /api/common/phase-statuses. Seeded options: Not Started, In Progress, Completed, On Hold.

ADM-CL-021

GLOBAL Lookup - Task Statuses

Endpoint: GET /api/common/task-statuses. Seeded options: In Progress, Completed, Skipped.

ADM-CL-022

GLOBAL Lookup - Theme Colors

Endpoint: GET /api/common/theme-colors. Returns theme colors (Amber, Harbor, Sage, Graphite, Concrete, Copper, Slate) including hex_value.

ADM-CL-023

GLOBAL Lookup - Theme Modes

Endpoint: GET /api/common/theme-modes. Seeded options: Dark Mode, Light Mode, System Preference.

ADM-CL-024

GLOBAL Lookup - Languages

Endpoint: GET /api/common/languages. Seeded options: English (code: en), Urdu (code: ur).