This answer uses a TreeTableView to represent the data rather than a TableView.
It uses a demonstration data model with hard-coded data.
The complicated nested lambda expressions could be replaced by procedural code mapping your actual model data to the TreeItem data structure which backs the TreeTableView.

import java.util.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;
@SuppressWarnings("unchecked")
public class TreeTableViewSample extends Application {
private final List<String> employeesNames = Arrays.asList(
"Ethan Williams",
"Emma Jones",
"Michael Brown"
);
private final Random random = new Random(42);
@Override
public void start(Stage stage) {
ObservableList<DutyAssignment> assignments =
determineAssignments(employeesNames);
TreeItem<DutyAssignment> assignmentTree = createAssignmentTree(assignments);
TreeTableView<DutyAssignment> dutyAssignmentView = createAssigmentTreeTableView(assignmentTree);
stage.setScene(new Scene(dutyAssignmentView));
stage.show();
}
private TreeTableView<DutyAssignment> createAssigmentTreeTableView(TreeItem<DutyAssignment> root) {
TreeTableColumn<DutyAssignment, String> employeeColumn =
new TreeTableColumn<>("Employee");
employeeColumn.setPrefWidth(120);
employeeColumn.setCellValueFactory(
param -> param.getValue().getValue().nameProperty()
);
TreeTableColumn<DutyAssignment, Workday> dayColumn =
new TreeTableColumn<>("Day");
dayColumn.setPrefWidth(100);
dayColumn.setCellValueFactory(param ->
param.getValue().getValue().dayProperty()
);
TreeTableColumn<DutyAssignment, Duty> dutyColumn =
new TreeTableColumn<>("Duty");
dutyColumn.setPrefWidth(100);
dutyColumn.setCellValueFactory(param ->
param.getValue().getValue().dutyProperty()
);
TreeTableView<DutyAssignment> dutyAssignmentView = new TreeTableView<>(root);
dutyAssignmentView.getColumns().setAll(employeeColumn, dayColumn, dutyColumn);
dutyAssignmentView.setShowRoot(false);
dutyAssignmentView.setPrefSize(380, 500);
return dutyAssignmentView;
}
private TreeItem<DutyAssignment> createAssignmentTree(ObservableList<DutyAssignment> assignments) {
TreeItem<DutyAssignment> root = new TreeItem<>(
new DutyAssignment("All Assignments", null, null)
);
root.setExpanded(true);
employeesNames.stream()
.sorted()
.forEach(employeeName -> {
TreeItem<DutyAssignment> employeeTitleItem = new TreeItem<>(
new DutyAssignment(employeeName, null, null)
);
root.getChildren().add(employeeTitleItem);
employeeTitleItem.setExpanded(true);
assignments.stream()
.sorted()
.filter(assignment -> employeeName.equals(assignment.getName()))
.forEach(dutyAssignment -> {
TreeItem<DutyAssignment> assignmentLineItem = new TreeItem<>(
new DutyAssignment(null, dutyAssignment.getDay(), dutyAssignment.getDuty())
);
employeeTitleItem.getChildren().add(assignmentLineItem);
});
});
return root;
}
private ObservableList<DutyAssignment> determineAssignments(List<String> employeesNames) {
ObservableList<DutyAssignment> assignments = FXCollections.observableArrayList();
for (String employeeName : employeesNames) {
for (Workday day : Workday.values()) {
assignments.add(
new DutyAssignment(
employeeName,
day,
selectRandomDuty()
)
);
}
}
return assignments;
}
private Duty selectRandomDuty() {
return
Duty.values()[
random.nextInt(
Duty.values().length
)
];
}
public static void main(String[] args) {
Application.launch(TreeTableViewSample.class, args);
}
public class DutyAssignment implements Comparable<DutyAssignment> {
final private SimpleStringProperty name = new SimpleStringProperty();
final private SimpleObjectProperty<Workday> day = new SimpleObjectProperty<>();
final private SimpleObjectProperty<Duty> duty = new SimpleObjectProperty<>();
final private Comparator<DutyAssignment> nameDayDutyComparator =
Comparator.comparing(DutyAssignment::getName)
.thenComparing(DutyAssignment::getDay)
.thenComparing(DutyAssignment::getDuty);
public DutyAssignment(String name, Workday day, Duty duty) {
setName(name);
setDay(day);
setDuty(duty);
}
public String getName() {
return name.get();
}
public SimpleStringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public Workday getDay() {
return day.get();
}
public SimpleObjectProperty<Workday> dayProperty() {
return day;
}
public void setDay(Workday day) {
this.day.set(day);
}
public Duty getDuty() {
return duty.get();
}
public SimpleObjectProperty<Duty> dutyProperty() {
return duty;
}
public void setDuty(Duty duty) {
this.duty.set(duty);
}
@Override
public int compareTo(DutyAssignment o) {
return nameDayDutyComparator.compare(this, o);
}
}
public enum Duty {
WRITING, EDITING, COLORING, COMPOSITING
}
public enum Workday {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY
}
}