Employee Portal — Business Guide Engineering Services API • v1.0 • Role: Employee
🏢

System Overview & Actor Model

EMP-001

The Employee Portal is one of five role-scoped portals in the Engineering Services platform. It is designed for field and office employees who need to clock in/out, view assigned tasks, log site observations, upload photos, and track their calendar events. Every data endpoint is scoped to the authenticated employee’s own user ID — no cross-employee or business-wide data is ever exposed through this portal.

An employee account is backed by two records: an Auth model (the login credential, user_type='employee') and a StaffProfile record that carries extended metadata such as the assigned business_id and the link to the employee’s projects and tasks. The connection between the two is the user_id foreign key on StaffProfile.

The Five-Portal Architecture

The platform partitions its user base into five distinct portal types. Each portal type is enforced at the API level using the rejectUnlessPortalUserType() guard, preventing cross-portal access even with a valid bearer token.

Engineering Services — Portal Architecture ADMIN user_type=admin Full platform control CLIENT user_type=client Project visibility EMPLOYEE user_type=employee Tasks • Clock • Visits ▲ THIS PORTAL CONTRACTOR user_type=contractor Daily logs • DWOs STAFF user_type=staff Internal ops Shared API Layer — Laravel Sanctum • rejectUnlessPortalUserType() guard per route All portals share one database — data isolation is enforced by user_id/business_id scoping, not separate schemas

Data Isolation — Scoping Rules per Model

Every query executed in the Employee Portal includes a WHERE clause that restricts results to the authenticated employee. The table below lists the exact field used for scoping on each model.

Model / Table Isolation Field Scope Value Notes
StaffProfileuser_idauth()->id()Used for assignment_count on dashboard
EmployeeTaskassigned_user_idauth()->id()Tasks & open_task_count
TimeClockEntryuser_idauth()->id()Clock-in/out history
CalendarEventassigned_user_idauth()->id()Only events assigned to this employee
SiteVisituser_idauth()->id()Visits created by this employee
GalleryItemuploaded_byauth()->id()Photos uploaded by this employee
AppNotificationuser_idauth()->id()Notifications targeted at this employee

StaffProfile Link — How Employees Are Connected to Businesses

The StaffProfile is the bridge between an employee’s login credentials and the business context. The setup flow is initiated by an Admin:

1
ADMIN
Creates a StaffProfile record in the Admin portal, setting name, role, business_id, and other HR fields.
2
ADMIN
Creates an Auth (user) record with user_type='employee' and links it to the StaffProfile by setting staff_profiles.user_id = auth.id.
3
EMPLOYEE
Receives credentials (email + password) and logs into the Employee Portal app.
4
API
On login, API validates user_type=employee and returns a Sanctum token. The employee’s business_id is inherited from their StaffProfile record for all subsequent queries.
5
EMPLOYEE
Employee portal is operational: dashboard shows assignment_count pulled from StaffProfile WHERE user_id=auth_id AND business_id=employee_business_id.
Flat permission model: Unlike the Admin portal (which has a role/permission matrix), all employees share identical API access. There is no employee-level role hierarchy — every authenticated employee can call every Employee Portal endpoint.
🔐

Authentication & Access Model

EMP-002

The Employee Portal uses Laravel Sanctum bearer token authentication. A token is obtained by posting credentials to the shared login endpoint with user_type='employee'. Every subsequent request must include the token in the Authorization header.

Login Request

POST /api/login
Content-Type: application/json

{
  "email": "ali@example.com",
  "password": "secret",
  "user_type": "employee"
}

// Successful response:
{ "token": "1|AbCdEf...", "user": { ... } }

4-Step Authentication Flow

1
APP
Mobile/web app sends POST /api/login with email, password, and user_type="employee".
2
API
API validates credentials against the users / auth table using Sanctum. If the email/password is invalid, returns 401 Unauthorized.
3
API
API checks that user_type = 'employee'. If the user is an admin or contractor who accidentally used this endpoint, returns 403 Forbidden.
4
API
API issues a Sanctum personal access token. App stores the token and includes it as Authorization: Bearer <token> on every subsequent request.

