200ok shiftplanning - Optimize your work force schedule
Table of Contents
1. 200ok shiftplanning
This is the service documentation for the 200ok Gmbh product 200ok shiftplanning. It is intended for developers working with or on 200ok shiftplanning.
1.1. Feedback
Please let us know about gaps or errors in our documentation at info@200ok.ch.
2. Introduction
200ok shiftplanning is a modern data-driven shift planning solution that drives the efficiency of it's customers through its stateless API solution.
The concept of 200ok shiftplanning is quite easy: Given a set of
credentials and a configured tenant, you can generate a Roster
with
planned shifts in just one single API call.
200ok has been founded by two former lecturers of the university of applied sciences of Zurich. During this time, they have acquired a lot of knowledge in the area of NP hard and NP complete problems and have supervised a series of related papers. Some of these papers are:
- Evaluation of solving the nurse-rostering-problem in web applications
- An explorative study into the realm of On-Device Machine Training
- Linked data-driven UI for task-based computing
- Comparing traditional statistical methods with machine learning
- Generic API to process fundamental computer science problems
- Evaluating concepts of NP-complete problems for web applications
- Evaluation on bringing business rules from customers into a web application
Apart from these papers, 200ok has been involved with providing products for shift planning since 2014 and hence has a lot of practical experience on the topic. 200ok shiftplanning is the culmination of decades of experience solving hard business problems and data driven API development.
2.1. Overview Domain Model
The following list gives a rough overview over the domain model. The application of the models depends on the specific use case.
- Roster (a list of employees and associated information for a given time period)
- Shift (a scheduled period of work or duty)
- Employee
- Spot (a work space, area, customer, or subject)
- Skill (the ability or permission to perform work in a given shift or spot)
- Availability (of an employee)
- Contract (time constraints for an employee)
3. Use cases
To optimize a Roster
, you create a Job
of a specific type
. The
type
depends on what resource you want to optimize for.
3.1. Optimize for employees (Classic shift operation)
In a classic shift operation, during optimization, shifts are
assigned to employees. Hence, the shiftplanning job type
is
called optimize_for_employees
. For an example how to create a
roster for this, see create roster.
3.1.1. Rules
To calculate a Roster
with planned shifts, there have to be
rules. Here is the list of rules that 200ok shiftplanning uses
by for Job type
optimize_for_employees
. These rules can, of
course, be adapted to your specific business and to your wishes.
We are also happy to listen to your requirements and add rules
specific to your business.
Rules are always formulated for the negative case. The reason is that our algorithm will give penalty points for every rule (constraint) that cannot be fulfilled.
- employee lacks required skill for shift - employee lacks required skill for spot - employee has overlapping shifts - daily minutes exceed contract maximum - weekly minutes exceed contract maximum - monthly minutes exceed contract maximum - yearly minutes exceed contract maximum - shift remains unassigned - locked employee was changed - employee assigned to shift but unavailable - weekly minutes exceed skill maximum - employee assigned to shift undesired - employee assigned to shift desired
3.1.2. Budgeting per Skill
Optionally, if you want employees to work a maximum of time on a
certain task, you can set a time budget per Skill
.
Example use-case: A support department wants their employees to work
on different Spot
\s for a maximum amount of time - 2 days a week on
the hotline, 2 days a week on second-level support and finally one day
a week giving trainings to customers. Achieve this kind of
budgeting by setting maximumMinutesPerWeekPerEmployee
on the
relevant Skill
.
3.2. Optimize for spots (Service provider)
Service providers classically aren't shift operations. However,
they have customers with budgets. The problem during shift planning
is to assign the shifts to those customers (i.e. campaigns,
locations, etc.). This can be achieved by providing Shifts
that
have pre-assigned Employees
, but are lacking the Spot
. Spots
have a time budget assigne through maximumMinutes
. The
shiftplanning job type
is called optimize_for_spots
.
Here is a full example on how to create a roster with
optimize_for_spots
.
3.2.1. Create Roster
Here's an example Roster
that has pre-assigned shifts that are
lacking a Spot
.
{ "meta": { "ruleFilePaths": [ "rosterConstraintsOptimizeForSpot.drl" ], "secondsSpentLimit": 10 }, "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "spots": [ { "id": "reiskocher", "name": "Reiskocher", "maximumMinutes": 180, "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "maximumMinutes": 240, "requiredSkills": [ "reiswaschen" ] } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerMonth": 0, "maximumMinutesPerYear": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "contract": "fulltime", "skills": [ "reiskochen", "reiswaschen" ] }, { "id": "mrrice", "name": "Mr Rice", "contract": "fulltime", "skills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "employee": "mrsrice", "startDateTime": "2021-06-23T08:00", "endDateTime": "2021-06-23T12:00" }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "employee": "mrsrice", "startDateTime": "2021-06-23T14:00", "endDateTime": "2021-06-23T17:00" }, { "id": "5eb45659-f88f-4634-a925-3d5ef706199f", "employee": "mrrice", "startDateTime": "2021-06-23T08:00", "endDateTime": "2021-06-23T12:00" } ] }
echo $example_roster | http $host/v1/tenants/$tenant_id/jobs \ Authorization:"Token ${auth_token}" \ webhook==$webhook_url \ type==optimize_for_spots
{"msg":"Job accepted.","webhook":"http://your-domain.ch/webhook","job_id":"875016f8-41fb-4ad2-851f-59900b471749"}
When the job is finished, the resulting Job
will be POSTed to
http://your-domain.ch/webhook
. It will include some
metadata. The actual Roster
will be nested under the key
results
. The Roster
, again, will include some metadata and
planned shifts indicated by the employee
id per Shift
.
This is what it looks like:
{ "payload": { "meta": { "ruleFilePaths": [ "rosterConstraintsOptimizeForSpot.drl" ], "secondsSpentLimit": 10 }, "spots": [ { "id": "reiskocher", "name": "Reiskocher", "maximumMinutes": 180, "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "maximumMinutes": 240, "requiredSkills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "employee": "mrsrice", "endDateTime": "2021-06-23T12:00", "startDateTime": "2021-06-23T08:00" }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "employee": "mrsrice", "endDateTime": "2021-06-23T17:00", "startDateTime": "2021-06-23T14:00" }, { "id": "5eb45659-f88f-4634-a925-3d5ef706199f", "employee": "mrrice", "endDateTime": "2021-06-23T12:00", "startDateTime": "2021-06-23T08:00" } ], "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerYear": 0, "maximumMinutesPerMonth": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "skills": [ "reiskochen", "reiswaschen" ], "contract": "fulltime" }, { "id": "mrrice", "name": "Mr Rice", "skills": [ "reiswaschen" ], "contract": "fulltime" } ] }, "started_at": "2022-01-25T15:33:08.196Z", "completed_at": "2022-01-25T15:33:20.767Z", "webhook": "https://webhook.site/e67d9837-5ae0-468f-8e04-04bf34bc87ea", "indictment": { "charge-0": { "justification": { "id": "5eb45659-f88f-4634-a925-3d5ef706199f", "spot": null, "startDateTime": "2021-06-23T08:00", "endDateTime": "2021-06-23T12:00", "requiredSkills": null, "employee": { "id": "mrrice", "name": "Mr Rice", "contract": { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerMonth": 0, "maximumMinutesPerYear": 0 }, "skills": [ { "id": "reiswaschen", "name": "Reis waschen" } ] }, "lockedEmployee": null }, "constraintMatchSet": [ { "constraintName": "spotRemainsUnassigned", "justificationList": [ { "id": "5eb45659-f88f-4634-a925-3d5ef706199f", "spot": null, "startDateTime": "2021-06-23T08:00", "endDateTime": "2021-06-23T12:00", "requiredSkills": null, "employee": { "id": "mrrice", "name": "Mr Rice", "contract": { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerMonth": 0, "maximumMinutesPerYear": 0 }, "skills": [ { "id": "reiswaschen", "name": "Reis waschen" } ] }, "lockedEmployee": null } ], "score": { "initScore": 0, "hardScore": 0, "softScore": -240, "feasible": true, "solutionInitialized": true }, "constraintId": "defaultpkg/spotRemainsUnassigned" } ], "score": { "initScore": 0, "hardScore": 0, "softScore": -240, "feasible": true, "solutionInitialized": true }, "constraintMatchCount": 1 } }, "type": "optimize_for_spots", "state": "complete", "id": 142, "tenant_id": 1, "uuid": "b50d871c-2194-49c1-8d6a-6a2ce78fb2dc", "created_at": "2022-01-25T15:33:08.169Z", "results": [ { "id": 197, "job_id": 142, "payload": { "meta": { "entityClass": "shift_for_spot", "ruleFilePaths": [ "rosterConstraintsOptimizeForSpot.drl" ], "secondsSpentLimit": 10 }, "score": { "feasible": true, "hardScore": 0, "initScore": 0, "softScore": -240, "solutionInitialized": true }, "spots": [ { "id": "reiskocher", "name": "Reiskocher", "maximumMinutes": 180, "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "maximumMinutes": 240, "requiredSkills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": "lavabo", "type": "shift_for_spot", "employee": "mrsrice", "endDateTime": "2021-06-23T12:00", "startDateTime": "2021-06-23T08:00", "lockedEmployee": null, "requiredSkills": null }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": "reiskocher", "type": "shift_for_spot", "employee": "mrsrice", "endDateTime": "2021-06-23T17:00", "startDateTime": "2021-06-23T14:00", "lockedEmployee": null, "requiredSkills": null }, { "id": "5eb45659-f88f-4634-a925-3d5ef706199f", "spot": null, "type": "shift_for_spot", "employee": "mrrice", "endDateTime": "2021-06-23T12:00", "startDateTime": "2021-06-23T08:00", "lockedEmployee": null, "requiredSkills": null } ], "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerYear": 0, "maximumMinutesPerMonth": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "skills": [ "reiskochen", "reiswaschen" ], "contract": "fulltime" }, { "id": "mrrice", "name": "Mr Rice", "skills": [ "reiswaschen" ], "contract": "fulltime" } ], "availabilities": [] }, "final": true, "created_at": "2022-01-25T15:33:20.757Z" } ] }
3.2.2. Rules
To calculate a Roster
with planned shifts, there have to be
rules. Here is the list of rules that 200ok shiftplanning uses
by for Job type
optimize_for_spots
. These rules can, of
course, be adapted to your specific business and to your wishes.
We are also happy to listen to your requirements and add rules
specific to your business.
Rules are always formulated for the negative case. The reason is that our algorithm will give penalty points for every rule (constraint) that cannot be fulfilled.
- employee lacks required skill for shift - employee lacks required skill for spot - minutes exceed spot maximum - weekly minutes exceed skill maximum - spot remains unassigned - employee assigned to shift undesired - employee assigned to shift desired
3.3. Optional rules
There are some rules that can optionally be enabled for every use case:
- context switch on same day
4. API
The API of 200ok shiftplanning is documented with Swagger and available at https://compute-demo.200ok.ch.
The following examples use httpie or curl to create http requests. The examples always have two code blocks: First the httpie or curl command, then immediately the result.
4.1. Authentication
This API employs authentication and authorization. You will get appropriate 401 (unauthorized) and 403 (forbidden) status codes. To make authenticated requestes, you first login using basic auth. This will yield a JWT Token that can you will use for subsequent requests in a "Authorization" header.
Here is how to login with basic auth using common commandline tools:
- curl:
curl --basic -u username:password https://compute-demo.200ok.ch/v1/login
- httpie:
http --auth username:password https://compute-demo.200ok.ch/v1/login
If you want to generate a basic auth token for use with this Swagger
API, you can use your favorite programming language. Here's an example
in shell: echo -n "Basic " $(echo -n "username:password" | base64)
This will generate something like Basic dXNlcjI6a29pcmExMg==
which you
then can use as authorization header to retrieve a JWT token. JWT
tokens expire after 2h. Example:
curl -H "Authorization: Basic dXNlcjI6a29pcmExMg=
" https://compute-demo.200ok.ch/v1/login=
After logging in via basic auth, the retrieved JWT token should be used for endpoints that require authentication. Here's an example:
curl -H "Authorization: Token eyJhbGc...2Bm63IBw" https://compute-demo.200ok.ch/v1/tenants
4.1.1. httpie
http --ignore-stdin --pretty format --auth user1:kissa13 $host/v1/login
{ "message": "Basic auth succeeded!", "user": { "id": 1, "roles": [ "admin", "user" ], "token": "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MSwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl0sImV4cCI6MTYzNzc2OTYyOX0.YjPQnzweBnzwVaRfo6QMfLzbvyf3xX2KJR7CtOJkBMYdiDRQA27Gj2I_pR0GXGl_Id87y5teGwUNoZDk60BnNw", "username": "user1" } }
4.1.2. curl
curl --basic -u user1:kissa13 $host/v1/login | jq '.'
{ "message": "Basic auth succeeded!", "user": { "username": "user1", "id": 1, "roles": [ "admin", "user" ], "token": "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MSwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl0sImV4cCI6MTYzODg5NDAyM30.9E1Wfyc_3iNn1IPnvrnU5qUV9Y4UnwE5UbG9uVRudrqq6gK4qbaS_425qOF-bBHZum0YvasDdUD9rbjdN9ZF5g" } }
4.2. Tenant
http --ignore-stdin $host/v1/tenants \ Authorization:"Token ${auth_token}" \ name="tenant 1"
{"id":118,"name":"tenant 1"}
4.3. Roster
To create a Roster
with the shifts planned, simply POST a
Roster
where the shifts are not yet planned. Optionally, if you
want to send a partially planned Roster
, instead, you can.
Find the schema definition of Roster
in the appendix or directly
in the Swagger API documentation: https://compute-demo.200ok.ch/index.html#/roster/post_v1_tenants__tenant_id__compute_roster.
Here's an example Roster
with no planned shifts:
{ "meta": { "ruleFilePaths": [ "rosterConstraints.drl" ], "secondsSpentLimit": 10 }, "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "spots": [ { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "requiredSkills": [ "reiswaschen" ] } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerMonth": 0, "maximumMinutesPerYear": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "contract": "fulltime", "skills": [ "reiskochen", "reiswaschen" ] }, { "id": "mrrice", "name": "Mr Rice", "contract": "fulltime", "skills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": "reiskocher", "startDateTime": "2021-06-23T09:00", "endDateTime": "2021-06-23T10:00" }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": "reiskocher", "startDateTime": "2021-06-23T09:30", "endDateTime": "2021-06-23T10:30" }, { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": "lavabo", "startDateTime": "2021-06-23T08:30", "endDateTime": "2021-06-23T09:00" } ] }
4.3.1. Create Roster
To optimize a Roster
, you create a Job
of a specific type
.
The type
depends on what resource you want to optimize for.
Let's first look at the case of a classic shift operation. In a
classic shift operation, during optimization, shifts are assigned
to employees. Hence, the Job type
is called optimize_for_employees
.
NB: For a complete list of available job types, please see job types.
Here is an example given the above example Roster
.
echo $example_roster | http $host/v1/tenants/$tenant_id/jobs \ Authorization:"Token ${auth_token}" \ webhook==$webhook_url \ type==optimize_for_employees
{"msg":"Job accepted.","webhook":"http://your-domain.ch/webhook","job_id":"cfc501ad-4b17-4c55-beed-4e2f5b59eb54"}
When the job is finished, the resulting Job
will be POSTed to
http://your-domain.ch/webhook
. It will include some
metadata. The actual Roster
will be nested under the key
results
. The Roster
, again, will include some metadata and
planned shifts indicated by the employee
id per Shift
.
This is what it looks like:
{ "payload": { "meta": { "ruleFilePaths": [ "rosterConstraints.drl" ], "secondsSpentLimit": 10 }, "spots": [ { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "requiredSkills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": "reiskocher", "endDateTime": "2021-06-23T10:00", "startDateTime": "2021-06-23T09:00" }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": "reiskocher", "endDateTime": "2021-06-23T10:30", "startDateTime": "2021-06-23T09:30" }, { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": "lavabo", "endDateTime": "2021-06-23T09:00", "startDateTime": "2021-06-23T08:30" } ], "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerYear": 0, "maximumMinutesPerMonth": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "skills": [ "reiskochen", "reiswaschen" ], "contract": "fulltime" }, { "id": "mrrice", "name": "Mr Rice", "skills": [ "reiswaschen" ], "contract": "fulltime" } ] }, "started_at": "2022-01-25T16:31:12.762Z", "completed_at": "2022-01-25T16:31:25.287Z", "webhook": "https://webhook.site/e67d9837-5ae0-468f-8e04-04bf34bc87ea", "indictment": { "charge-0": { "justification": { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": { "id": "reiskocher", "name": "Reiskocher", "maximumMinutes": 0, "requiredSkills": [ { "id": "reiskochen", "name": "Reis kochen" } ] }, "startDateTime": "2021-06-23T09:30", "endDateTime": "2021-06-23T10:30", "requiredSkills": null, "employee": { "id": "mrrice", "name": "Mr Rice", "contract": { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerMonth": 0, "maximumMinutesPerYear": 0 }, "skills": [ { "id": "reiswaschen", "name": "Reis waschen" } ] }, "lockedEmployee": null }, "constraintMatchSet": [ { "constraintName": "employeeLacksRequiredSkillForSpot", "justificationList": [ { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": { "id": "reiskocher", "name": "Reiskocher", "maximumMinutes": 0, "requiredSkills": [ { "id": "reiskochen", "name": "Reis kochen" } ] }, "startDateTime": "2021-06-23T09:30", "endDateTime": "2021-06-23T10:30", "requiredSkills": null, "employee": { "id": "mrrice", "name": "Mr Rice", "contract": { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerMonth": 0, "maximumMinutesPerYear": 0 }, "skills": [ { "id": "reiswaschen", "name": "Reis waschen" } ] }, "lockedEmployee": null } ], "score": { "initScore": 0, "hardScore": -60, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintId": "defaultpkg/employeeLacksRequiredSkillForSpot" } ], "score": { "initScore": 0, "hardScore": -60, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintMatchCount": 1 } }, "type": "optimize_for_employees", "state": "complete", "id": 147, "tenant_id": 1, "uuid": "bcdc1f19-c788-4379-bd6f-d03f4f87c702", "created_at": "2022-01-25T16:31:12.651Z", "results": [ { "id": 209, "job_id": 147, "payload": { "meta": { "ruleFilePaths": [ "rosterConstraints.drl" ], "secondsSpentLimit": 10 }, "score": { "feasible": false, "hardScore": -60, "initScore": 0, "softScore": 0, "solutionInitialized": true }, "spots": [ { "id": "reiskocher", "name": "Reiskocher", "maximumMinutes": 0, "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "maximumMinutes": 0, "requiredSkills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": "reiskocher", "employee": "mrsrice", "endDateTime": "2021-06-23T10:00", "startDateTime": "2021-06-23T09:00", "lockedEmployee": null, "requiredSkills": null }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": "reiskocher", "employee": "mrrice", "endDateTime": "2021-06-23T10:30", "startDateTime": "2021-06-23T09:30", "lockedEmployee": null, "requiredSkills": null }, { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": "lavabo", "employee": "mrsrice", "endDateTime": "2021-06-23T09:00", "startDateTime": "2021-06-23T08:30", "lockedEmployee": null, "requiredSkills": null } ], "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerYear": 0, "maximumMinutesPerMonth": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "skills": [ "reiskochen", "reiswaschen" ], "contract": "fulltime" }, { "id": "mrrice", "name": "Mr Rice", "skills": [ "reiswaschen" ], "contract": "fulltime" } ], "availabilities": [] }, "final": true, "created_at": "2022-01-25T16:31:25.276Z" } ] }
4.3.2. Validate Roster
To validate a Roster
against the given rules, you create a Job
of type
score_roster
. Here is an example given the above
example Roster
.
echo $example_roster | http $host/v1/tenants/$tenant_id/jobs/ \ Authorization:"Token ${auth_token}" \ webhook==$webhook_url \ type==score_roster
{"msg":"Job accepted.","webhook":"https://webhook.site/3d45188b-2932-4f37-8115-b65cfaadba26","job_id":"dfe1209d-7fad-4582-9e6c-b7551a037542"}
This is what the payload will look like. It includes the key
indictment
which has all the information regarding the given
rules. Each of them has various metadata including a score
. The
top level also includes the key results
which includes your
given Roster
, but has the overall score
updated.
{ "payload": { "meta": { "ruleFilePaths": [ "rosterConstraints.drl" ], "secondsSpentLimit": 10 }, "spots": [ { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "requiredSkills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": "reiskocher", "endDateTime": "2021-06-23T10:00", "startDateTime": "2021-06-23T09:00" }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": "reiskocher", "endDateTime": "2021-06-23T10:30", "startDateTime": "2021-06-23T09:30" }, { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": "lavabo", "endDateTime": "2021-06-23T09:00", "startDateTime": "2021-06-23T08:30" } ], "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerYear": 0, "maximumMinutesPerMonth": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "skills": [ "reiskochen", "reiswaschen" ], "contract": "fulltime" }, { "id": "mrrice", "name": "Mr Rice", "skills": [ "reiswaschen" ], "contract": "fulltime" } ] }, "started_at": "2021-12-02T15:56:25.367Z", "completed_at": "2021-12-02T15:56:28.047Z", "webhook": "https://webhook.site/3d45188b-2932-4f37-8115-b65cfaadba26", "indictment": { "charge-0": { "justification": { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ { "id": "reiskochen", "name": "Reis kochen" } ] }, "startDateTime": "2021-06-23T09:00", "endDateTime": "2021-06-23T10:00", "requiredSkills": null, "employee": null, "lockedEmployee": null }, "constraintMatchSet": [ { "constraintName": "shiftRemainsUnassigned", "justificationList": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ { "id": "reiskochen", "name": "Reis kochen" } ] }, "startDateTime": "2021-06-23T09:00", "endDateTime": "2021-06-23T10:00", "requiredSkills": null, "employee": null, "lockedEmployee": null } ], "score": { "initScore": 0, "hardScore": -60, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintId": "defaultpkg/shiftRemainsUnassigned" } ], "score": { "initScore": 0, "hardScore": -60, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintMatchCount": 1 }, "charge-1": { "justification": { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ { "id": "reiskochen", "name": "Reis kochen" } ] }, "startDateTime": "2021-06-23T09:30", "endDateTime": "2021-06-23T10:30", "requiredSkills": null, "employee": null, "lockedEmployee": null }, "constraintMatchSet": [ { "constraintName": "shiftRemainsUnassigned", "justificationList": [ { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ { "id": "reiskochen", "name": "Reis kochen" } ] }, "startDateTime": "2021-06-23T09:30", "endDateTime": "2021-06-23T10:30", "requiredSkills": null, "employee": null, "lockedEmployee": null } ], "score": { "initScore": 0, "hardScore": -60, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintId": "defaultpkg/shiftRemainsUnassigned" } ], "score": { "initScore": 0, "hardScore": -60, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintMatchCount": 1 }, "charge-2": { "justification": { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": { "id": "lavabo", "name": "Lavabo", "requiredSkills": [ { "id": "reiswaschen", "name": "Reis waschen" } ] }, "startDateTime": "2021-06-23T08:30", "endDateTime": "2021-06-23T09:00", "requiredSkills": null, "employee": null, "lockedEmployee": null }, "constraintMatchSet": [ { "constraintName": "shiftRemainsUnassigned", "justificationList": [ { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": { "id": "lavabo", "name": "Lavabo", "requiredSkills": [ { "id": "reiswaschen", "name": "Reis waschen" } ] }, "startDateTime": "2021-06-23T08:30", "endDateTime": "2021-06-23T09:00", "requiredSkills": null, "employee": null, "lockedEmployee": null } ], "score": { "initScore": 0, "hardScore": -30, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintId": "defaultpkg/shiftRemainsUnassigned" } ], "score": { "initScore": 0, "hardScore": -30, "softScore": 0, "feasible": false, "solutionInitialized": true }, "constraintMatchCount": 1 } }, "type": "score_roster", "state": "complete", "id": 92, "tenant_id": 179, "uuid": "dfe1209d-7fad-4582-9e6c-b7551a037542", "created_at": "2021-12-02T15:56:25.310Z", "results": [ { "id": 107, "job_id": 92, "payload": { "meta": { "ruleFilePaths": [ "rosterConstraints.drl" ], "secondsSpentLimit": 10 }, "score": { "feasible": false, "hardScore": -150, "initScore": -3, "softScore": 0, "solutionInitialized": false }, "spots": [ { "id": "reiskocher", "name": "Reiskocher", "requiredSkills": [ "reiskochen" ] }, { "id": "lavabo", "name": "Lavabo", "requiredSkills": [ "reiswaschen" ] } ], "shifts": [ { "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b", "spot": "reiskocher", "employee": null, "endDateTime": "2021-06-23T10:00", "startDateTime": "2021-06-23T09:00", "lockedEmployee": null, "requiredSkills": null }, { "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7", "spot": "reiskocher", "employee": null, "endDateTime": "2021-06-23T10:30", "startDateTime": "2021-06-23T09:30", "lockedEmployee": null, "requiredSkills": null }, { "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee", "spot": "lavabo", "employee": null, "endDateTime": "2021-06-23T09:00", "startDateTime": "2021-06-23T08:30", "lockedEmployee": null, "requiredSkills": null } ], "skills": [ { "id": "reiskochen", "name": "Reis kochen" }, { "id": "reiswaschen", "name": "Reis waschen" } ], "contracts": [ { "id": "fulltime", "name": "Fulltime", "maximumMinutesPerDay": 0, "maximumMinutesPerWeek": 2550, "maximumMinutesPerYear": 0, "maximumMinutesPerMonth": 0 } ], "employees": [ { "id": "mrsrice", "name": "Mrs Rice", "skills": [ "reiskochen", "reiswaschen" ], "contract": "fulltime" }, { "id": "mrrice", "name": "Mr Rice", "skills": [ "reiswaschen" ], "contract": "fulltime" } ], "availabilities": [] }, "final": true, "created_at": "2021-12-02T15:56:28.035Z" } ] }
4.4. Get Job
(Polling)
echo $compute_roster | jq .job_id
http --ignore-stdin --pretty format \ $host/v1/tenants/$tenant_id/jobs/$job_id \ Authorization:"Token ${auth_token}"
{ "coercion": "spec", "in": [ "request", "path-params" ], "problems": [ { "in": [ "tenant_id" ], "path": [ "tenant_id" ], "pred": "clojure.core/int?", "val": "nil", "via": [ "spec$220/tenant_id" ] } ], "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$220/tenant_id :spec$220/uuid]), :type :map, :leaf? false})", "type": "reitit.coercion/request-coercion", "value": { "tenant_id": "nil", "uuid": "nil" } }
5. Appendix
5.1. Schema defintions
5.1.1. Roster
- Spec
(s/def ::secondsSpentLimit int?) (s/def ::ruleFilePath string?) (s/def ::ruleFilePaths (s/coll-of ::ruleFilePath)) (s/def ::meta (s/keys :req-un [::ruleFilePaths ::secondsSpentLimit])) (def ^:private id? (partial re-matches #"[\w-:.,;/]+")) (s/def ::id (s/and string? id?)) (s/def ::name string?) (s/def ::maximumMinutesPerDayPerEmployee int?) (s/def ::maximumMinutesPerWeekPerEmployee int?) (s/def ::maximumMinutesPerMonthPerEmployee int?) (s/def ::maximumMinutesPerYearPerEmployee int?) (s/def ::skill (s/keys :req-un [::id ::name] :opt-un [::maximumMinutesPerWeekPerEmployee])) (s/def ::skills (s/coll-of ::skill)) (s/def ::requiredSkills (s/coll-of ::id)) (s/def ::maximumMinutes int?) (s/def ::spot (s/keys :req-un [::id ::name] :opt-un [::requiredSkills ::maximumMinutes])) (s/def ::spots (s/coll-of ::spot)) (s/def ::maximumMinutesPerDay int?) (s/def ::maximumMinutesPerWeek int?) (s/def ::maximumMinutesPerMonth int?) (s/def ::maximumMinutesPerYear int?) (s/def ::contract (s/keys :req-un [::id ::name] :opt-un [::maximumMinutesPerDay ::maximumMinutesPerWeek ::maximumMinutesPerMonth ::maximumMinutesPerYear])) (s/def ::contracts (s/coll-of ::contract)) (s/def :id/contract ::id) (s/def :id/skills (s/coll-of ::id)) (s/def ::employee (s/keys :req-un [::id ::name :id/contract :id/skills])) (s/def ::employees (s/coll-of ::employee)) (s/def :id/spot ::id) (s/def :id/employee ::id) (s/def ::lockedEmployee ::id) (s/def ::shift (s/or :optimize-for-employee (s/keys :req-un [::id :id/spot ::startDateTime ::endDateTime] :opt-un [::requiredSkills :id/employee ::lockedEmployee]) :optimize-for-spot (s/keys :req-un [::id :id/employee ::startDateTime ::endDateTime] :opt-un [::requiredSkills :id/spot ::lockedEmployee]))) (s/def ::shifts (s/coll-of ::shift)) (def ^:private datetime? (partial re-matches #"\d{4}-\d\d-\d\dT\d\d:\d\d")) (s/def ::startDateTime (s/and string? datetime?)) (s/def ::endDateTime (s/and string? datetime?)) (s/def ::state #{"UNAVAILABLE" "UNDESIRED" "DESIRED"}) (s/def ::availability (s/keys :req-un [::startDateTime ::endDateTime ::state :id/employee])) (s/def ::availabilities (s/coll-of ::availability)) (s/def ::initScore int?) (s/def ::hardScore int?) (s/def ::softScore int?) (s/def ::feasible boolean?) (s/def ::solutionInitialized boolean?) (s/def ::score (s/keys :req-un [::initScore ::hardScore ::softScore ::feasible ::solutionInitialized])) (s/def ::roster (s/keys :req-un [::skills ::spots ::contracts ::employees ::shifts] :opt-un [::meta ::availabilities ::score]))
- Swagger
{ "type": "object", "properties": { "skills": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "name": { "type": "string" } }, "required": [ "id", "name" ], "title": "spapi.roster/skill" } }, "spots": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "name": { "type": "string" }, "requiredSkills": { "type": "array", "items": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] } } }, "required": [ "id", "name" ], "title": "spapi.roster/spot" } }, "contracts": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "name": { "type": "string" }, "maximumMinutesPerDay": { "type": "integer", "format": "int64" }, "maximumMinutesPerWeek": { "type": "integer", "format": "int64" }, "maximumMinutesPerMonth": { "type": "integer", "format": "int64" }, "maximumMinutesPerYear": { "type": "integer", "format": "int64" } }, "required": [ "id", "name" ], "title": "spapi.roster/contract" } }, "employees": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "name": { "type": "string" }, "contract": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "skills": { "type": "array", "items": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] } } }, "required": [ "id", "name", "contract", "skills" ], "title": "spapi.roster/employee" } }, "shifts": { "type": "array", "items": { "type": "object", "properties": { "spot": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "startDateTime": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "endDateTime": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "requiredSkills": { "type": "array", "items": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] } }, "employee": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "lockedEmployee": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] } }, "required": [ "spot", "startDateTime", "endDateTime" ], "title": "spapi.roster/shift" } }, "meta": { "type": "object", "properties": { "ruleFilePaths": { "type": "array", "items": { "type": "string" } }, "secondsSpentLimit": { "type": "integer", "format": "int64" } }, "required": [ "ruleFilePaths", "secondsSpentLimit" ], "title": "spapi.roster/meta" }, "availabilities": { "type": "array", "items": { "type": "object", "properties": { "startDateTime": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "endDateTime": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] }, "state": { "enum": [ "DESIRED", "UNAVAILABLE", "UNDESIRED" ], "type": "string" }, "employee": { "type": "string", "x-allOf": [ { "type": "string" }, {} ] } }, "required": [ "startDateTime", "endDateTime", "state", "employee" ], "title": "spapi.roster/availability" } }, "score": { "type": "object", "properties": { "initScore": { "type": "integer", "format": "int64" }, "hardScore": { "type": "integer", "format": "int64" }, "softScore": { "type": "integer", "format": "int64" }, "feasible": { "type": "boolean" }, "solutionInitialized": { "type": "boolean" } }, "required": [ "initScore", "hardScore", "softScore", "feasible", "solutionInitialized" ], "title": "spapi.roster/score" } }, "required": [ "skills", "spots", "contracts", "employees", "shifts" ], "title": "spapi.roster/roster" }