diff --git a/src/main/java/org/olat/core/util/DateUtils.java b/src/main/java/org/olat/core/util/DateUtils.java
index 3786636b3283fbe54a40e020fa52b42323bfd93c..7da4c0386ad19edabf8de95ada24cdb5c33e9060 100644
--- a/src/main/java/org/olat/core/util/DateUtils.java
+++ b/src/main/java/org/olat/core/util/DateUtils.java
@@ -63,11 +63,41 @@ public class DateUtils {
 		return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
 	}
 	
+	public static boolean isSameDate(Date date1, Date date2) {
+		Calendar cal1 = Calendar.getInstance();
+		cal1.setTime(date1);
+		Calendar cal2 = Calendar.getInstance();
+		cal2.setTime(date2);
+		return isSameDate(cal1, cal2);
+	}
+
+	protected static boolean isSameDate(Calendar cal1, Calendar cal2) {
+		return isSameDay(cal1, cal2) && isSameTime(cal1, cal2);
+	}
+	
+	public static boolean isSameDay(Date date1, Date date2) {
+		Calendar cal1 = Calendar.getInstance();
+		cal1.setTime(date1);
+		Calendar cal2 = Calendar.getInstance();
+		cal2.setTime(date2);
+		return isSameDay(cal1, cal2);
+	}
+
+	public static boolean isSameDay(Calendar cal1, Calendar cal2) {
+		return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
+				cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+				cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
+	}
+	
 	public static boolean isSameTime(Date date1, Date date2) {
 		Calendar cal1 = Calendar.getInstance();
 		cal1.setTime(date1);
 		Calendar cal2 = Calendar.getInstance();
 		cal2.setTime(date2);
+		return isSameTime(cal1, cal2);
+	}
+
+	public static boolean isSameTime(Calendar cal1, Calendar cal2) {
 		return (cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND) &&
 				cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
 				cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR));
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java
index cc7b3ee2a31b35f0651d9602732282386785879d..c0794946d9a777d0eeed20a001be4df7ddf0054e 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentDataModel.java
@@ -19,15 +19,20 @@
  */
 package org.olat.course.nodes.appointments.ui;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.stream.Collectors;
 
 import org.olat.core.commons.persistence.SortKey;
+import org.olat.core.gui.components.form.flexible.elements.FlexiTableFilter;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FilterableFlexiTableModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiSortableColumnDef;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableDataModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableModelDelegate;
+import org.olat.core.util.DateUtils;
 
 /**
  * 
@@ -36,9 +41,14 @@ import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFl
  *
  */
 public class AppointmentDataModel extends DefaultFlexiTableDataModel<AppointmentRow>