The rejectUnlessPortalUserType Guard

Every route in the Employee Portal is wrapped with a middleware guard that calls rejectUnlessPortalUserType('employee'). This check runs after token validation and inspects the authenticated user’s user_type column.

ScenarioToken Valid?user_typeResult
Normal employee loginYesemployee200 OK
Admin token used on employee routeYesadmin403 Forbidden
Contractor token used on employee routeYescontractor403 Forbidden
Token expired or missingNoN/A401 Unauthorized
Employee token used on admin routeYesemployee403 Forbidden
⚠ Cross-portal token reuse is blocked: Employee users cannot access Admin, Client, or Contractor APIs even with a valid Sanctum token. The user_type mismatch triggers a 403 on every non-employee route. Likewise, admin or contractor tokens will always fail on Employee Portal endpoints.

No Permission Sub-System

Unlike the Admin portal (which has a granular permission matrix — e.g., can_manage_invoices, can_view_reports), the Employee Portal has no role or permission sub-system. Every authenticated employee can call every Employee Portal endpoint. Access control is binary: either you are a valid employee (pass) or you are not (fail).

✓ What Any Employee Can Do

View own dashboard KPIs, see assigned tasks, clock in/out, log site visits, upload gallery items, view calendar, read notifications.

✗ What No Employee Can Do

Access other employees’ data, update task status, change gallery visibility, create calendar events, dismiss notifications programmatically (API), or access any admin/client/contractor endpoint.

Using the Bearer Token

// Include on every request after login:
Authorization: Bearer 1|AbCdEfGhIjKl...

// Example authenticated request:
GET /api/employee/dashboard
Authorization: Bearer 1|AbCdEfGhIjKl...
Accept: application/json
📈

Dashboard KPIs

EMP-003

The employee dashboard (GET /api/employee/dashboard) returns a lightweight summary of the employee’s current work state. It is the entry point for the Employee Portal app and provides two numeric KPIs plus a personalized welcome message.

KPI FieldSource ModelQuery ConditionMeaning
assignment_count StaffProfile user_id = auth()->id() AND business_id = employee_business_id Number of staff profile records linked to this employee in the current business context (typically 1, but allows for multi-business setups)
open_task_count EmployeeTask assigned_user_id = auth()->id() AND status = 'pending' Number of tasks currently assigned to the employee that have not yet started or been completed
welcome_message Generated N/A Personalized greeting string, e.g. “Welcome back, Ali!”

Underlying Queries

-- assignment_count
SELECT COUNT(*) FROM staff_profiles
WHERE user_id = {auth_id}
AND business_id = {employee_business_id};

-- open_task_count
SELECT COUNT(*) FROM employee_tasks
WHERE assigned_user_id = {auth_id}
AND status = 'pending';

-- The controller composes the response:
return [
  'assignment_count' => $assignmentCount,
  'open_task_count' => $openTaskCount,
  'welcome_message' => 'Welcome back, ' . $user->name . '!'
];
Design intent: The employee dashboard is intentionally minimal. Employees are expected to work from the task list, time clock, and calendar — not from aggregate stats. Heavy analytics and reporting belong to the Admin portal. The employee dashboard serves as a quick orientation: “How many tasks do I have pending? Am I set up correctly in the system?”
Sample Dashboard Response
assignment_count2
open_task_count3
welcome_message"Welcome back, Ali Hassan!"
⚠ Status filter: open_task_count only counts tasks with status='pending'. Tasks that are in_progress are not included in this count. If a task has been picked up (moved to in_progress) but not yet completed, it will disappear from the dashboard counter. This is intentional — the counter is a “new work” alert, not a total workload indicator.

Task Lifecycle

EMP-004

Employee tasks are work items created and managed by Admin users and assigned to individual employees via the assigned_user_id field. The Employee Portal provides read-only access to tasks — employees can view their task list but cannot create, update, or delete tasks. All lifecycle transitions are performed by Admin or Staff users.

Task Status State Machine

pending
in_progress
completed
pending
cancelled
in_progress
cancelled

All status transitions are Admin-initiated. Employees observe status changes but cannot trigger them.

