Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC 001 - Scheduling #35

Open
gskorokhod opened this issue Aug 19, 2024 · 2 comments
Open

RFC 001 - Scheduling #35

gskorokhod opened this issue Aug 19, 2024 · 2 comments
Assignees
Labels
area:logic Related to generating Essence and other business logic of the app area:ui Related to Svelte UI components rfc Request for comment. Used for detailed planning & discussion of planned features and changes

Comments

@gskorokhod
Copy link
Contributor

gskorokhod commented Aug 19, 2024

Overview

This RFC defines the new format for representing information about Tasks, Shifts and the assignment of Employees to Tasks, including the handling of date/time and recurrence information for Shifts, type definitions for the new Assignment, Task and Shift objects, serialisation / deserialisation of these objects, and their representation in the UI.

Its aims are to:

  • Create a flexible way for users to define recurring Shifts and the assignment of employees to tasks
  • Use existing standards for handling date/time and recurrence to reduce the amount of custom logic that we would need to write and to simplify the process of exporting employee assignments as iCal calendar files that can be imported into users' existing calendar and workforce planning software
  • Allow constraints to have a temporal element (i.e. apply only to specific scheduled shifts, or assignments on specific days of the week, rather than all the time)
  • Clearly outline requirements, data types and the general approach to implementation

Introduction

The main purpose of this app is to compute an optimal assignment of employees to tasks while satisfying a set of user-defined constraints, using a constraints solver.

As such, given the following set of facts:

  • A list of Employees, with relevant information about them (e.g. their qualification)
  • A list of Tasks, that represent a general kind of work that needs to be done
  • A list of Locations, that represent offices, buildings and other physical places where tasks can be done (See: TODO Locations RFC)
  • A list of Shifts, that represent a set of Tasks being done during specific recurring periods of time
  • A partial assignment of Employees to Tasks during a specific shift

As well as:

  • A list of Constraints, that represent user-defined rules operating on the above objects that need to be satisfied. (See: TODO Constraints RFC)

The goal is to find a complete assignment for a given time period, such that:

  • All Tasks for every occurrence of every shift have at least the minimum number of Employees assigned to them
  • All Constraints are satisfied

Requirements

As part of this, the following problems need to be solved:

  • Representing temporal information for Shifts - a recurring schedule for when they take place, as well as their duration.
    For example:
    • "There is a Day Shift from 09:00 to 17:00; It takes place on Mon-Sat every week, and consists of Tasks A, B, C, D"
    • "Due to additional workload, there is a temporary Night Shift from 17:00 to 1:00 (of the next day); It takes place every other day, from 19.08 to 19.09"
  • Representing similar temporal information for Constraints, such that a constraint can apply:
    • Throughout the full time period for which the schedule is being computed
    • For all occurrences of a given Shift
    • On specific days, which can overlap with individual occurrences of one or more Shifts
    • For example:
      • "Task A cannot be done at Location B"
        (ever; regardless of which shift Task B is part of or the date when it occurs)
      • "There must be at least 2 people trained in First Aid on every Night Shift"
        (this applies to all Night Shifts on all days, but does not apply to other shifts on the same day)
      • "John cannot work on Mondays"
        (this applies weekly on every Monday - John cannot be assigned to any Task of any Shift on these days)
      • "Kate is on holiday from 19.08 to 25.08"
        (this applies every day within a specified time period, but does not apply for the rest of the scheduling period)
  • Mapping general information about Shifts (e.g: "Tasks A, B need to be done from 9:00 to 17:00 every weekday") to specific Assignments (e.g.: "Jane is doing Task A on Monday at Office 1, and there are 2 more unfilled slots for this task, at this location, at this time")

The solution needs to be flexible to accommodate different approaches to scheduling that can exist in different industries

Design

