Skip to content

Shift Management

Shift Management provides a workspace-scoped CRUD API for managing work shifts used in field force scheduling. Shifts define time windows (start and end times) that can be assigned to visits, enabling principals to organize merchandising activities across different time periods.

  • Scheduling: Assign specific shifts (morning, afternoon, night) to outlet visits
  • Reporting: Filter and analyze visit performance by shift periods
  • Time Windows: Define operational hours for different outlet categories
  • Workforce Planning: Track which shifts are covered by team members

All shift operations are scoped to the authenticated user’s workspace. Cross-workspace access is prevented at the repository layer by mandatory workspace_id filters on all queries.

FieldTypeDescriptionConstraints
idintegerPrimary keyAuto-increment
workspace_idintegerFK to workspacesNOT NULL, indexed
namestringShift name (e.g., “Shift Pagi”)VARCHAR(100), NOT NULL
start_timestringStart time in HH:MM formatVARCHAR(5), NOT NULL
end_timestringEnd time in HH:MM formatVARCHAR(5), NOT NULL
is_activebooleanWhether shift is currently activeDEFAULT TRUE
created_attimestampCreation timestampTIMESTAMPTZ
updated_attimestampLast update timestampTIMESTAMPTZ
deleted_attimestampSoft delete timestampTIMESTAMPTZ (nullable)
MethodEndpointPermissionDescription
GET/office/v1/shiftsshift.view or shift.manageList shifts (paginated)
GET/office/v1/shifts/:idshift.view or shift.manageGet shift by ID
POST/office/v1/shiftsshift.manage (full access)Create new shift
PUT/office/v1/shifts/:idshift.manage (full access)Update shift
DELETE/office/v1/shifts/:idshift.manage (full access)Delete shift (soft)
MethodEndpointPermissionDescription
GET/app/v1/shiftsValid auth tokenList shifts (read-only for mobile app)

Mobile routes provide read-only access for the field force app to retrieve available shifts for visit check-in and reporting.

Retrieve paginated list of shifts with optional keyword filtering.

GET /office/v1/shifts?page=1&limit=50&keyword=Pagi
Authorization: Bearer <token>
{
"data": {
"data": [
{
"id": 1,
"name": "Shift Pagi",
"start_time": "07:00",
"end_time": "15:00",
"is_active": true,
"workspace_id": 1,
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 3,
"total_pages": 1
}
},
"message": "shifts retrieved",
"code": "SHIFT_LIST"
}

Query Parameters:

ParameterTypeRequiredDefaultDescription
pageintegerNo1Page number
limitintegerNo50Items per page (max 100)
keywordstringNo-Filter by shift name (case-insensitive)

Retrieve a single shift by its ID.

GET /office/v1/shifts/1
Authorization: Bearer <token>
{
"data": {
"id": 1,
"name": "Shift Pagi",
"start_time": "07:00",
"end_time": "15:00",
"is_active": true,
"workspace_id": 1,
"created_at": "2026-03-04T00:00:00Z",
"updated_at": "2026-03-04T00:00:00Z"
},
"message": "shift retrieved",
"code": "SHIFT_DETAIL"
}

Create a new work shift.

POST /office/v1/shifts
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Weekend Shift",
"start_time": "09:00",
"end_time": "17:00",
"is_active": true
}
{
"data": {
"id": 4,
"name": "Weekend Shift",
"start_time": "09:00",
"end_time": "17:00",
"is_active": true,
"workspace_id": 1,
"created_at": "2026-03-05T00:00:00Z",
"updated_at": "2026-03-05T00:00:00Z"
},
"message": "shift created",
"code": "SHIFT_CREATED"
}

Request Body Fields:

FieldTypeRequiredDescription
namestringYesShift name (max 100 characters)
start_timestringYesStart time in HH:MM format (24-hour)
end_timestringYesEnd time in HH:MM format (24-hour)
is_activebooleanNoActive status (defaults to true)

Update an existing shift.

PUT /office/v1/shifts/4
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Weekend Shift",
"start_time": "10:00",
"end_time": "18:00",
"is_active": false
}
{
"message": "shift updated",
"code": "SHIFT_UPDATED"
}

Soft delete a shift (marks deleted_at timestamp).

DELETE /office/v1/shifts/4
Authorization: Bearer <token>
{
"message": "shift deleted",
"code": "SHIFT_DELETED"
}

The seeder 010_work_shifts.go creates three default shifts for the root workspace (workspace ID 1).

NameStart TimeEnd TimeDescription
Shift Pagi07:0015:00Morning shift (8 hours)
Shift Siang15:0023:00Afternoon shift (8 hours)
Shift Malam23:0007:00Night shift (8 hours, crosses midnight)

To run the seeder on a fresh or existing installation:

Terminal window
go run ./cmd/seederv2/... --run 010_work_shifts

The seeder is idempotent — it uses check-before-insert pattern and will not create duplicate shifts if they already exist.

The seeder also creates and assigns the shift.manage permission:

  1. Creates shift.manage permission with key PermissionKeyShiftManage
  2. Assigns full access to roles:
    • Super Admin
    • Admin
    • Manager

All shift queries are automatically scoped to the authenticated user’s workspace:

  • Extraction: workspace_id is extracted from the JWT auth context
  • Filtering: All repository queries include WHERE workspace_id = ? clause
  • Isolation: Users cannot access shifts from other workspaces
  • Assignment: New shifts automatically receive the workspace ID from context

This ensures multi-tenant data isolation and prevents unauthorized cross-workspace access.

Shift Management uses two permission levels for access control.

Permission KeyAccess LevelOperations
shift.manageFullCreate, Read, Update, Delete
shift.viewRead-onlyRead (List + Detail)
OperationEndpointRequired PermissionAccess Level
ListGET /office/v1/shiftsshift.view or shift.manageView or Full
DetailGET /office/v1/shifts/:idshift.view or shift.manageView or Full
CreatePOST /office/v1/shiftsshift.manageFull only
UpdatePUT /office/v1/shifts/:idshift.manageFull only
DeleteDELETE /office/v1/shifts/:idshift.manageFull only

The mobile app (/app/v1/shifts) provides read-only access to all authenticated users. This allows field force members to:

  • View available shifts during visit check-in
  • See shift assignments on visit details
  • Filter reports by shift periods

No RBAC checks are performed on app routes — authentication is sufficient.

400 Bad Request — Invalid ID:

{
"error": "invalid shift ID",
"code": "BAD_REQUEST"
}

400 Bad Request — Validation Failed:

{
"error": "validation failed",
"data": {
"name": "Name is required",
"start_time": "Start time is required"
},
"code": "VALIDATION_ERROR"
}

404 Not Found:

{
"error": "shift not found",
"code": "NOT_FOUND"
}

403 Forbidden — Insufficient Permission:

{
"error": "insufficient permission",
"code": "FORBIDDEN"
}