Task Priority Values

high
normal
low

EmployeeTask Fields

FieldTypeDescriptionVisible to Employee?
idintegerAuto-increment primary keyYes
titlestringShort task descriptionYes
descriptiontext (nullable)Detailed instructions or contextYes
statusenumpending / in_progress / completed / cancelledYes (read-only)
priorityenumhigh / normal / lowYes
assigned_user_idinteger (FK)Links to Auth.id — the employee this task belongs toImplicit (scoping field)
project_idinteger (nullable FK)Optional project associationYes
project_namestring (derived)Denormalized from project.name via eager-loaded relation; empty string if project_id is nullNo — read-only, API-computed
due_datedate (nullable)Target completion dateYes
business_idinteger (FK)Owning businessImplicit

Task List Query

-- GET /api/employee/tasks
SELECT * FROM employee_tasks
WHERE assigned_user_id = {auth_id}
ORDER BY id DESC;

-- Returns newest tasks first (most recently created at top)

5-Step Task Flow (Employee Perspective)

1
ADMIN
Admin creates a new task via Admin Portal: sets title, description, priority, due_date, project_id, and sets assigned_user_id to the employee’s user ID. Task is created with status='pending'.
2
API
API auto-generates an AppNotification for the employee: “New task assigned: [title]”. The task appears in the employee’s task list immediately.
3
EMPLOYEE
Employee calls GET /api/employee/tasks and sees the new task at the top of the list (ordered by id DESC). Dashboard open_task_count has incremented by 1.
4
ADMIN
Admin updates task status to in_progress or completed via Admin Portal after verifying the work. The employee cannot trigger this transition.
5
EMPLOYEE
Employee refreshes task list and sees the updated status. A completed task no longer appears in open_task_count on the dashboard.
⚠ Read-only constraint: Employees cannot self-update task status via the Employee Portal API. There is no PATCH /api/employee/tasks/{id} endpoint. All task lifecycle transitions must be performed by an Admin or Staff user in their respective portals.
Example Task Object
id42
title"Inspect foundation north wall"
description"Check for cracks near column B3, photograph and report"
status"pending"
priority"high"
assigned_user_id17
project_id4
project_name"Block A"
due_date"2024-03-15"

Time Clock System

EMP-005

The time clock system allows employees to record their working hours directly from the mobile or web app. The API enforces a strict one-open-entry rule: an employee can only have one active clock-in record at a time. Attempting to clock in while already clocked in, or to clock out without an open entry, returns a 422 Unprocessable Entity error with a descriptive message.

TimeClockEntry Fields

FieldTypeSet ByDescription
idintegerAutoPrimary key
user_idinteger (FK)API (auth)The authenticated employee — scoping field
business_idinteger (FK)APIInherited from employee’s StaffProfile
project_idinteger (nullable FK)Employee (optional)Project this time entry is billed/attributed to
project_namestring (derived)APIDenormalized from project.name; empty string if project_id is null
notestext (nullable)Employee (optional)Free-text notes (can be updated on clock-out)
clock_in_atdatetimeAPI (now())Auto-set to current server time on clock-in
clock_out_atdatetime (nullable)API (now())NULL while entry is open; set on clock-out

Clock-In Endpoint

POST /api/employee/clock/in

// Accepted body fields:
{ project_id: 4, // nullable int — optional
  notes: "Starting foundation inspection" // nullable — optional
}

// Server-side logic:
$open = TimeClockEntry::where('user_id', auth()->id())
  ->whereNull('clock_out_at')->first();

if ($open) abort(422, "Already clocked in");

TimeClockEntry::create([
  'clock_in_at' => now(),
  'user_id' => auth()->id(),
  'business_id' => $employee->business_id,
  'project_id' => $request->project_id,
  'notes' => $request->notes,
]);

Clock-Out Endpoint

POST /api/employee/clock/out

// Accepted body fields:
{ notes: "Completed north wall inspection" // nullable — updates notes if provided
}

// Server-side logic:
$open = TimeClockEntry::where('user_id', auth()->id())
  ->whereNull('clock_out_at')->first();

if (!$open) abort(422, "Not clocked in");