-implements SortableFlexiTableDataModel<AppointmentRow> {
+implements SortableFlexiTableDataModel<AppointmentRow>, FilterableFlexiTableModel {
+	
+	public static final String FILTER_ALL = "all";
+	public static final String FILTER_PARTICIPATED = "participated";
+	public static final String FILTER_FUTURE = "future";
 	
 	private final Locale locale;
+	private List<AppointmentRow> backups;
 	
 	public AppointmentDataModel(FlexiTableColumnModel columnsModel, Locale locale) {
 		super(columnsModel);
@@ -50,6 +60,42 @@ implements SortableFlexiTableDataModel<AppointmentRow> {
 		List<AppointmentRow> rows = new SortableFlexiTableModelDelegate<>(orderBy, this, locale).sort();
 		super.setObjects(rows);
 	}
+	
+	@Override
+	public void setObjects(List<AppointmentRow> objects) {
+		super.setObjects(objects);
+		backups = objects;
+	}
+	
+	@Override
+	public void filter(String searchString, List<FlexiTableFilter> filters) {
+		if (filters == null || filters.isEmpty() || FILTER_ALL.equals(filters.get(0).getFilter())) {
+			super.setObjects(backups);
+		} else {
+			List<String> filterKeys = filters.stream().map(FlexiTableFilter::getFilter).collect(Collectors.toList());
+			Date now = new Date();
+			List<AppointmentRow> filteredRows = backups.stream()
+					.filter(r -> apply(filterKeys, r, now))
+					.collect(Collectors.toList());
+			super.setObjects(filteredRows);
+		}
+	}
+	
+	private boolean apply(List<String> filterKeys, AppointmentRow row, Date date) {
+		if (filterKeys.contains(FILTER_PARTICIPATED) && row.getParticipation() != null) {
+			return true;
+		}
+		if (filterKeys.contains(FILTER_FUTURE)) {
+			Date end = row.getAppointment().getEnd();
+			end = DateUtils.isSameDate(end, row.getAppointment().getStart())
+					? DateUtils.setTime(end, 23, 59, 59)
+					: end;
+			if (date.before(end)) {
+				return true;
+			}
+		}
+		return false;
+	}
 
 	@Override
 	public Object getValueAt(int row, int col) {
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java
index aebe9e86cab11a06f4a82681ca73a79264fb2bf1..69d7831f2c7f99070e975ebffd08058001a433c1 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java
@@ -24,7 +24,6 @@ import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 
-import org.apache.commons.lang.time.DateUtils;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.date.DateComponentFactory;
@@ -32,6 +31,7 @@ import org.olat.core.gui.components.date.DateElement;
 import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
 import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
+import org.olat.core.gui.components.form.flexible.elements.FlexiTableFilter;
 import org.olat.core.gui.components.form.flexible.elements.FormLink;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
 import org.olat.core.gui.components.form.flexible.impl.FormEvent;
@@ -49,6 +49,7 @@ import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
 import org.olat.core.gui.control.generic.modal.DialogBoxController;
 import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
+import org.olat.core.util.DateUtils;
 import org.olat.core.util.StringHelper;
 import org.olat.course.nodes.appointments.Appointment;
 import org.olat.course.nodes.appointments.Appointment.Status;
@@ -115,6 +116,10 @@ public abstract class AppointmentListController extends FormBasicController impl
 	
 	protected abstract String getTableCssClass();
 	
+	protected abstract List<String> getFilters();
+	
+	protected abstract List<String>  getDefaultFilters();
+	
 	protected abstract String getPersistedPreferencesId();
 	
 	protected abstract List<AppointmentRow> loadModel();
@@ -210,6 +215,39 @@ public abstract class AppointmentListController extends FormBasicController impl
 		VelocityContainer rowVC = createVelocityContainer("appointment_row");
 		rowVC.setDomReplacementWrapperRequired(false);
 		tableEl.setRowRenderer(rowVC, this);
+		
+		initFilters();
+	}
+
+	protected void initFilters() {
+		List<String> filters = getFilters();
+		if (filters != null && !filters.isEmpty()) {
+			List<FlexiTableFilter> tableFilters = new ArrayList<>(3);
+			List<String> defaultFilters = getDefaultFilters();
+			List<FlexiTableFilter> selectedFilters = new ArrayList<>(defaultFilters.size());
+			if (filters.contains(AppointmentDataModel.FILTER_PARTICIPATED)) {
+				FlexiTableFilter filter = new FlexiTableFilter(translate("filter.participated"), AppointmentDataModel.FILTER_PARTICIPATED, false);
+				tableFilters.add(filter);
+				if (defaultFilters.contains(AppointmentDataModel.FILTER_PARTICIPATED)) {
+					selectedFilters.add(filter);
+				}
+			}
+			if (filters.contains(AppointmentDataModel.FILTER_FUTURE)) {
+				FlexiTableFilter filter = new FlexiTableFilter(translate("filter.future"), AppointmentDataModel.FILTER_FUTURE, false);
+				tableFilters.add(filter);
+				if (defaultFilters.contains(AppointmentDataModel.FILTER_FUTURE)) {
+					selectedFilters.add(filter);
+				}
+			}
+			tableFilters.add(FlexiTableFilter.SPACER);
+			FlexiTableFilter filter = new FlexiTableFilter(translate("filter.all"), AppointmentDataModel.FILTER_ALL, true);
+			tableFilters.add(filter);
+			if (defaultFilters.contains(AppointmentDataModel.FILTER_ALL)) {
+				selectedFilters.add(filter);
+			}
+			tableEl.setFilters("Filters", tableFilters, true);
+			tableEl.setSelectedFilters(selectedFilters);
+		}
 	}
 
 	private void updateModel() {
@@ -229,7 +267,7 @@ public abstract class AppointmentListController extends FormBasicController impl
 		String time = null;
 
 		boolean sameDay = DateUtils.isSameDay(begin, end);
-		boolean sameTime = org.olat.core.util.DateUtils.isSameTime(begin, end);
+		boolean sameTime = DateUtils.isSameTime(begin, end);
 		String startDate = StringHelper.formatLocaleDateFull(begin.getTime(), locale);
 		String startTime = StringHelper.formatLocaleTime(begin.getTime(), locale);
 		String endDate = StringHelper.formatLocaleDateFull(end.getTime(), locale);
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListEditController.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListEditController.java
index eb5416ad888574c13d522e93409fe7630c441115..1260e7f2c53603e2a01e83e284a6e7666011dbcb 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListEditController.java
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListEditController.java
@@ -22,6 +22,7 @@ package org.olat.course.nodes.appointments.ui;
 import static java.util.Collections.emptyList;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -63,6 +64,16 @@ public class AppointmentListEditController extends AppointmentListController {
 		return "o_edit";
 	}
 
+	@Override
+	protected List<String> getFilters() {
+		return Collections.singletonList(AppointmentDataModel.FILTER_FUTURE);
+	}
+
+	@Override
+	protected List<String> getDefaultFilters() {
+		return Collections.singletonList(AppointmentDataModel.FILTER_ALL);
+	}
+
 	@Override
 	protected String getPersistedPreferencesId() {
 		return "ap-appointment-edit";
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListSelectionController.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListSelectionController.java
index af347a9ec3979ecfaa7ca11ab54b55a086fdf74e..954748f6e7c1ef211c103fefebfaa97c9ffceae5 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListSelectionController.java
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListSelectionController.java
@@ -22,6 +22,7 @@ package org.olat.course.nodes.appointments.ui;
 import static java.util.Collections.emptyList;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -65,6 +66,16 @@ public class AppointmentListSelectionController extends AppointmentListControlle
 		return "o_selection";
 	}
 
+	@Override
+	protected List<String> getFilters() {
+		return Arrays.asList(AppointmentDataModel.FILTER_PARTICIPATED, AppointmentDataModel.FILTER_FUTURE);
+	}
+
+	@Override
+	protected List<String> getDefaultFilters() {
+		return Arrays.asList(AppointmentDataModel.FILTER_PARTICIPATED, AppointmentDataModel.FILTER_FUTURE);
+	}
+
 	@Override
 	protected String getPersistedPreferencesId() {
 		return "ap-appointment-selection";
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
index 0762ca3231edb55df6529757064382deb2be862b..bd123a1b40b022fe2eb6cef31276752a1fd4155d 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
@@ -66,6 +66,9 @@ error.select.appointment=Sie m\u00fcssen einen Termin ausw\u00e4hlen.
 error.select.participant=Sie m\u00fcssen einen Teilnehmer ausw\u00e4hlen.
 error.start.after.end=Das Ende darf nicht vor dem Start liegen.
 error.too.much.participations=Es gibt bereits {0} Teilnahmen.
+filter.all=Alle anzeigen
+filter.future=Zuk\u00fcnftig
+filter.participated=Ausgew\u00e4hlt
 full.day=Ganzer Tag
 general=Konfiguration
 mail.appointments.deleted.body=Liebe/r {0} <br><br>Folgende Termine wurden abgesagt.<br><br>{1}
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
index d84fbc900ae532d5f31976d1dd53e902a1c9074a..b00faaa14732f578858d842ba250c38e1e0fd465 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
@@ -65,6 +65,9 @@ error.select.appointment=You have to select an appointment.
 error.select.participant=You have to select a participant.
 error.start.after.end=The end date must not be before the start date.
 error.too.much.participations=There are already {0} participations.
+filter.all=Show all
+filter.future=Future
+filter.participated=Selected
 full.day=Full day
 general=Configuration
 mail.appointments.deleted.body=Dear {0} <br><br>The following appointments were deleted.<br><br>{1}