The core ideas are to:

  • Separate:
    • Tasks (which will now represent a generic kind of work that needs to be done, with no temporal or assignment information)
    • Shifts (which define recurring schedules for a set of tasks)
    • Assignments (which represent specific Employees doing specific Tasks at a specific Location doing a specific non-recurring period of time - i.e. an "occurrence" of a Shift)
  • Use iCal Recurrence Rules (See: RFC5545, and its implementation rrule.js) for representing scheduling information for Shifts and Constraints
  • Use the a shift's recurrence rule and duration to generate a list of its specific occurrences
  • Have Assignments reference a Shift object, along with a number representing the n-th occurrence of the Shift
  • Instead of storing all empty Assignments for Shifts, create them as needed and only store the non-empty ones.
    (Empty assignments can be added as needed when passing data to the solver)
  • Implement an Outlook-style drag-and-drop UI for rescheduling Shifts
  • Implement a Kanban-style "Assignment" view for viewing rich information about Tasks and Assignments within one specific Shift, and editing the assignments

This approach:

  • Uses a standard and highly flexible format for handling scheduling information
  • Decouples Assignments from Tasks and Shifts, such that different people can be assigned to different tasks on different days, but their date/time is still tied directly to the shift's schedule
    (Thus, when the shift is rescheduled, all Assignment information automatically updates with it)
  • Creates a simple and familiar UI for manipulating them

Examples

Note

Types in these examples have been simplified; Full and accurate type definitions are given bellow

Recurrence Example

Let Shift 1 has the following properties:

shift1 = {
	rrule = "DTSTART:20240201T103000ZRRULE:FREQ=WEEKLY;UNTIL=20241230T103000Z;INTERVAL=2;WKST=MO;BYDAY=MO,FR",
	duration = {
		hours: 8,
		minutes: 0
	},
	tasks = ["A", "B"]
}
  • Its starting date/time is given by the recurrence rule (01.02.2024, 9:30)
  • It occurs on Mondays and Fridays every other week
  • It lasts for 8 hours and consists of tasks "A", "B"
  • The starting date/times of every occurrence are computed by expanding the rrule:
# Day Date Time Timezone
1 Fri 02 Feb 2024 10:30:00 GMT
2 Mon 12 Feb 2024 10:30:00 GMT
... ... ... ... ...
  • The occurrences of the shift can be easily computed by adding the duration:
occurrences = [
	["02-02-2024T10:30:00Z", "02-02-2024T18:30:00Z"],
	["12-02-2024T10:30:00Z", "12-02-2024T18:30:00Z"],
	...
]

Assignment Example

assignment = {
	task: "A",
	location: "Office 1",
	people: ["Bob", "Alice"],
	shift: shift1,
	occurrence: 2
}

This assignment refers to the 2nd occurrence of dayShift, i.e. the time period of 10:30 - 18:30 on 12.02.2024.

It represents the fact that Alice and Bob have been assigned to do Task A during this time as part of the shift Shift 1, and that they are working at Office 1.

Types

Serialisation / Deserialisation

Note

Assignments, Tasks and Shifts need to be serialisable to be stored in local storage. Unless we write custom serialisation / deserialisation code for all of them (and maintain it as the types change), we can only use interfaces with primitive types or Dates as their fields - these can be serialised and parsed by devalue as is, without any extra configuration

Time

A helper type to represent a duration of time

interface Time {
	hours: number;
	minutes: number;
	seconds: number;
}

// Create a Time object from a number (milliseconds since unix epoch)
function fromMs(ms: number): Time;

// Convert a Time to milliseconds
function toMs(time: Time): number;

Task

The Task type will no longer contain a list of people assigned to it. Other fields remain unchanged:

interface TaskProps {
  description: string;
  icon: IconType;
  max_people: number;
  min_people: number;
  name: string;
  required_skill_uuids: string[];
}

// Construct a Task (same properties + a uuid and a type enum)
function createTask(props: TaskProps): Task;

// Get a list of skills required to do a Task
function getRequiredSkillsForTask(task: Task): Skill[];

New functions will be added to access Shifts and Assignments that reference a Task:

/*
 Get all assignments that reference this task within
 a given time period and a given list of shifts
 (if not specified, all shifts will be used)
*/
getAssignmentsForTask(task: Task, after: Date, before: Date, inShifts?: Shift[]): Assignment[]

// Get all shifts that reference this task
getShiftsWithTask(task: Task): Shift[]

Note