$open->update([
  'clock_out_at' => now(),
  'notes' => $request->notes ?? $open->notes,
]);

Duration & Pay Calculation Formulas

The API does not compute duration or pay directly — it stores raw clock_in_at and clock_out_at datetimes. Duration and pay are computed client-side or by a reporting layer.

// Duration for a single entry:
duration_hours = (clock_out_at - clock_in_at).total_seconds() / 3600

// Weekly total for an employee:
weekly_total = SUM(duration_hours)
  WHERE user_id = N
  AND clock_in_at BETWEEN week_start AND week_end

// Pay calculation (if hourly rate is stored):
pay_amount = weekly_total * hourly_rate

// Example: 9.0 hours @ $35/hr = $315.00

Clock History Query

-- GET /api/employee/clock (history)
SELECT * FROM time_clock_entries
WHERE user_id = {auth_id}
ORDER BY clock_in_at DESC;

5-Step Clock Flow

1
EMPLOYEE
Employee arrives at work site and opens the app. Optionally selects a project (e.g., Project ID 4: “Residential Block A”) and taps “Clock In”.
2
APP
App sends POST /api/employee/clock/in with optional { project_id: 4, notes: "Starting morning shift" }.
3
API
API checks for open entry (clock_out_at IS NULL). If none exists, creates a new TimeClockEntry with clock_in_at = now(). Returns the new entry.
4
EMPLOYEE
Employee works. At end of shift, taps “Clock Out” in the app. Optionally adds closing notes (e.g., “Completed foundation inspection, 3 cracks documented”).
5
API
API finds the single open entry (clock_out_at IS NULL), sets clock_out_at = now(), updates notes if provided. The entry is now closed and appears in history with a computed duration.
⚠ project_id is optional: Employees can clock in without specifying a project. This is useful for general administrative work, training, or situations where the project is not yet known. A null project_id means the time entry is unattributed and will need manual assignment by Admin if required for billing.
Example Clock Entry (Closed)
id88
user_id17
project_id4
project_name"Block A"
clock_in_at"2024-03-15 08:00:00"
clock_out_at"2024-03-15 17:00:00"
notes"Foundation inspection complete"
duration (computed)9.0 hours
📍

Site Visits

EMP-006

A Site Visit is a structured log entry recording that an employee attended a physical location for a specific purpose — typically a field inspection, progress check, or client meeting. It is distinct from a task (which is a work item assigned by Admin) and from a time clock entry (which tracks time worked). A site visit focuses on what was observed rather than how long was worked.

Status auto-set: Every site visit is created with status='completed' automatically. The employee cannot change this. The completed status signals that the visit has been performed and the record is a historical log, not an upcoming event.

Site Visit Fields

FieldValidationSet ByDescription
idAutoDatabasePrimary key
titlerequired, string, max:255EmployeeShort description of the visit purpose
notesnullableEmployeeDetailed observations, findings, or follow-up items
project_idnullable intEmployeeOptional project association
project_namestring (derived)APIDenormalized from project.name; empty string if project_id is null
visited_atnullable dateEmployee (or defaults to now())When the visit occurred; supports backdating
statusAuto-setAPI always sets 'completed'Always completed — cannot be changed
user_idAutoAPI (auth)The employee who logged the visit
business_idAutoAPIBusiness context

Create Site Visit Request

POST /api/employee/site-visits

{ title: "Foundation inspection — north wall", // required
  notes: "Hairline crack found at column B3, ~15cm long, no displacement",
  project_id: 4,
  visited_at: "2024-03-15" // nullable — defaults to today
}

// API auto-sets:
status = "completed",
user_id = auth()->id(),
business_id = $employee->business_id

Backdating Support

The visited_at field accepts any valid date string, including past dates. This supports end-of-day logging workflows where employees fill in their visit log at the end of a shift rather than immediately upon returning from the site.

⚠ visited_at vs. created_at: If visited_at is omitted from the request, the system will store null (or the current date, depending on the model default). Always send visited_at explicitly for accurate site visit timestamps, especially when backdating.

SiteVisit vs. DailyLog (Contractor) Comparison

