Hi,
I spent a good amount of time coming up with some sort of algorithm, and ended up with a greedy approach that seems to do a decent job. I had a real good time coding it 
Ok, so you have to start with a data frame in the form like this one (here I generate a random one)
library(dplyr)
library(lubridate)
set.seed(10)
#Tasks to be done (Random generated), replace with your data
#Name, start and end must be columns present
n = 8
tasks = data.frame(
name = LETTERS[1:n],
start = sample(8:14, n, replace = T)) %>%
mutate(end = start + sample(0:2, n, replace = T),
start = paste0(start, sample(c(":00", ":15"), n, replace = T)),
end = paste0(end, sample(c(":30", ":45"), n, replace = T)))
tasks
#> name start end
#> 1 A 10:00 12:45
#> 2 B 8:15 10:30
#> 3 C 9:15 11:30
#> 4 D 11:00 12:30
#> 5 E 14:15 16:30
#> 6 F 13:00 14:45
#> 7 G 14:00 15:45
#> 8 H 14:15 14:30
Once you have your data in a data frame called 'tasks' with these 3 columns, you can run the rest of the code:
library(dplyr)
library(lubridate)
#Convert the time to a numeric scale
tasks = tasks %>% mutate(
taskId = 1:n,
iStart = hm(start)$hour*60 + hm(start)$minute,
iEnd = hm(end)$hour*60 + hm(end)$minute,
iEnd = iEnd - min(iStart) + 1,
iStart = iStart - min(iStart) + 1,
duration = iEnd - iStart, employeeId = ""
)
#Quick check that input is correct...
if(any(tasks$iStart > tasks$iEnd)){
stop("Start time must be lower than End in all cases!")
}
#Create a time table of all tasks (matrix with rows tasks and columns time)
iEndTime = max(tasks$iEnd)
timeTable = mapply(function(taskId, iStart, iEnd){
y = rep(NA, iEndTime)
y[iStart:(iEnd - 1)] = taskId
y
}, tasks$taskId, tasks$iStart, tasks$iEnd) %>% t()
timeTable = rbind(timeTable, NA) #add one blank row for code to run properly (1 row is not a matrix)
#Max employees needed at any time
maxEmpl = max(apply(timeTable, 2, function(x) sum(!is.na(x))))
employees = data.frame(id = 1:maxEmpl, timeSpent = 0, nextFreeTime = 1)
#Start at time one
currentTime = 1
#Continue as long as there are unassigned tasks
while(sum(tasks$employeeId == "") != 0){
#Find the next time when one or more tasks iStart
nextTasks = timeTable[,currentTime]
nextTasks = nextTasks[!is.na(nextTasks)]
#Assign each task to the employee that is free
for(nextTask in nextTasks){
#Employee with most time spent on tasks first filled-up
assignTaskTo = employees %>% filter(currentTime >= nextFreeTime) %>%
filter(timeSpent == max(timeSpent)) %>% slice(1) %>% pull(id)
#Remove the task from the time table
timeTable = timeTable[-which((tasks %>% filter(employeeId == "") %>% pull(taskId)) == nextTask),]
#Update the task and employee table
tasks[tasks$taskId == nextTask, "employeeId"] = assignTaskTo
employees[employees$id == assignTaskTo, c("timeSpent", "nextFreeTime")] =
c( employees[employees$id == assignTaskTo, "timeSpent"] +
tasks[tasks$taskId == nextTask, "duration"],
tasks[tasks$taskId == nextTask, "iEnd"])
}
#Get iStart time of the next unassigned task(s)
currentTime = if(sum(tasks$employeeId == "") != 0) {
which(cumsum(colSums(timeTable, na.rm = T)) > 0)[1]
}
}
#Updated tasks list
tasks = tasks %>% arrange(iStart, iEnd) %>% select(-iStart, -iEnd)
tasks
#> name start end taskId duration employeeId
#> 1 B 8:15 10:30 2 135 1
#> 2 C 9:15 11:30 3 135 2
#> 3 A 10:00 12:45 1 165 3
#> 4 D 11:00 12:30 4 90 1
#> 5 F 13:00 14:45 6 105 1
#> 6 G 14:00 15:45 7 105 3
#> 7 H 14:15 14:30 8 15 4
#> 8 E 14:15 16:30 5 135 2
As you can see, the results is 3 more columns added to your tasks data frame: a unique ID, the duration of the task in minutes, and the employee Id it's assigned to. The code generates as many employees as needed to complete all tasks, based on the max number of tasks that are going on at the same time at any given moment.
You can then use this new task data frame to generate other summaries like this one:
#Summary of tasks per employee and time spent on tasks
employeeStats = tasks %>% group_by(employeeId) %>%
summarise(
startTime = start[1], endTime = end[n()],
timeOnTasks = sum(duration),
taskList = paste(name, collapse = ", "),
.groups = "drop")
employeeStats
#> # A tibble: 4 x 5
#> employeeId startTime endTime timeOnTasks taskList
#> <chr> <chr> <chr> <dbl> <chr>
#> 1 1 8:15 14:45 330 B, D, F
#> 2 2 9:15 16:30 270 C, E
#> 3 3 10:00 15:45 270 A, G
#> 4 4 14:15 14:30 15 H
The code is written so that the max number of tasks are assigned to the first employee in the list, and others get filled up once the previous ones are busy. I've put comments in the code to explain most steps, but I think running it yourself line by line might give you more insight.
Hope this helps,
PJ