Since rrule will be evaluated dynamically when fetching the assignments, we have to specify a time period, otherwise the recurrence may be infinite. inShifts is an optional parameter to restrict the search to specific shifts, for performance reasons (evaluating rrule's can be expensive)

Assignment

The Assignment type will now reference only one Task (not a list of them), and will not contain any temporal information of its own. Instead, it would reference a Shift and contain the number of the Shift's occurrence for which it is scheduled.
It may also contain the optional parameters start_offset (allowing the Assignment to start later than the shift starts) and end_offset (allowing the Assignment to end earlier than the shift starts). If not specified, these are assumed to be 0.

interface AssignmentProps {
	task_uuid: string;
	location_uuid: string;
	shift_uuid: string;
	people_uuids: string[];
	occurrence: number;
	start_offset?: Time;
	end_offset? Time;
}

// Get the task for this assignment
function getTaskForAssignment(assignment: Assignment): Task;

// Get the location for this assignment
function getLocationForAssignment(assignment: Assignment): Location;

// Get shift for this assignment
function getShiftForAssignment(assignment: Assignment): Shift;

// Get assigned people for this assignment
function getPeopleForAssignment(assignment: Assignment): Person[];

Shift

A shift defines a recurring schedule on which certain Tasks need to be done.

// Represents the properties of a Shift
interface ShiftProps {
	rrule: string;
	duration: Time;
	task_uuids: string[];
	default_assignment_props?: Partial<AssignmentProps>;
}

// Helper type that represents a specific occurrence of a shift
interface ShiftOccurrence {
	n: number;
	shift: Shift;
	dtstart: Date;
	until: Date;
}

// Retrieves the start date of the given shift.
function getStartDate(shift: Shift): Date;

// Retrieves the end date of the given shift, if defined.
// (undefined means indefinite recurrence)
function getEndDate(shift: Shift): Date | undefined;

// Retrieves a specific occurrence of the shift by its occurrence number.
function getOccurrence(n: number): ShiftOccurrence | undefined;

// Retrieves all occurrences of the shift between two dates.
function getOccurrences(after: Date, before: Date): ShiftOccurrence[];

// Retrieves the assignment associated with a specific shift occurrence.
function getAssignment(occ: ShiftOccurrence): Assignment | undefined;

// Retrieves all assignments for a shift between two dates.
function getAssignments(shift: Shift, after: Date, before: Date, create_missing: boolean = true): Assignment[];

UI

Assignments

  • A simplified version of the current Assignment card will be used to represent an assignment
  • A new Assignments view will be added, showing assignments for a given shift during a given period of time:
    • The columns will represent individual occurrences of the shift
    • Columns will be populated with Assignment cards
    • Dragging an Assignment card to another column will reschedule it to that occurrence of the shift
    • Tasks that have no Assignment for that occurrence of the shift will be shown at the bottom of the column
  • A calendar sidebar will be used to adjust the time period to display

Shifts

  • For now, only Weekly and Daily recurrence patterns should be supported by the UI. An existing rrule component can be used to generate the recurrence rules for shifts.
  • An Outlook-style calendar UI will be used to display shifts.
    Dragging shift occurrences would reschedule them (and other shifts in their series), and vertically dragging them would change their start time and duration
  • Assignments can be displayed as compact badges inside Shift blocks
  • A calendar sidebar will be used to adjust the time period to display
@gskorokhod gskorokhod added area:ui Related to Svelte UI components area:logic Related to generating Essence and other business logic of the app rfc Request for comment. Used for detailed planning & discussion of planned features and changes labels Aug 19, 2024
@gskorokhod gskorokhod self-assigned this Aug 19, 2024
@gskorokhod
Copy link
Contributor Author

PR #38 implements the underlying logic for recurrence patterns, but we don't actually need this level of complexity for now

@gskorokhod
Copy link
Contributor Author

PR #52 (and subsequent minor fixes #53 #54) implement a basic recurrence editor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:logic Related to generating Essence and other business logic of the app area:ui Related to Svelte UI components rfc Request for comment. Used for detailed planning & discussion of planned features and changes
Projects
None yet
Development

No branches or pull requests

1 participant