Both models capture field activity, but they serve different actor roles and purposes:

ModelActorPrimary PurposeKey FieldsStatus Lifecycle
SiteVisit Employee Observation report & findings log title, notes, visited_at, project_id Always completed on creation
DailyLog Contractor Labour headcount + daily work summary headcount, work_summary, weather, equipment draft → submitted → approved

4-Step Site Visit Logging Flow

1
EMPLOYEE
Employee visits the construction site to perform an inspection. Notes observations on paper or phone camera.
2
EMPLOYEE
Returns to device (on-site or back at office). Opens app → Site Visits → New Visit. Fills in title, notes, project, and visit date.
3
APP
App sends POST /api/employee/site-visits with the form data. API validates title is present, auto-sets status=completed, and saves the record.
4
EMPLOYEE
Visit appears in the site visit log (GET /api/employee/site-visits), ordered by visited_at DESC. Admin can view all employee site visits in the Admin Portal.
Example Site Visit Record
id31
title"Foundation inspection — hairline crack on north wall"
notes"Crack at column B3, approx 15cm, no displacement. Recommend structural review."
project_id4
project_name"Block A"
visited_at"2024-03-15"
status"completed"
user_id17
📷

Gallery Upload

EMP-007

The gallery feature allows employees to upload site photos, inspection images, and progress documentation directly through the app. Uploaded images are stored as GalleryItem records. The employee can view their own uploads but has no control over visibility — that is an Admin-only privilege.

Scoping Rule

-- GET /api/employee/gallery
SELECT * FROM gallery_items
WHERE uploaded_by = {auth_id}
ORDER BY id DESC;

-- Note: uses uploaded_by, not user_id

Visibility Model

Gallery items have a visibility attribute controlled exclusively by Admin. The employee’s upload creates the item but does not set visibility. The workflow is:

uploaded (no visibility)
→ Admin review →
internal
/
client
/
public
Visibility LevelWho Can SeeSet By
internalAdmin and Staff users onlyAdmin
clientAdmin, Staff, and the associated ClientAdmin
publicAnyone with access to the public galleryAdmin

3-Step Gallery Upload Flow

1
EMPLOYEE
Employee takes a photo on site (or selects from device gallery). Opens app → Gallery → Upload. Adds a caption/title and taps “Upload”.
2
APP
App sends a POST /api/employee/gallery multipart request with the image file and metadata (title, project_id, etc.). API saves the file and creates a GalleryItem record with uploaded_by = auth()->id(). Visibility is not yet set.
3
ADMIN
Admin sees the new upload in the Admin Portal gallery queue. Reviews the image and sets visibility to internal, client, or public based on content sensitivity. The image is now available to the appropriate audience.
Employee can always see their own uploads: The employee gallery endpoint (GET /api/employee/gallery) returns all items WHERE uploaded_by = auth()->id() regardless of visibility setting. The employee is the author and can always review what they’ve uploaded.
⚠ No delete endpoint: The Employee Portal currently does not expose a delete endpoint for gallery items. If an employee uploads an incorrect or sensitive image, they must contact the Admin to remove it via the Admin Portal.
📅

Calendar

EMP-008

The calendar feature surfaces events that an Admin has created and assigned to the employee. The employee portal provides read-only access to calendar events — employees can view scheduled meetings, site visits (calendared), and project milestones but cannot create or modify events.

Calendar Query

-- GET /api/employee/calendar
SELECT * FROM calendar_events
WHERE assigned_user_id = {auth_id}
ORDER BY starts_at ASC;

-- Note: ordered ASC (upcoming first), unlike tasks which are DESC

CalendarEvent Fields

FieldTypeDescription
idintegerPrimary key
titlestringEvent name (e.g., “Site Meeting — Block A”)
starts_atdatetimeEvent start time — used for ordering (ASC)
ends_atdatetime (nullable)Event end time
project_idinteger (nullable FK)Associated project
project_namestring (derived)Denormalized from project.name; empty string if project_id is null
event_typestring (nullable)Category: meeting, milestone, inspection, etc.
assigned_user_idinteger (FK)The employee this event is assigned to — scoping field
business_idinteger (FK)Business context

Shared Model — Cross-Portal Awareness

The CalendarEvent model is shared across all portals that use calendar functionality. The same table serves Admin, Employee, and potentially other portals. Data isolation is achieved by the assigned_user_id scope. This means:

Admin Portal View

Admin sees ALL calendar events across the entire business, filtered by business_id. Can create, update, and delete events. Assigns events to employees via assigned_user_id.

Employee Portal View

Employee sees ONLY events where assigned_user_id = auth()->id(). Read-only. Cannot create or modify events. Sees upcoming events first (ASC ordering).

Example Calendar Event
id19
title"Site Meeting — Residential Block A"
starts_at"2024-03-16 14:00:00"
ends_at"2024-03-16 15:30:00"
project_id4
project_name"Block A"
event_type"meeting"
assigned_user_id17
Ordering difference: Calendar events are ordered ASC by starts_at (soonest first), while Tasks and Clock entries are ordered DESC by id (newest first). This matches the use case: employees want to see what’s coming up next on the calendar, but want the most recent tasks and clock entries at the top of those lists.
🔔

Notifications

EMP-009

The Employee Portal exposes two notification endpoints: a bell dropdown (recent unread) and a bulk mark-all-read action. Notifications are generated by system events (task assignment, calendar creation, site-visit review) and Admin-initiated announcements. Employees cannot delete or create notifications.

Endpoints

MethodPathPurposeAuth guard
GET/api/employee/notificationsBell dropdown — ≤20 most-recent unread notificationsuser_type === 'employee'
PATCH/api/employee/notifications/read-allMark every unread notification as read for the authenticated employeeuser_type === 'employee'

Bell Dropdown Query

-- GET /api/employee/notifications
SELECT * FROM t_app_notifications
WHERE user_id = {auth_id}
AND is_read = false
ORDER BY id DESC
LIMIT 20;

-- Returns newest unread first; capped at 20 rows; no pagination key

Mark-All-Read Action

-- PATCH /api/employee/notifications/read-all
UPDATE t_app_notifications
SET is_read = true
WHERE user_id = {auth_id}
AND is_read = false;

-- Returns: { "success": true, "msg": "All marked read", "data": null }

Response Row Shape (GET)

FieldTypeDescription
idintegerNotification PK — also determines sort order (DESC)
typestringMachine-readable event type, e.g. task_assigned, calendar_event
titlestringShort notification headline for display
bodystringFull notification message (empty string if null)
is_readbooleanAlways false in the bell dropdown response (only unread returned)
dataobjectArbitrary JSON payload — e.g. { "task_id": 45 }; empty object if null
created_atstring (ISO 8601)When the notification was created

Common Notification Triggers

Trigger Eventtype valueExample Title
Admin assigns a new task to employeetask_assigned"New task assigned: Inspect foundation north wall"
Admin creates a calendar event for employeecalendar_event"New event: Site Meeting — Block A"
Admin updates employee's task statustask_updated"Task updated: Inspect foundation is now in_progress"
Admin sends manual announcementannouncement"Important: Safety briefing at 9 AM tomorrow"
Admin reviews a site visit or gallery uploadsite_visit_reviewed"Your site visit log has been reviewed"
ℹ Scoping: WHERE user_id = Auth::id() is the only isolation mechanism — there is no business_id column on t_app_notifications. The user account itself is scoped to one business, so cross-business leakage is impossible.
Example GET /api/employee/notifications Response
successtrue
msg"Notifications"
data[0].id312
data[0].type"task_assigned"
data[0].title"New task assigned: Inspect foundation north wall"
data[0].body"High priority. Due: 2026-07-10."
data[0].is_readfalse
data[0].data{ "task_id": 45 }
data[0].created_at"2026-07-03T08:14:22.000000Z"
🌞

End-to-End Employee Journey

EMP-010

This scenario traces a complete working day for Ali Hassan, a Site Engineer at a construction company using the Engineering Services platform. It illustrates how every Employee Portal feature connects in real-world usage and shows the interplay between Admin actions and employee-facing data.

Scenario Actor: Ali Hassan — Site Engineer
user_id17
user_type"employee"
business_id3
projectResidential Block A (project_id: 4)
1
ADMIN
Staff setup. Admin creates a StaffProfile for Ali Hassan in the Admin Portal: name="Ali Hassan", role="Site Engineer", business_id=3. Admin then creates an Auth user record with email="ali@blockaconstruction.com", user_type="employee", and links it: staff_profiles.user_id = 17. Ali receives his credentials by email.
2
EMPLOYEE
Login & dashboard. Ali opens the Employee Portal app, enters credentials with user_type="employee". API validates, returns Sanctum token. Ali sees his dashboard: assignment_count=2 (two staff profile records in the business), open_task_count=3 (three pending tasks), welcome_message="Welcome back, Ali Hassan!".
3
EMPLOYEE
Clock in. Ali taps “Clock In” and selects Residential Block A (project_id=4). App sends POST /api/employee/clock/in with { project_id: 4, notes: "Morning shift — foundation inspection" }. API checks no open entry exists, creates TimeClockEntry with clock_in_at="08:00:00". Status bar in app shows “Clocked In — 08:00 AM”.
4
EMPLOYEE
Review task list. Ali taps “Tasks”. App calls GET /api/employee/tasks. Three tasks are returned, all status="pending":
  • Task #42: “Inspect foundation north wall” — priority: high, due: today
  • Task #39: “Document rebar placement” — priority: normal
  • Task #35: “Submit weekly safety checklist” — priority: normal
Ali decides to start with the high-priority foundation inspection.
5
EMPLOYEE
Perform site inspection. Ali walks the site, examining the foundation. On the north wall, he finds a hairline crack near column B3, approximately 15cm long with no structural displacement. He photographs it with his phone.
6
EMPLOYEE
Log site visit. Ali opens “Site Visits” → “New Visit”. Fills in:
  • Title: “Foundation inspection — hairline crack on north wall”
  • Notes: “Crack at column B3, ~15cm, no displacement. Recommend structural engineer review.”
  • Project: Residential Block A (project_id=4)
  • Visited at: today’s date
App sends POST /api/employee/site-visits. API auto-sets status="completed". Visit is saved as record #31.
7
EMPLOYEE
Upload photos. Ali navigates to “Gallery” → “Upload Photo”. Uploads 3 photos (overview shot, close-up of crack, scale reference). Each creates a GalleryItem record with uploaded_by=17. Visibility is not yet set — Admin will review and classify these as internal or client-visible.
8
ADMIN
Task status update. Admin reviews the situation in the Admin Portal and updates Task #42 status from pending to in_progress. The system generates a notification for Ali: “Task updated: Inspect foundation north wall is now in_progress”. Ali’s open_task_count drops from 3 to 2 on his next dashboard refresh.
9
ADMIN
Calendar event created. Admin creates a CalendarEvent: title="Site Meeting — Block A Structural Review", starts_at="2024-03-16 14:00:00", ends_at="2024-03-16 15:30:00", assigned_user_id=17, event_type="meeting". A notification is sent to Ali: “New event: Site Meeting — Block A Structural Review”.
10
EMPLOYEE
Clock out. At 5:00 PM, Ali taps “Clock Out”. App sends POST /api/employee/clock/out with { notes: "Foundation inspection complete, site visit logged, 3 photos uploaded" }. API finds the open entry, sets clock_out_at="17:00:00".
clock_in  = 08:00:00
clock_out = 17:00:00
duration  = 9.0 hours
// (17:00 - 08:00) = 9.0 × 3600 seconds / 3600 = 9.0 hours
11
EMPLOYEE
Check calendar. Ali taps “Calendar”. App calls GET /api/employee/calendar. Returns the new event: “Site Meeting — Block A Structural Review” on tomorrow at 2:00 PM. Ali adds it to his personal calendar. He’s prepared: the site visit notes and photos are on record for the structural engineer at the meeting.
12
EMPLOYEE
Notifications. Ali checks the notification bell. Two unread notifications: (1) “Task updated: Inspect foundation north wall is now in_progress” and (2) “New event: Site Meeting — Block A Structural Review”. He reads them and considers his day complete. Tomorrow he will attend the structural review meeting and await further task assignments.

Day Summary

ActivityAPI EndpointRecord Created/Updated
LoginPOST /api/loginSanctum token issued
Dashboard checkGET /api/employee/dashboardRead-only KPIs
Clock inPOST /api/employee/clock/inTimeClockEntry #88 created (open)
Task listGET /api/employee/tasksRead-only — 3 tasks returned
Site visitPOST /api/employee/site-visitsSiteVisit #31 created
Gallery upload (x3)POST /api/employee/galleryGalleryItem #55, #56, #57 created
Clock outPOST /api/employee/clock/outTimeClockEntry #88 closed, 9h logged
Calendar checkGET /api/employee/calendarRead-only — 1 event returned
NotificationsGET /api/employee/notificationsRead-only — 2 notifications

Error Reference

EMP-011

The following table documents all error responses that an Employee Portal client may encounter. Errors are returned as JSON with an appropriate HTTP status code and a descriptive message field.

HTTP CodeMessage / TriggerRoot CauseResolution
401 Unauthenticated Authorization header is missing, the token has expired, or the token has been revoked (e.g., user logged out from another device). Re-authenticate via POST /api/login. Store the new token and retry the request.
403 Forbidden — user_type mismatch The authenticated user’s user_type is not employee. Common causes: using an admin or contractor token on an employee endpoint, or the user account was changed to a different type after the token was issued. Ensure the login was performed with user_type="employee" credentials. Do not reuse tokens across portal types. If the account was reclassified, contact the system Admin.
422 “Already clocked in” Employee sent POST /api/employee/clock/in while a TimeClockEntry with clock_out_at IS NULL already exists for their user_id. Only one open entry is allowed at a time. Clock out first via POST /api/employee/clock/out, then clock in again if a new session is needed. Check clock history (GET /api/employee/clock) to verify the open entry.
422 “Not clocked in” Employee sent POST /api/employee/clock/out but no open TimeClockEntry (clock_out_at IS NULL) exists for their user_id. Cannot clock out without first clocking in. Clock in first via POST /api/employee/clock/in. If the employee believes they were clocked in but the entry is missing, contact Admin — a manual correction may be needed in the Admin Portal.
422 Validation error — “title is required” A required field was omitted from the request body. The most common case is POST /api/employee/site-visits without a title field. The errors object in the response will list all failing fields. Include all required fields. For site visits: title is mandatory. For clock-in: no required fields (project_id and notes are both nullable). Check the full errors response object for a field-by-field breakdown.
404 Not Found The requested resource does not exist, or it exists but belongs to a different user (isolation prevents returning 403 on resource-not-found to avoid leaking existence information). Verify the resource ID is correct and belongs to the authenticated employee. If the ID was provided by Admin, confirm it was set up correctly.
500 Internal Server Error Unexpected server-side error. Not caused by client input. May indicate a database connection issue, missing configuration, or an application bug. Retry the request after a short delay. If the error persists, report it to the system administrator with the request details and timestamp. Check server logs for the exception and trace.

Error Response Format

// Standard Laravel validation error (422):
{
  "message": "The title field is required.",
  "errors": {
    "title": [ "The title field is required." ]
  }
}

// Business logic error (422 — clock state):
{
  "message": "Already clocked in"
}

// Auth guard error (403):
{
  "message": "Forbidden"
}

Clock State Machine — Error Conditions

The time clock system is a two-state machine. The following diagram shows when errors occur:

CLOCKED OUT clock_out_at IS NOT NULL (or no entry exists) CLOCKED IN clock_out_at IS NULL (open entry exists) POST /clock/in ✓ POST /clock/out ✓ 422 "Already clocked in" 422 "Not clocked in" Dashed red = invalid action • Solid = valid transition
Client-side guard recommendation: Mobile and web apps should maintain local clock state (clocked in / clocked out) and disable the inappropriate button to prevent user error. However, the API enforces the state server-side regardless of client behavior, so a race condition (e.g., two devices used simultaneously) will always be caught at the API level.