diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py
index 098a31aabd9b1032c24cd7a5d8b1ff0530df86b6..08be1500d63dfb7898b9a940659f217ece424973 100644
--- a/smash/web/api_urls.py
+++ b/smash/web/api_urls.py
@@ -16,7 +16,7 @@ Including another URLconf
 from django.conf.urls import url
 
 from web.api_views import worker, location, subject, appointment_type, appointment, configuration, daily_planning, \
-    redcap, flying_team
+    redcap, flying_team, visit
 
 urlpatterns = [
     # appointments
@@ -38,6 +38,10 @@ urlpatterns = [
         name='web.api.subjects.columns'),
     url(r'^subject_types', subject.types, name='web.api.subject_types'),
 
+    # visits data
+    url(r'^visits/(?P<visit_list_type>[A-z]+)$', visit.visits, name='web.api.visits'),
+    url(r'^visits:columns/(?P<visit_list_type>[A-z]+)$', visit.get_visit_columns, name='web.api.visits.columns'),
+
     # locations
     url(r'^locations$', location.locations, name='web.api.locations'),
 
diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1e96dbcfc8944eeff773fbd95016f76f5ef2db1
--- /dev/null
+++ b/smash/web/api_views/serialization_utils.py
@@ -0,0 +1,61 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def bool_to_yes_no(val):
+    if val:
+        return "YES"
+    else:
+        return "NO"
+
+
+def flying_team_to_str(flying_team):
+    result = ""
+    if flying_team is not None:
+        result = unicode(flying_team)
+    return result
+
+
+def location_to_str(location):
+    result = ""
+    if location is not None:
+        result = unicode(location.name)
+    return result
+
+
+def serialize_date(date):
+    if date is not None:
+        result = date.strftime('%Y-%m-%d')
+    else:
+        result = ""
+    return result
+
+
+def serialize_datetime(date):
+    if date is not None:
+        result = date.strftime('%Y-%m-%d %H:%M')
+    else:
+        result = ""
+    return result
+
+
+def add_column(result, name, field_name, column_list, param, columns_used_in_study=None, visible_param=None,
+               sortable=True):
+    add = True
+    if columns_used_in_study:
+        add = getattr(columns_used_in_study, field_name)
+    if add:
+        if visible_param is not None:
+            visible = visible_param
+        elif column_list is None:
+            visible = True
+        else:
+            visible = getattr(column_list, field_name)
+        result.append({
+            "type": field_name,
+            "name": name,
+            "filter": param,
+            "visible": visible,
+            "sortable": sortable
+        })
diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py
index ab558c01959f748304588899bebd91204d84ced0..9d2d67b3ac12f5afcc6fa32eaca82f4e1f57a0ce 100644
--- a/smash/web/api_views/subject.py
+++ b/smash/web/api_views/subject.py
@@ -5,6 +5,8 @@ from django.db.models import Count, Case, When, Min, Max
 from django.db.models import Q
 from django.http import JsonResponse
 
+from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \
+    serialize_date, serialize_datetime
 from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study, ContactAttempt
 from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID
 from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \
@@ -31,25 +33,6 @@ def referrals(request):
     })
 
 
-def add_column(result, name, field_name, column_list, param, columns_used_in_study=None, visible_param=None):
-    add = True
-    if columns_used_in_study:
-        add = getattr(columns_used_in_study, field_name)
-    if add:
-        if visible_param is not None:
-            visible = visible_param
-        elif column_list is None:
-            visible = True
-        else:
-            visible = getattr(column_list, field_name)
-        result.append({
-            "type": field_name,
-            "name": name,
-            "filter": param,
-            "visible": visible
-        })
-
-
 @login_required
 def get_subject_columns(request, subject_list_type):
     study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0]
@@ -80,7 +63,7 @@ def get_subject_columns(request, subject_list_type):
     add_column(result, "Postponed", "postponed", study_subject_columns, "yes_no_filter", study.columns)
     add_column(result, "Info sent", "information_sent", study_subject_columns, "yes_no_filter", study.columns)
     add_column(result, "Type", "type", study_subject_columns, "type_filter", study.columns)
-    add_column(result, "Edit", "edit", None, None)
+    add_column(result, "Edit", "edit", None, None, sortable=False)
     for visit_number in range(1, 9):
         visit_key = "visit_" + str(visit_number)
         add_column(result, "Visit " + str(visit_number), visit_key, None, "visit_filter",
@@ -321,20 +304,9 @@ def types(request):
     })
 
 
-def get_yes_no(val):
-    if val:
-        return "YES"
-    else:
-        return "NO"
-
-
 def serialize_subject(study_subject):
-    location = ""
-    if study_subject.default_location is not None:
-        location = study_subject.default_location.name
-    flying_team = ""
-    if study_subject.flying_team is not None:
-        flying_team = unicode(study_subject.flying_team)
+    location = location_to_str(study_subject.default_location)
+    flying_team = flying_team_to_str(study_subject.flying_team)
     visits = Visit.objects.filter(subject=study_subject).order_by('visit_number')
     serialized_visits = []
     for visit in visits:
@@ -359,17 +331,15 @@ def serialize_subject(study_subject):
             status = "UPCOMING"
         serialized_visits.append({
             "status": status,
-            "datetime_start": visit.datetime_begin.strftime('%Y-%m-%d'),
-            "datetime_end": visit.datetime_end.strftime('%Y-%m-%d'),
+            "datetime_start": serialize_date(visit.datetime_begin),
+            "datetime_end": serialize_date(visit.datetime_end),
         })
-    contact_reminder = study_subject.datetime_contact_reminder
-    if contact_reminder is not None:
-        contact_reminder = contact_reminder.strftime('%Y-%m-%d %H:%M')
+    contact_reminder = serialize_datetime(study_subject.datetime_contact_reminder)
     contact_attempts = ContactAttempt.objects.filter(subject=study_subject).order_by("-datetime_when")
     if len(contact_attempts) > 0:
         last_contact_attempt = contact_attempts[0]
-        last_contact_attempt_string = last_contact_attempt.datetime_when.strftime(
-            '%Y-%m-%d %H:%M') + "<br/>" + str(last_contact_attempt.worker) + "<br/> Success: " + get_yes_no(
+        last_contact_attempt_string = serialize_datetime(last_contact_attempt.datetime_when) + "<br/>" + str(
+            last_contact_attempt.worker) + "<br/> Success: " + bool_to_yes_no(
             last_contact_attempt.success) + "<br/>" + last_contact_attempt.comment
 
     else:
@@ -385,10 +355,10 @@ def serialize_subject(study_subject):
         "referral": study_subject.referral,
         "default_location": location,
         "flying_team": flying_team,
-        "dead": get_yes_no(study_subject.subject.dead),
-        "resigned": get_yes_no(study_subject.resigned),
-        "postponed": get_yes_no(study_subject.postponed),
-        "information_sent": get_yes_no(study_subject.information_sent),
+        "dead": bool_to_yes_no(study_subject.subject.dead),
+        "resigned": bool_to_yes_no(study_subject.resigned),
+        "postponed": bool_to_yes_no(study_subject.postponed),
+        "information_sent": bool_to_yes_no(study_subject.information_sent),
         "type": study_subject.get_type_display(),
         "id": study_subject.id,
         "visits": serialized_visits,
diff --git a/smash/web/api_views/visit.py b/smash/web/api_views/visit.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba2258b9f19eae217529aefc70ec98b99521deab
--- /dev/null
+++ b/smash/web/api_views/visit.py
@@ -0,0 +1,254 @@
+import logging
+
+from django.contrib.auth.decorators import login_required
+from django.db.models import Q
+from django.http import JsonResponse
+
+from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \
+    serialize_date
+from web.models import AppointmentType, Appointment
+from web.models import SubjectColumns
+from web.models import Visit, Study, VisitColumns, StudyVisitList, StudyColumns
+from web.models.constants import GLOBAL_STUDY_ID
+from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_EXCEEDED_TIME, VISIT_LIST_UNFINISHED, \
+    VISIT_LIST_MISSING_APPOINTMENTS, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, \
+    VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT
+from web.views.notifications import get_unfinished_visits, get_active_visits_with_missing_appointments, \
+    get_approaching_visits_without_appointments, get_approaching_visits_for_mail_contact, get_exceeded_visits
+
+logger = logging.getLogger(__name__)
+
+
+# noinspection PyUnusedLocal
+@login_required
+def get_visit_columns(request, visit_list_type):
+    study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0]
+    study_visit_lists = StudyVisitList.objects.filter(study=study, type=visit_list_type)
+    if len(study_visit_lists) > 0:
+        visit_list = study_visit_lists[0]
+        visit_columns = visit_list.visible_visit_columns
+        visit_subject_columns = visit_list.visible_subject_columns
+        visit_subject_study_columns = visit_list.visible_study_subject_columns
+    else:
+        visit_list = StudyVisitList()
+        visit_columns = VisitColumns()
+        visit_subject_columns = SubjectColumns()
+        visit_subject_study_columns = StudyColumns()
+
+    result = []
+    add_column(result, "First name", "first_name", visit_subject_columns, "string_filter")
+    add_column(result, "Last name", "last_name", visit_subject_columns, "string_filter")
+    add_column(result, "Location", "default_location", visit_subject_study_columns, "location_filter",
+               study.columns)
+    add_column(result, "Flying team location", "flying_team", visit_subject_study_columns, "flying_team_filter",
+               study.columns)
+    add_column(result, "Visit begins", "datetime_begin", visit_columns, None)
+    add_column(result, "Visit ends", "datetime_end", visit_columns, None)
+    add_column(result, "Finished", "is_finished", visit_columns, "yes_no_filter")
+    add_column(result, "Post mail sent", "post_mail_sent", visit_columns, "yes_no_filter")
+    add_column(result, "Visit number", "visit_number", visit_columns, "integer_filter")
+    add_column(result, "Appointments in progress", "visible_appointment_types_in_progress", visit_list,
+               "appointment_type_filter", sortable=False)
+    add_column(result, "Done appointments", "visible_appointment_types_done", visit_list, "appointment_type_filter",
+               sortable=False)
+    add_column(result, "Missing appointments", "visible_appointment_types_missing", visit_list,
+               "appointment_type_filter", sortable=False)
+    add_column(result, "All appointments", "visible_appointment_types", visit_columns, "appointment_type_filter",
+               sortable=False)
+    add_column(result, "Edit", "edit", None, None, sortable=False)
+
+    return JsonResponse({"columns": result})
+
+
+# noinspection PyUnusedLocal
+@login_required
+def get_visits(request, visit_type):
+    if visit_type == VISIT_LIST_GENERIC:
+        return Visit.objects.all()
+    elif visit_type == VISIT_LIST_EXCEEDED_TIME:
+        return get_exceeded_visits(request.user).all()
+    elif visit_type == VISIT_LIST_UNFINISHED:
+        return get_unfinished_visits(request.user).all()
+    elif visit_type == VISIT_LIST_MISSING_APPOINTMENTS:
+        return get_active_visits_with_missing_appointments(request.user).all()
+    elif visit_type == VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS:
+        return get_approaching_visits_without_appointments(request.user).all()
+    elif visit_type == VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT:
+        return get_approaching_visits_for_mail_contact(request.user).all()
+    else:
+        raise TypeError("Unknown query type: " + visit_type)
+
+
+def get_visits_order(visits_to_be_ordered, order_column, order_direction):
+    result = visits_to_be_ordered
+    if order_direction == "asc":
+        order_direction = ""
+    else:
+        order_direction = "-"
+    if order_column == "first_name":
+        result = visits_to_be_ordered.order_by(order_direction + 'subject__subject__first_name')
+    elif order_column == "last_name":
+        result = visits_to_be_ordered.order_by(order_direction + 'subject__subject__last_name')
+    elif order_column == "default_location":
+        result = visits_to_be_ordered.order_by(order_direction + 'subject__default_location')
+    elif order_column == "flying_team":
+        result = visits_to_be_ordered.order_by(order_direction + 'subject__flying_team')
+    elif order_column == "datetime_begin":
+        result = visits_to_be_ordered.order_by(order_direction + 'datetime_begin')
+    elif order_column == "datetime_end":
+        result = visits_to_be_ordered.order_by(order_direction + 'datetime_end')
+    elif order_column == "is_finished":
+        result = visits_to_be_ordered.order_by(order_direction + 'is_finished')
+    elif order_column == "post_mail_sent":
+        result = visits_to_be_ordered.order_by(order_direction + 'post_mail_sent')
+    elif order_column == "visit_number":
+        result = visits_to_be_ordered.order_by(order_direction + 'visit_number')
+    else:
+        logger.warn("Unknown sort column: " + str(order_column))
+    return result
+
+
+def filter_by_appointment_in_progress(visits_to_filter, appointment_type):
+    result = visits_to_filter.filter(Q(appointment__appointment_types=appointment_type) & Q(
+        appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED))
+    return result
+
+
+def filter_by_appointment_done(visits_to_filter, appointment_type):
+    result = visits_to_filter.filter(Q(appointment__appointment_types=appointment_type) & Q(
+        appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED))
+    return result
+
+
+def filter_by_appointment_missing(visits_to_filter, appointment_type):
+    result = filter_by_appointment(visits_to_filter, appointment_type).exclude(
+        Q(appointment__appointment_types=appointment_type), Q(
+            appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED) | Q(
+            appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED))
+    return result
+
+
+def filter_by_appointment(visits_to_filter, appointment_type):
+    result = visits_to_filter.filter(appointment_types=appointment_type)
+    return result
+
+
+def get_visits_filtered(visits_to_be_filtered, filters):
+    result = visits_to_be_filtered
+    for row in filters:
+        column = row[0]
+        value = row[1]
+        if column == "first_name":
+            result = result.filter(subject__subject__first_name__icontains=value)
+        elif column == "last_name":
+            result = result.filter(subject__subject__last_name__icontains=value)
+        elif column == "flying_team":
+            result = result.filter(subject__flying_team=value)
+        elif column == "default_location":
+            result = result.filter(subject__default_location=value)
+        elif column == "is_finished":
+            result = result.filter(is_finished=(value == "true"))
+        elif column == "post_mail_sent":
+            result = result.filter(post_mail_sent=(value == "true"))
+        elif column == "visit_number":
+            result = result.filter(visit_number=int(value))
+        elif column == "visible_appointment_types_in_progress":
+            result = filter_by_appointment_in_progress(result, value)
+        elif column == "visible_appointment_types_done":
+            result = filter_by_appointment_done(result, value)
+        elif column == "visible_appointment_types_missing":
+            result = filter_by_appointment_missing(result, value)
+        elif column == "visible_appointment_types":
+            result = filter_by_appointment(result, value)
+        else:
+            message = "UNKNOWN filter: "
+            if column is None:
+                message += "[None]"
+            else:
+                message += str(column)
+            logger.warn(message)
+
+    return result
+
+
+@login_required
+def visits(request, visit_list_type):
+    # id of the query from dataTable: https://datatables.net/manual/server-side
+    draw = int(request.GET.get("draw", "-1"))
+
+    start = int(request.GET.get("start", "0"))
+    length = int(request.GET.get("length", "10"))
+    order = int(request.GET.get("order[0][column]", "0"))
+    order_dir = request.GET.get("order[0][dir]", "asc")
+    order_column = request.GET.get("columns[" + str(order) + "][data]", "last_name")
+
+    filters = []
+    column_id = 0
+    while request.GET.get("columns[" + str(column_id) + "][search][value]", "unknown") != "unknown":
+        val = request.GET.get("columns[" + str(column_id) + "][search][value]", "unknown")
+        if val != "":
+            filters.append([request.GET.get("columns[" + str(column_id) + "][data]"), val])
+        column_id += 1
+
+    all_visits = get_visits(request, visit_list_type)
+
+    count = all_visits.count()
+
+    ordered_visits = get_visits_order(all_visits, order_column, order_dir)
+    filtered_visits = get_visits_filtered(ordered_visits, filters)
+    sliced_visits = filtered_visits[start:(start + length)]
+
+    result_visits = sliced_visits
+
+    count_filtered = filtered_visits.count()
+
+    data = []
+    for visit in result_visits:
+        data.append(serialize_visit(visit))
+
+    return JsonResponse({
+        "draw": draw,
+        "recordsTotal": count,
+        "recordsFiltered": count_filtered,
+        "data": data,
+    })
+
+
+def appointment_types_to_str(appointment_types):
+    result = ""
+    for appointment_type in appointment_types:
+        result += unicode(appointment_type.code) + ", "
+    return result
+
+
+def serialize_visit(visit):
+    appointment_types = visit.appointment_types.all()
+    appointment_types_in_progress = AppointmentType.objects.filter(Q(appointmenttypelink__appointment__visit=visit) & Q(
+        appointmenttypelink__appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED)).distinct().all()
+    appointment_types_done = AppointmentType.objects.filter(Q(appointmenttypelink__appointment__visit=visit) & Q(
+        appointmenttypelink__appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED)).distinct().all()
+
+    appointment_types_missing = []
+    for appointment_type in appointment_types:
+        if appointment_types_in_progress.filter(id=appointment_type.id).count() == 0 and appointment_types_done.filter(
+                id=appointment_type.id).count() == 0:
+            appointment_types_missing.append(appointment_type)
+
+    result = {
+        "first_name": visit.subject.subject.first_name,
+        "last_name": visit.subject.subject.last_name,
+        "datetime_begin": serialize_date(visit.datetime_begin),
+        "datetime_end": serialize_date(visit.datetime_end),
+        "flying_team": flying_team_to_str(visit.subject.flying_team),
+        "default_location": location_to_str(visit.subject.default_location),
+        "is_finished": bool_to_yes_no(visit.is_finished),
+        "post_mail_sent": bool_to_yes_no(visit.post_mail_sent),
+        "visit_number": visit.visit_number,
+        "id": visit.id,
+        "visible_appointment_types_in_progress": appointment_types_to_str(appointment_types_in_progress),
+        "visible_appointment_types_done": appointment_types_to_str(appointment_types_done),
+        "visible_appointment_types_missing": appointment_types_to_str(appointment_types_missing),
+        "visible_appointment_types": appointment_types_to_str(appointment_types),
+    }
+
+    return result
diff --git a/smash/web/migrations/0083_auto_20171205_1251.py b/smash/web/migrations/0083_auto_20171205_1251.py
new file mode 100644
index 0000000000000000000000000000000000000000..cce682bd38f6119785b96ce29c81d5ecc1abed1f
--- /dev/null
+++ b/smash/web/migrations/0083_auto_20171205_1251.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-12-05 12:51
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('web', '0082_studysubjectlist_visits'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='StudyVisitList',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('type', models.CharField(choices=[(b'GENERIC', b'Generic')], max_length=50, verbose_name=b'Type o list')),
+                ('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Study')),
+                ('visible_subject_columns', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.SubjectColumns')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='VisitColumns',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('datetime_begin', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Visit starts date')),
+                ('datetime_end', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Visit ends date')),
+                ('is_finished', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Is finished')),
+                ('post_mail_sent', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Post mail sent')),
+                ('visit_number', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Visit number')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='studyvisitlist',
+            name='visible_visit_columns',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.VisitColumns'),
+        ),
+    ]
diff --git a/smash/web/migrations/0084_auto_20171205_1640.py b/smash/web/migrations/0084_auto_20171205_1640.py
new file mode 100644
index 0000000000000000000000000000000000000000..10e2c9e1b343cab292e032586bb25a0f6e11796c
--- /dev/null
+++ b/smash/web/migrations/0084_auto_20171205_1640.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-12-05 16:40
+from __future__ import unicode_literals
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('web', '0083_auto_20171205_1251'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='studyvisitlist',
+            name='visible_appointment_types_done',
+            field=models.BooleanField(default=False, verbose_name=b'Done appointments'),
+        ),
+        migrations.AddField(
+            model_name='studyvisitlist',
+            name='visible_appointment_types_in_progress',
+            field=models.BooleanField(default=False, verbose_name=b'Appointments in progress'),
+        ),
+        migrations.AddField(
+            model_name='studyvisitlist',
+            name='visible_appointment_types_missing',
+            field=models.BooleanField(default=False, verbose_name=b'Missing appointments'),
+        ),
+        migrations.AddField(
+            model_name='studyvisitlist',
+            name='visible_study_subject_columns',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='web.StudyColumns'),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='visitcolumns',
+            name='visible_appointment_types',
+            field=models.BooleanField(default=False, verbose_name=b'All appointments'),
+        ),
+        migrations.AlterField(
+            model_name='studyvisitlist',
+            name='type',
+            field=models.CharField(choices=[(b'UNFINISHED', b'unfinished visits'),
+                                            (b'APPROACHING_WITHOUT_APPOINTMENTS', b'approaching visits'),
+                                            (b'APPROACHING_FOR_MAIL_CONTACT', b'post mail for approaching visits'),
+                                            (b'GENERIC', b'Generic'),
+                                            (b'MISSING_APPOINTMENTS', b'visits with missing appointments'),
+                                            (b'EXCEEDED_TIME', b'exceeded visit time')], max_length=50,
+                                   verbose_name=b'Type of list'),
+        ),
+    ]
diff --git a/smash/web/migrations/0085_auto_20171205_1650.py b/smash/web/migrations/0085_auto_20171205_1650.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ffcafec5c7f58f48eafb8b20dd9c61fa809d1cd
--- /dev/null
+++ b/smash/web/migrations/0085_auto_20171205_1650.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-12-05 16:50
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+# noinspection PyUnusedLocal
+# noinspection PyPep8Naming
+def create_default_columns_for_VISIT_LIST_EXCEEDED_TIME(apps, schema_editor):
+    # We can't import the Study model directly as it may be a newer
+    # version than this migration expects. We use the historical version.
+    SubjectColumns = apps.get_model("web", "SubjectColumns")
+    subject_columns = SubjectColumns.objects.create()
+    subject_columns.sex = False
+    subject_columns.first_name = True
+    subject_columns.last_name = True
+    subject_columns.languages = False
+    subject_columns.default_written_communication_language = False
+    subject_columns.phone_number = False
+    subject_columns.phone_number_2 = False
+    subject_columns.phone_number_3 = False
+    subject_columns.email = False
+    subject_columns.date_born = False
+    subject_columns.address = False
+    subject_columns.postal_code = False
+    subject_columns.city = False
+    subject_columns.country = False
+    subject_columns.dead = False
+    subject_columns.save()
+
+    StudyColumns = apps.get_model("web", "StudyColumns")
+    study_columns = StudyColumns.objects.create()
+    study_columns.postponed = False
+    study_columns.datetime_contact_reminder = False
+    study_columns.type = False
+    study_columns.default_location = True
+    study_columns.flying_team = True
+    study_columns.screening_number = False
+    study_columns.nd_number = False
+    study_columns.mpower_id = False
+    study_columns.comments = False
+    study_columns.referral = False
+    study_columns.diagnosis = False
+    study_columns.year_of_diagnosis = False
+    study_columns.information_sent = False
+    study_columns.pd_in_family = False
+    study_columns.resigned = False
+    study_columns.resign_reason = False
+    study_columns.save()
+
+    VisitColumns = apps.get_model("web", "VisitColumns")
+    visit_columns = VisitColumns.objects.create()
+    visit_columns.datetime_begin = False
+    visit_columns.datetime_end = True
+    visit_columns.is_finished = False
+    visit_columns.post_mail_sent = False
+    visit_columns.visit_number = True
+    visit_columns.visible_appointment_types = False
+    visit_columns.save()
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('web', '0084_auto_20171205_1640'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='studysubjectlist',
+            name='type',
+            field=models.CharField(blank=True, choices=[(b'GENERIC', b'Generic'), (b'NO_VISIT', b'Subjects without visit'), (b'REQUIRE_CONTACT', b'Subjects required contact')], max_length=50, null=True, verbose_name=b'Type of list'),
+        ),
+        migrations.RunPython(create_default_columns_for_VISIT_LIST_EXCEEDED_TIME),
+        migrations.RunSQL('INSERT INTO web_studyvisitlist (' +
+                          'study_id, ' +
+                          'visible_visit_columns_id, ' +
+                          'visible_subject_columns_id, ' +
+                          'visible_study_subject_columns_id, ' +
+                          'visible_appointment_types_done,'
+                          'visible_appointment_types_in_progress,'
+                          'visible_appointment_types_missing,'
+                          'type) ' +
+                          "SELECT " +
+                          "1, " +
+                          "max(web_visitcolumns.id), " +
+                          "max(web_subjectcolumns.id), " +
+                          "max(web_studycolumns.id), " +
+                          "TRUE, " +
+                          "TRUE, " +
+                          "TRUE, " +
+                          "'EXCEEDED_TIME' FROM web_visitcolumns, web_studycolumns, web_subjectcolumns;"),
+    ]
diff --git a/smash/web/migrations/0086_unfinished_visit_list.py b/smash/web/migrations/0086_unfinished_visit_list.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b8f79975a0b56335cc1b52f1e391cb1dce7938c
--- /dev/null
+++ b/smash/web/migrations/0086_unfinished_visit_list.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-12-05 16:50
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+# noinspection PyUnusedLocal
+# noinspection PyPep8Naming
+def create_default_columns_for_VISIT_LIST_EXCEEDED_TIME(apps, schema_editor):
+    # We can't import the Study model directly as it may be a newer
+    # version than this migration expects. We use the historical version.
+    SubjectColumns = apps.get_model("web", "SubjectColumns")
+    subject_columns = SubjectColumns.objects.create()
+    subject_columns.sex = False
+    subject_columns.first_name = True
+    subject_columns.last_name = True
+    subject_columns.languages = False
+    subject_columns.default_written_communication_language = False
+    subject_columns.phone_number = False
+    subject_columns.phone_number_2 = False
+    subject_columns.phone_number_3 = False
+    subject_columns.email = False
+    subject_columns.date_born = False
+    subject_columns.address = False
+    subject_columns.postal_code = False
+    subject_columns.city = False
+    subject_columns.country = False
+    subject_columns.dead = False
+    subject_columns.save()
+
+    StudyColumns = apps.get_model("web", "StudyColumns")
+    study_columns = StudyColumns.objects.create()
+    study_columns.postponed = False
+    study_columns.datetime_contact_reminder = False
+    study_columns.type = False
+    study_columns.default_location = True
+    study_columns.flying_team = True
+    study_columns.screening_number = False
+    study_columns.nd_number = False
+    study_columns.mpower_id = False
+    study_columns.comments = False
+    study_columns.referral = False
+    study_columns.diagnosis = False
+    study_columns.year_of_diagnosis = False
+    study_columns.information_sent = False
+    study_columns.pd_in_family = False
+    study_columns.resigned = False
+    study_columns.resign_reason = False
+    study_columns.save()
+
+    VisitColumns = apps.get_model("web", "VisitColumns")
+    visit_columns = VisitColumns.objects.create()
+    visit_columns.datetime_begin = False
+    visit_columns.datetime_end = True
+    visit_columns.is_finished = False
+    visit_columns.post_mail_sent = False
+    visit_columns.visit_number = True
+    visit_columns.visible_appointment_types = False
+    visit_columns.save()
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('web', '0085_auto_20171205_1650'),
+    ]
+
+    operations = [
+        migrations.RunPython(create_default_columns_for_VISIT_LIST_EXCEEDED_TIME),
+        migrations.RunSQL('INSERT INTO web_studyvisitlist (' +
+                          'study_id, ' +
+                          'visible_visit_columns_id, ' +
+                          'visible_subject_columns_id, ' +
+                          'visible_study_subject_columns_id, ' +
+                          'visible_appointment_types_done,'
+                          'visible_appointment_types_in_progress,'
+                          'visible_appointment_types_missing,'
+                          'type) ' +
+                          "SELECT " +
+                          "1, " +
+                          "max(web_visitcolumns.id), " +
+                          "max(web_subjectcolumns.id), " +
+                          "max(web_studycolumns.id), " +
+                          "TRUE, " +
+                          "TRUE, " +
+                          "TRUE, " +
+                          "'UNFINISHED' FROM web_visitcolumns, web_studycolumns, web_subjectcolumns;"),
+    ]
diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py
index 7d37f387ad3dd5da911ecc29472cb4687d0c2fb6..746b15bf599f4f08e2605b8274ddc97137b609d8 100644
--- a/smash/web/models/__init__.py
+++ b/smash/web/models/__init__.py
@@ -10,6 +10,7 @@ from appointment_type_link import AppointmentTypeLink
 from country import Country
 from subject_columns import SubjectColumns
 from study_columns import StudyColumns
+from visit_columns import VisitColumns
 from notification_columns import StudyNotificationParameters
 from study import Study
 from room import Room
@@ -24,6 +25,7 @@ from language import Language
 from subject import Subject
 from study_subject import StudySubject
 from study_subject_list import StudySubjectList
+from study_visit_list import StudyVisitList
 from contact_attempt import ContactAttempt
 from mail_template import MailTemplate
 from missing_subject import MissingSubject
@@ -32,4 +34,4 @@ from inconsistent_subject import InconsistentSubject, InconsistentField
 __all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room,
            Subject, StudySubject, StudySubjectList, SubjectColumns, StudyNotificationParameters,
            Visit, Worker, ContactAttempt, ConfigurationItem, MailTemplate, AppointmentTypeLink, MissingSubject,
-           InconsistentSubject, InconsistentField, Country, StudyColumns]
+           InconsistentSubject, InconsistentField, Country, StudyColumns, VisitColumns, StudyVisitList]
diff --git a/smash/web/models/study_subject_list.py b/smash/web/models/study_subject_list.py
index 1b09054fd1391f85de1bbaccf8a390c63bc2a09c..cd2f5bbbbfff4142032e0cd68b9624b53e0f7668 100644
--- a/smash/web/models/study_subject_list.py
+++ b/smash/web/models/study_subject_list.py
@@ -48,7 +48,7 @@ class StudySubjectList(models.Model):
 
     type = models.CharField(max_length=50,
                             choices=SUBJECT_LIST_CHOICES.items(),
-                            verbose_name='Type o list',
+                            verbose_name='Type of list',
                             null=True,
                             blank=True
                             )
diff --git a/smash/web/models/study_visit_list.py b/smash/web/models/study_visit_list.py
new file mode 100644
index 0000000000000000000000000000000000000000..e16c334ec9a97fcf953ea49fe36a454c0ccf49ba
--- /dev/null
+++ b/smash/web/models/study_visit_list.py
@@ -0,0 +1,64 @@
+# coding=utf-8
+from django.db import models
+
+from web.models import Study, SubjectColumns, VisitColumns
+from web.models import StudyColumns
+
+VISIT_LIST_GENERIC = "GENERIC"
+VISIT_LIST_EXCEEDED_TIME = "EXCEEDED_TIME"
+VISIT_LIST_UNFINISHED = "UNFINISHED"
+VISIT_LIST_MISSING_APPOINTMENTS = "MISSING_APPOINTMENTS"
+VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS = "APPROACHING_WITHOUT_APPOINTMENTS"
+VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT = "APPROACHING_FOR_MAIL_CONTACT"
+
+VISIT_LIST_CHOICES = {
+    VISIT_LIST_GENERIC: 'Generic',
+    VISIT_LIST_EXCEEDED_TIME: 'exceeded visit time',
+    VISIT_LIST_UNFINISHED: 'unfinished visits',
+    VISIT_LIST_MISSING_APPOINTMENTS: 'visits with missing appointments',
+    VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS: 'approaching visits',
+    VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT: 'post mail for approaching visits',
+}
+
+
+class StudyVisitList(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    study = models.ForeignKey(
+        Study,
+        on_delete=models.CASCADE,
+        null=False,
+    )
+
+    visible_visit_columns = models.ForeignKey(
+        VisitColumns,
+        on_delete=models.CASCADE,
+        null=False,
+    )
+
+    visible_subject_columns = models.ForeignKey(
+        SubjectColumns,
+        on_delete=models.CASCADE,
+        null=False,
+    )
+    visible_study_subject_columns = models.ForeignKey(
+        StudyColumns,
+        on_delete=models.CASCADE,
+        null=False,
+    )
+
+    visible_appointment_types_in_progress = models.BooleanField(default=False,
+                                                                verbose_name='Appointments in progress'
+                                                                )
+    visible_appointment_types_done = models.BooleanField(default=False,
+                                                         verbose_name='Done appointments'
+                                                         )
+    visible_appointment_types_missing = models.BooleanField(default=False,
+                                                            verbose_name='Missing appointments'
+                                                            )
+
+    type = models.CharField(max_length=50,
+                            choices=VISIT_LIST_CHOICES.items(),
+                            verbose_name='Type of list',
+                            )
diff --git a/smash/web/models/visit_columns.py b/smash/web/models/visit_columns.py
new file mode 100644
index 0000000000000000000000000000000000000000..6984c6de1ad5aef2ea64f389711d169cf1f8e076
--- /dev/null
+++ b/smash/web/models/visit_columns.py
@@ -0,0 +1,37 @@
+# coding=utf-8
+from django.db import models
+
+from web.models.constants import BOOL_CHOICES
+
+
+class VisitColumns(models.Model):
+    class Meta:
+        app_label = 'web'
+
+    datetime_begin = models.BooleanField(choices=BOOL_CHOICES,
+                                         verbose_name='Visit starts date',
+                                         default=True
+                                         )
+
+    datetime_end = models.BooleanField(choices=BOOL_CHOICES,
+                                       verbose_name='Visit ends date',
+                                       default=True
+                                       )
+
+    is_finished = models.BooleanField(choices=BOOL_CHOICES,
+                                      verbose_name='Is finished',
+                                      default=True
+                                      )
+
+    post_mail_sent = models.BooleanField(choices=BOOL_CHOICES,
+                                         verbose_name='Post mail sent',
+                                         default=True
+                                         )
+
+    visit_number = models.BooleanField(choices=BOOL_CHOICES,
+                                       verbose_name='Visit number',
+                                       default=True
+                                       )
+    visible_appointment_types = models.BooleanField(default=False,
+                                                    verbose_name='All appointments'
+                                                    )
diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js
index 3bfb678925183ceaf0ac7f5f88e61d4700ca48ce..6da68884c152b60a3e56823dc4e808d4b604e76c 100644
--- a/smash/web/static/js/smash.js
+++ b/smash/web/static/js/smash.js
@@ -70,4 +70,294 @@ function showErrorInfo(content) {
     $(errorDialogDiv).attr("role", "dialog");
     $(errorDialogDiv).addClass("modal modal-danger fade");
     $(errorDialogDiv).modal("show");
-}
\ No newline at end of file
+}
+
+//------------------------------------------------------
+// common code for dynamic Data Tables
+//------------------------------------------------------
+
+if (!String.prototype.startsWith) {
+    String.prototype.startsWith = function (searchString, position) {
+        position = position || 0;
+        return this.indexOf(searchString, position) === position;
+    };
+}
+
+function createColumn(dataType, name, filter, visible, sortable, renderFunction) {
+    if (renderFunction === undefined) {
+        renderFunction = function (data, type, row, meta) {
+            return row[dataType];
+        }
+    }
+    return {
+        "type": dataType,
+        "name": name,
+        "filter": filter,
+        "visible": visible,
+        "render": renderFunction,
+        "sortable": sortable
+    };
+}
+
+function getColumns(columns, getSubjectEditUrl) {
+    var result = [];
+    for (var i = 0; i < columns.length; i++) {
+        var columnRow = columns[i];
+        if (columnRow.type === "edit") {
+            result.push(createColumn("id", columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable, function (data, type, row) {
+                var url = getSubjectEditUrl(row.id.toString());
+
+                return '<a href="' + url + '" type="button" class="btn btn-block btn-default">Edit</a>';
+            }));
+        } else if (/^visit_[0-9]+$/.test(columnRow.type)) {
+            var renderFunction = (function () {
+                var x = columnRow.type.replace("visit_", "");
+                return function (data, type, row) {
+                    return create_visit_row(row.visits[x - 1]);
+                };
+            })();
+
+            result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable, renderFunction));
+
+        } else {
+            result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable));
+        }
+    }
+    return result;
+}
+
+function createHeader(columnsDefinition) {
+    var header = document.createElement("thead");
+    var headerRow = document.createElement("tr");
+    header.appendChild(headerRow);
+    for (var i = 0; i < columnsDefinition.length; i++) {
+        var column = columnsDefinition[i];
+        var element = document.createElement("th");
+        element.innerHTML = column.name;
+        headerRow.appendChild(element);
+    }
+    return header;
+}
+
+function createFilter(columnsDefinition) {
+    var footer = document.createElement("tfoot");
+    footer.style.display = "table-header-group";
+    var footerRow = document.createElement("tr");
+    footer.appendChild(footerRow);
+    for (var i = 0; i < columnsDefinition.length; i++) {
+        var column = columnsDefinition[i];
+        var element = document.createElement("th");
+        if (column.filter !== null) {
+            element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>";
+        }
+        footerRow.appendChild(element);
+    }
+    return footer;
+}
+
+function create_visit_row(visit) {
+    var color = "white";
+    var text = "---";
+    if (visit !== undefined && visit !== null) {
+        if (visit.status === "DONE") {
+            color = "green";
+            text = "OK";
+        } else if (visit.status === "MISSED") {
+            color = "pink";
+            text = "MISSED";
+        } else if (visit.status === "UPCOMING") {
+            color = "#00ffff";
+            text = "UPCOMING";
+        } else if (visit.status === "EXCEEDED") {
+            color = "orange";
+            text = "EXCEEDED";
+        } else if (visit.status === "SHOULD_BE_IN_PROGRESS") {
+            color = "orange";
+            text = "IN PROGRESS (NO APPOINTMENTS)";
+        } else if (visit.status === "IN_PROGRESS") {
+            color = "lightgreen";
+            text = "IN PROGRESS";
+        }
+        text += "<br/>" + visit.datetime_start + " - " + visit.datetime_end;
+    }
+    return "<div style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>";
+}
+
+function createVisibilityCheckboxes(checkboxesElement, columns) {
+    var row = null;
+    for (var i = 0; i < columns.length; i++) {
+        if (i % 10 === 0) {
+            row = document.createElement("div");
+            row.style.display = "table-row";
+            checkboxesElement.appendChild(row);
+        }
+        var column = columns[i];
+        var element = document.createElement("div");
+        element.style.display = "table-cell";
+        var checked = "";
+        if (column.visible) {
+            checked = "checked";
+        }
+        element.innerHTML = "<input type='checkbox' " + checked + " data-column='" + i + "' name='" + column.type + "'/>" + column.name;
+        row.appendChild(element);
+    }
+
+}
+
+function createTable(params) {
+    var tableElement = params.tableElement;
+    var worker_locations = params.worker_locations;
+    if (worker_locations === undefined) {
+        worker_locations = [];
+    }
+    var subject_types_url = params.subject_types_url;
+    var locations_url = params.locations_url;
+    var flying_teams_url = params.flying_teams_url;
+    var appointment_types_url = params.appointment_types_url;
+    var subjects_url = params.subjects_url;
+    var columnsDefinition = params.columns;
+
+    tableElement.appendChild(createHeader(columnsDefinition));
+    tableElement.appendChild(createFilter(columnsDefinition));
+    tableElement.appendChild(document.createElement("tbody"));
+    createVisibilityCheckboxes(params.checkboxesElement, columnsDefinition);
+
+    var table;
+    $(tableElement).find('tfoot div[name="string_filter"]').each(function () {
+        var title = $(this).text();
+        $(this).html('<input type="text" style="width:80px" placeholder="' + title + '" />');
+    });
+
+    $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () {
+        $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>');
+    });
+    $(tableElement).find('tfoot div[name="integer_filter"]').each(function () {
+        var options = '<option value selected="selected">---</option>';
+        for (var i = 1; i <= 8; i++) {
+            options += '<option value="' + i + '">' + i + '</option>';
+        }
+        $(this).html('<select style="width:60px" >' + options + '</select>');
+    });
+
+    $(tableElement).find('tfoot div[name="visit_filter"]').each(function () {
+        $(this).html('<select style="width:60px" >' +
+            '<option value selected="selected">---</option>' +
+            '<option value="MISSED">MISSED</option>' +
+            '<option value="DONE">DONE</option>' +
+            '<option value="EXCEED">EXCEED</option>' +
+            '<option value="IN_PROGRESS">IN PROGRESS</option>' +
+            '<option value="SHOULD_BE_IN_PROGRESS">SHOULD BE IN PROGRESS</option>' +
+            '<option value="UPCOMING">UPCOMING</option>' +
+            '</select>');
+    });
+
+    $(tableElement).find('tfoot div[name="location_filter"]').each(function () {
+        var obj = $(this);
+        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
+        var select = $('select', obj);
+        $.get(locations_url, function (data) {
+            $.each(data.locations, function (index, location) {
+                select.append('<option value="' + location.id + '">' + location.name + '</option>');
+            });
+            if (worker_locations.length === 1) {
+                select.val(worker_locations[0].id);
+            }
+        });
+    });
+
+    $(tableElement).find('tfoot div[name="flying_team_filter"]').each(function () {
+        var obj = $(this);
+        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
+        var select = $('select', obj);
+        $.get(flying_teams_url, function (data) {
+            $.each(data.flying_teams, function (index, flying_team) {
+                select.append('<option value="' + flying_team.id + '">' + flying_team.name + '</option>');
+            });
+            if (worker_locations.length === 1) {
+                select.val(worker_locations[0].id);
+            }
+        });
+    });
+
+    $(tableElement).find('tfoot div[name="appointment_type_filter"]').each(function () {
+        var obj = $(this);
+        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
+        var select = $('select', obj);
+        $.get(appointment_types_url, function (data) {
+            $.each(data.appointment_types, function (index, appointment_type) {
+                select.append('<option value="' + appointment_type.id + '">' + appointment_type.type + '</option>');
+            });
+            if (worker_locations.length === 1) {
+                select.val(worker_locations[0].id);
+            }
+        });
+    });
+
+    $(tableElement).find('tfoot div[name="type_filter"]').each(function () {
+        var obj = $(this);
+        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
+        var select = $('select', obj);
+        $.get(subject_types_url, function (data) {
+            $.each(data.types, function (index, type) {
+                select.append('<option value="' + type.id + '">' + type.name + '</option>');
+            });
+        });
+    });
+
+
+    $(function () {
+        var columns = [];
+        var columnDefs = [];
+        for (var i = 0; i < columnsDefinition.length; i++) {
+            var column = columnsDefinition[i];
+            columns.push({"data": column.type});
+            columnDefs.push({
+                "targets": i,
+                "render": column.render,
+                visible: column.visible,
+                orderable: column.sortable
+            });
+        }
+
+
+        table = $('#table').DataTable({
+            pageLength: 25,
+            serverSide: true,
+            processing: true,
+            responsive: true,
+            ajax: subjects_url,
+            columns: columns,
+            columnDefs: columnDefs,
+            order: [[0, 'desc']]
+        });
+
+        // Apply the search
+        table.columns().every(function () {
+            var that = this;
+
+            $('input', this.footer()).on('keyup change', function () {
+                if (that.search() !== this.value) {
+                    that.search(this.value).draw();
+                }
+            });
+            $('select', this.footer()).on('keyup change', function () {
+                if (that.search() !== this.value) {
+                    that.search(this.value).draw();
+                }
+            });
+        });
+        $('#table_filter').css("display", "null");
+    });
+    $('#visible-column-checkboxes input').on('click', function (e) {
+        var visible = $(this).is(":checked");
+
+        // Get the column API object
+        var column = table.column($(this).attr('data-column'));
+        // Toggle the visibility
+        column.visible(visible);
+    });
+}
+
+//------------------------------------------------------
+// END common code for dynamic Data Tables
+//------------------------------------------------------
diff --git a/smash/web/static/js/subject.js b/smash/web/static/js/subject.js
index 585d1b9f5b568e31ffa03dcc29fbe23fafcb45d1..434b796a2e48a78f93ecd710d02de1ad9f453efa 100644
--- a/smash/web/static/js/subject.js
+++ b/smash/web/static/js/subject.js
@@ -1,253 +1,3 @@
-if (!String.prototype.startsWith) {
-    String.prototype.startsWith = function (searchString, position) {
-        position = position || 0;
-        return this.indexOf(searchString, position) === position;
-    };
-}
-
-function createColumn(dataType, name, filter, visible, renderFunction) {
-    if (renderFunction === undefined) {
-        renderFunction = function (data, type, row, meta) {
-            return row[dataType];
-        }
-    }
-    return {
-        "type": dataType,
-        "name": name,
-        "filter": filter,
-        "visible": visible,
-        "render": renderFunction
-    };
-}
-
-function getColumns(columns, getSubjectEditUrl) {
-    var result = [];
-    for (var i = 0; i < columns.length; i++) {
-        var columnRow = columns[i];
-        if (columnRow.type === "edit") {
-            result.push(createColumn("id", columnRow.name, columnRow.filter, columnRow.visible, function (data, type, row) {
-                var url = getSubjectEditUrl(row.id.toString());
-
-                return '<a href="' + url + '" type="button" class="btn btn-block btn-default">Edit</a>';
-            }));
-        } else if (columnRow.type.startsWith("visit")) {
-            var renderFunction = (function () {
-                var x = i;
-                return function (data, type, row) {
-                    return create_visit_row(row.visits[x - 1]);
-                };
-            })();
-
-            result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, renderFunction));
-
-        } else {
-            result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible));
-        }
-    }
-    return result;
-}
-
-function createHeader(columnsDefinition) {
-    var header = document.createElement("thead");
-    var headerRow = document.createElement("tr");
-    header.appendChild(headerRow);
-    for (var i = 0; i < columnsDefinition.length; i++) {
-        var column = columnsDefinition[i];
-        var element = document.createElement("th");
-        element.innerHTML = column.name;
-        headerRow.appendChild(element);
-    }
-    return header;
-}
-
-function createFilter(columnsDefinition) {
-    var footer = document.createElement("tfoot");
-    footer.style.display = "table-header-group";
-    var footerRow = document.createElement("tr");
-    footer.appendChild(footerRow);
-    for (var i = 0; i < columnsDefinition.length; i++) {
-        var column = columnsDefinition[i];
-        var element = document.createElement("th");
-        if (column.filter !== null) {
-            element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>";
-        }
-        footerRow.appendChild(element);
-    }
-    return footer;
-}
-
-function create_visit_row(visit) {
-    var color = "white";
-    var text = "---";
-    if (visit !== undefined && visit !== null) {
-        if (visit.status === "DONE") {
-            color = "green";
-            text = "OK";
-        } else if (visit.status === "MISSED") {
-            color = "pink";
-            text = "MISSED";
-        } else if (visit.status === "UPCOMING") {
-            color = "#00ffff";
-            text = "UPCOMING";
-        } else if (visit.status === "EXCEEDED") {
-            color = "orange";
-            text = "EXCEEDED";
-        } else if (visit.status === "SHOULD_BE_IN_PROGRESS") {
-            color = "orange";
-            text = "IN PROGRESS (NO APPOINTMENTS)";
-        } else if (visit.status === "IN_PROGRESS") {
-            color = "lightgreen";
-            text = "IN PROGRESS";
-        }
-        text += "<br/>" + visit.datetime_start + " - " + visit.datetime_end;
-    }
-    return "<div style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>";
-}
-
-function createVisibilityCheckboxes(checkboxesElement, columns) {
-    var row = null;
-    for (var i = 0; i < columns.length; i++) {
-        if (i % 10 === 0) {
-            row = document.createElement("div");
-            row.style.display = "table-row";
-            checkboxesElement.appendChild(row);
-        }
-        var column = columns[i];
-        var element = document.createElement("div");
-        element.style.display = "table-cell";
-        var checked = "";
-        if (column.visible) {
-            checked = "checked";
-        }
-        element.innerHTML = "<input type='checkbox' " + checked + " data-column='" + i + "' name='" + column.type + "'/>" + column.name;
-        row.appendChild(element);
-    }
-
-}
-
 function createSubjectsTable(params) {
-    var tableElement = params.tableElement;
-    var worker_locations = params.worker_locations;
-    var subject_types_url = params.subject_types_url;
-    var locations_url = params.locations_url;
-    var flying_teams_url = params.flying_teams_url;
-    var subjects_url = params.subjects_url;
-    var columnsDefinition = params.columns;
-
-    tableElement.appendChild(createHeader(columnsDefinition));
-    tableElement.appendChild(createFilter(columnsDefinition));
-    tableElement.appendChild(document.createElement("tbody"));
-    createVisibilityCheckboxes(params.checkboxesElement, columnsDefinition);
-
-    var table;
-    $(tableElement).find('tfoot div[name="string_filter"]').each(function () {
-        var title = $(this).text();
-        $(this).html('<input type="text" style="width:80px" placeholder="' + title + '" />');
-    });
-
-    $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () {
-        $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>');
-    });
-
-    $(tableElement).find('tfoot div[name="visit_filter"]').each(function () {
-        $(this).html('<select style="width:60px" >' +
-            '<option value selected="selected">---</option>' +
-            '<option value="MISSED">MISSED</option>' +
-            '<option value="DONE">DONE</option>' +
-            '<option value="EXCEED">EXCEED</option>' +
-            '<option value="IN_PROGRESS">IN PROGRESS</option>' +
-            '<option value="SHOULD_BE_IN_PROGRESS">SHOULD BE IN PROGRESS</option>' +
-            '<option value="UPCOMING">UPCOMING</option>' +
-            '</select>');
-    });
-
-    $(tableElement).find('tfoot div[name="location_filter"]').each(function () {
-        var obj = $(this);
-        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
-        var select = $('select', obj);
-        $.get(locations_url, function (data) {
-            $.each(data.locations, function (index, location) {
-                select.append('<option value="' + location.id + '">' + location.name + '</option>');
-            });
-            if (worker_locations.length === 1) {
-                select.val(worker_locations[0].id);
-            }
-        });
-    });
-
-    $(tableElement).find('tfoot div[name="flying_team_filter"]').each(function () {
-        var obj = $(this);
-        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
-        var select = $('select', obj);
-        $.get(flying_teams_url, function (data) {
-            $.each(data.flying_teams, function (index, flying_team) {
-                select.append('<option value="' + flying_team.id + '">' + flying_team.name + '</option>');
-            });
-            if (worker_locations.length === 1) {
-                select.val(worker_locations[0].id);
-            }
-        });
-    });
-
-
-
-    $(tableElement).find('tfoot div[name="type_filter"]').each(function () {
-        var obj = $(this);
-        obj.html('<select style="width:80px"><option value selected="selected">---</option></select>');
-        var select = $('select', obj);
-        $.get(subject_types_url, function (data) {
-            $.each(data.types, function (index, type) {
-                select.append('<option value="' + type.id + '">' + type.name + '</option>');
-            });
-        });
-    });
-
-
-    $(function () {
-        var columns = [];
-        var columnDefs = [];
-        for (var i = 0; i < columnsDefinition.length; i++) {
-            var column = columnsDefinition[i];
-            columns.push({"data": column.type});
-            columnDefs.push({"targets": i, "render": column.render, visible: column.visible});
-        }
-
-
-        table = $('#table').DataTable({
-            pageLength: 25,
-            serverSide: true,
-            processing: true,
-            responsive: true,
-            ajax: subjects_url,
-            columns: columns,
-            columnDefs: columnDefs,
-            order: [[0, 'desc']]
-        });
-
-        // Apply the search
-        table.columns().every(function () {
-            var that = this;
-
-            $('input', this.footer()).on('keyup change', function () {
-                if (that.search() !== this.value) {
-                    that.search(this.value).draw();
-                }
-            });
-            $('select', this.footer()).on('keyup change', function () {
-                if (that.search() !== this.value) {
-                    that.search(this.value).draw();
-                }
-            });
-        });
-        $('#table_filter').css("display", "null");
-    });
-    $('#visible-column-checkboxes input').on('click', function (e) {
-        var visible = $(this).is(":checked");
-
-        // Get the column API object
-        var column = table.column($(this).attr('data-column'));
-        console.log($(this).attr('data-column'));
-        // Toggle the visibility
-        column.visible(visible);
-    });
-}
\ No newline at end of file
+    return createTable(params);
+}
diff --git a/smash/web/static/js/visit.js b/smash/web/static/js/visit.js
index c4eac088fd1fe16a7bad5f2baa307481184bc58b..a2636ed219748484b648296a736608d14732589c 100644
--- a/smash/web/static/js/visit.js
+++ b/smash/web/static/js/visit.js
@@ -10,3 +10,8 @@ function visit_dates_behaviour(startDateInput, endDateInput) {
         }
     });
 }
+
+
+function createVisitsTable(params) {
+    return createTable(params);
+}
diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html
index 8779d279c12492c247e55b19c3e5223cc6803d8c..4f4b59bf72f636625948a85cbd018358e2857a39 100644
--- a/smash/web/templates/_base.html
+++ b/smash/web/templates/_base.html
@@ -186,8 +186,8 @@ desired effect
                   </div>
                 </div>
                 <!-- /.row -->
-              {% endcomment %}
                             </li>
+              {% endcomment %}
                             <!-- Menu Footer-->
                             <li class="user-footer">
                                 <!--<div class="pull-left">
diff --git a/smash/web/templates/mail_templates/index.html b/smash/web/templates/mail_templates/index.html
deleted file mode 100644
index 137ade1b2b90f7773f205a2a6d81981db5212e75..0000000000000000000000000000000000000000
--- a/smash/web/templates/mail_templates/index.html
+++ /dev/null
@@ -1,134 +0,0 @@
-{% extends "_base.html" %}
-{% load static %}
-
-{% block styles %}
-    {{ block.super }}
-    <!-- DataTables -->
-    <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}">
-{% endblock styles %}
-
-{% block ui_active_tab %}'mail_templates'{% endblock ui_active_tab %}
-{% block page_header %}Mail templates{% endblock page_header %}
-{% block page_description %}{% endblock page_description %}
-
-{% block breadcrumb %}
-    {% include "mail_templates/breadcrumb.html" %}
-{% endblock breadcrumb %}
-
-{% block maincontent %}
-    <div class="box box-danger box-solid">
-        <div class="box-header with-border">
-            <h3 class="box-title">Not yet implemented</h3>
-
-            <div class="box-tools pull-right">
-                <button type="button" class="btn btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button>
-            </div>
-            <!-- /.box-tools -->
-        </div>
-        <!-- /.box-header -->
-        <div class="box-body">
-            This is only a static page to show, how the system would look like.
-            A comprehensive system for template editing and generation has to be developed.
-        </div>
-        <!-- /.box-body -->
-    </div>
-
-    <div>
-        <a class="btn btn-app">
-            <i class="fa fa-plus"></i> Add new letter
-        </a>
-    </div>
-
-    <div class="box-body">
-        <table id="table" class="table table-bordered table-striped">
-            <thead>
-            <tr>
-                <th>No.</th>
-                <th>Destination</th>
-                <th>Language</th>
-                <th>Title</th>
-                <th>Details</th>
-                <th>Edit</th>
-                <th>Delete</th>
-                <th>Generate</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr>
-                <td>1</td>
-                <td>Patients</td>
-                <td>english</td>
-                <td>"Thank you" letter</td>
-                <td>
-                    <button type="button" class="btn btn-block btn-default">Details</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-warning">Edit</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-danger">Delete</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-default">Generate</button>
-                </td>
-            </tr>
-            <tr>
-                <td>2</td>
-                <td>Patients</td>
-                <td>english</td>
-                <td>"Dankescreiben" letter</td>
-                <td>
-                    <button type="button" class="btn btn-block btn-default">Details</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-warning">Edit</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-danger">Delete</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-default">Generate</button>
-                </td>
-            </tr>
-            <tr>
-                <td>3</td>
-                <td>Doctors</td>
-                <td>english</td>
-                <td>"New Scheduling system"</td>
-                <td>
-                    <button type="button" class="btn btn-block btn-default">Details</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-warning">Edit</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-danger">Delete</button>
-                </td>
-                <td>
-                    <button type="button" class="btn btn-block btn-default">Generate</button>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-    </div>
-{% endblock maincontent %}
-
-{% block scripts %}
-    {{ block.super }}
-
-    <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script>
-    <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script>
-
-    <script>
-        $(function () {
-            $('#table').DataTable({
-                "paging": true,
-                "lengthChange": false,
-                "searching": true,
-                "ordering": true,
-                "info": true,
-                "autoWidth": false
-            });
-        });
-    </script>
-{% endblock scripts %}
diff --git a/smash/web/templates/visits/index.html b/smash/web/templates/visits/index.html
index 6d82fe9098392bd9c903d3442379326a71c25378..eb25d83536ca7336b78e770abc4c2e9aae082f63 100644
--- a/smash/web/templates/visits/index.html
+++ b/smash/web/templates/visits/index.html
@@ -7,9 +7,6 @@
     <!-- DataTables -->
     <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}">
 
-    <!-- fullCalendar 2.2.5-->
-    <link rel="stylesheet" href="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar.min.css' %}">
-    <link rel="stylesheet" href="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar.print.css' %}" media="print">
 {% endblock styles %}
 
 {% block ui_active_tab %}'visits'{% endblock ui_active_tab %}
@@ -23,62 +20,12 @@
 {% endblock breadcrumb %}
 
 {% block maincontent %}
-
-    <div>
-        <a href="{% url 'web.views.visit_add' %}" class="btn btn-app">
-            <i class="fa fa-plus"></i>
-            Add new visit
-        </a>
+    <div class="box-body">
+        <table id="table" class="table table-bordered table-striped table-responsive">
+        </table>
     </div>
-
-    <div>
-        <div>
-            {% if visit_list %}
-                <table id="visit_table" class="table table-bordered table-striped">
-                    <thead>
-                    <tr>
-                        <th>Subject name</th>
-                        <th>Full information</th>
-                        <th>Visit begins</th>
-                        <th>Visit ends</th>
-                        <th>Finished?</th>
-                    </tr>
-                    </thead>
-                    <tbody>
-                    {% for visit in visit_list %}
-                        <tr>
-                            <td>{{ visit.subject.first_name }} {{ visit.subject.last_name }}</td>
-                            <td>
-                                <a href="{% url 'web.views.visit_details' visit.id %}" type="button"
-                                   class="btn btn-block btn-default">Details</a>
-                            </td>
-                            <td>
-                                {{ visit.datetime_begin }}
-                            </td>
-                            <td>
-                                {{ visit.datetime_end }}
-                            </td>
-                            <td>
-                                {% if visit.is_finished %}
-                                    <button type="button" class="btn btn-block btn-success">YES</button>
-                                {% else %}
-                                    <button type="button" class="btn btn-block btn-danger">NO</button>
-                                {% endif %}
-                            </td>
-
-                        </tr>
-                    {% endfor %}
-                    </tbody>
-                </table>
-
-            {% else %}
-                <p>No visits to display.</p>
-            {% endif %}
-
-            <hr/>
-
-        </div>
-
+    <h3>Visible columns</h3>
+    <div id="visible-column-checkboxes" style="display:table; width:100%">
     </div>
 {% endblock maincontent %}
 
@@ -87,19 +34,22 @@
 
     <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script>
     <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script>
-    <script src="{% static 'AdminLTE/plugins/moment.js/moment.min.js' %}"></script>
-    <script src="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar.min.js' %}"></script>
+    <script src="{% static 'js/visit.js' %}"></script>
 
     <script>
-        $(function () {
-            $('#planning_table, #approaching_table').DataTable({
-                "paging": true,
-                "lengthChange": false,
-                "searching": true,
-                "ordering": true,
-                "info": true,
-                "autoWidth": false
-            });
+        function getVisitEditUrl(id) {
+            return "{% url 'web.views.visit_details' 12345678 %}".replace(/12345678/, id);
+        }
+        $.get("{% url 'web.api.visits.columns' visit_list_type %}", function (data) {
+            createVisitsTable({
+                locations_url: "{% url 'web.api.locations' %}",
+                flying_teams_url: "{% url 'web.api.flying_teams' %}",
+                appointment_types_url: "{% url 'web.api.appointment_types' %}",
+                subjects_url: "{% url 'web.api.visits' visit_list_type %}",
+                tableElement: document.getElementById("table"),
+                columns: getColumns(data.columns, getVisitEditUrl),
+                checkboxesElement: document.getElementById("visible-column-checkboxes")
+            })
         });
     </script>
 {% endblock scripts %}
diff --git a/smash/web/tests/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py
index b3da6195882493d7c95980d006977720ebf2bb12..2ed3fc0a5721f39de92e27066bbc28ce6c362882 100644
--- a/smash/web/tests/api_views/test_subject.py
+++ b/smash/web/tests/api_views/test_subject.py
@@ -3,33 +3,25 @@ import datetime
 import json
 import logging
 
-from django.contrib.auth.models import User
-from django.test import Client
-from django.test import TestCase
 from django.urls import reverse
 
+from web.tests import LoggedInWithWorkerTestCase
 from web.api_views.subject import get_subjects_order, get_subjects_filtered, serialize_subject
 from web.models import StudySubject, Appointment, Study
 from web.models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES_PATIENT, SUBJECT_TYPE_CHOICES_CONTROL
 from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \
     StudySubjectList
-from web.tests.functions import create_study_subject, create_worker, create_get_suffix, create_visit, \
+from web.tests.functions import create_study_subject, create_get_suffix, create_visit, \
     create_appointment, create_empty_study_columns, create_contact_attempt, create_flying_team
 from web.views.notifications import get_today_midnight_date
 
 logger = logging.getLogger(__name__)
 
 
-class TestApi(TestCase):
+class TestSubjectApi(LoggedInWithWorkerTestCase):
     def setUp(self):
+        super(TestSubjectApi, self).setUp()
         self.study_subject = create_study_subject()
-        self.client = Client()
-        username = 'piotr'
-        password = 'top_secret'
-        self.user = User.objects.create_user(
-            username=username, email='jacob@bla', password=password)
-        self.worker = create_worker(self.user)
-        self.client.login(username=username, password=password)
 
     def test_cities(self):
         city_name = "some city"
diff --git a/smash/web/tests/api_views/test_visit.py b/smash/web/tests/api_views/test_visit.py
new file mode 100644
index 0000000000000000000000000000000000000000..2020af6324887766b1922b4aee8a1721f4dcef94
--- /dev/null
+++ b/smash/web/tests/api_views/test_visit.py
@@ -0,0 +1,273 @@
+# coding=utf-8
+import datetime
+import json
+import logging
+
+from django.urls import reverse
+
+from web.api_views.visit import get_visits_filtered, get_visits_order
+from web.models import Visit, AppointmentTypeLink, Appointment
+from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_EXCEEDED_TIME, VISIT_LIST_UNFINISHED, \
+    VISIT_LIST_MISSING_APPOINTMENTS, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, \
+    VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT
+from web.tests import LoggedInWithWorkerTestCase
+from web.tests.functions import create_study_subject, create_get_suffix, create_visit, create_appointment_type, \
+    create_flying_team, create_appointment
+from web.views.notifications import get_today_midnight_date
+
+logger = logging.getLogger(__name__)
+
+
+class TestVisitApi(LoggedInWithWorkerTestCase):
+    def setUp(self):
+        super(TestVisitApi, self).setUp()
+        self.study_subject = create_study_subject()
+        self.visit = create_visit(self.study_subject)
+
+    def test_get_columns(self):
+        response = self.client.get(reverse('web.api.visits.columns', kwargs={'visit_list_type': VISIT_LIST_GENERIC}))
+        self.assertEqual(response.status_code, 200)
+
+        columns = json.loads(response.content)['columns']
+        self.assertTrue(len(columns) > 0)
+
+    def test_get_columns_for_time_exceeded(self):
+        response = self.client.get(
+            reverse('web.api.visits.columns', kwargs={'visit_list_type': VISIT_LIST_EXCEEDED_TIME}))
+        self.assertEqual(response.status_code, 200)
+
+        columns = json.loads(response.content)['columns']
+        self.assertTrue(len(columns) > 0)
+
+    def test_visits_exceeded(self):
+        response = self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_EXCEEDED_TIME}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_visits_generic(self):
+        self.visit.appointment_types.add(create_appointment_type())
+        self.visit.save()
+        response = self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_GENERIC}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_visits_unfinished(self):
+        response = self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_UNFINISHED}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_visits_missing_appointments(self):
+        response = self.client.get(
+            reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_MISSING_APPOINTMENTS}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_visits_approaching_without_appointments(self):
+        response = self.client.get(
+            reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_visits_approaching_for_mail_contact(self):
+        response = self.client.get(
+            reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_visits_invalid(self):
+        with self.assertRaises(TypeError):
+            self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': "invalid_type"}))
+
+    def test_visits_general_search(self):
+        name = "Piotrek"
+        self.study_subject.subject.first_name = name
+        self.study_subject.subject.save()
+        create_visit(self.study_subject)
+
+        params = {
+            "columns[0][search][value]": "another_name",
+            "columns[0][data]": "first_name"
+        }
+        url = ("%s" + create_get_suffix(params)) % reverse('web.api.visits',
+                                                           kwargs={'visit_list_type': VISIT_LIST_GENERIC})
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse(name in response.content)
+
+        params["columns[0][search][value]"] = name
+        url = ("%s" + create_get_suffix(params)) % reverse('web.api.visits',
+                                                           kwargs={'visit_list_type': VISIT_LIST_GENERIC})
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(name in response.content)
+
+    def check_visit_filtered(self, filters, result):
+        subjects = get_visits_filtered(Visit.objects.all(), filters)
+        self.assertEqual(len(result), subjects.count())
+        for index in range(len(result)):
+            self.assertEqual(result[index], subjects[index])
+
+    def check_visit_ordered(self, order, result):
+        visits = get_visits_order(Visit.objects.all(), order, "asc")
+        self.assertEqual(len(result), visits.count())
+        for index in range(len(result)):
+            self.assertEqual(result[index], visits[index])
+
+        visits = get_visits_order(Visit.objects.all(), order, "desc")
+        length = len(result)
+        self.assertEqual(length, visits.count())
+        for index in range(length):
+            self.assertEqual(result[length - index - 1], visits[index])
+
+    def test_visits_sort_first_name(self):
+        subject = self.study_subject
+        subject.subject.first_name = "PPP"
+        subject.subject.save()
+
+        subject2 = create_study_subject(2)
+        subject2.subject.first_name = "QQQ"
+        subject2.subject.save()
+
+        visit2 = create_visit(subject2)
+
+        self.check_visit_ordered("first_name", [self.visit, visit2])
+
+    def test_visits_sort_last_name(self):
+        subject = self.study_subject
+        subject.subject.last_name = "PPP"
+        subject.subject.save()
+
+        subject2 = create_study_subject(2)
+        subject2.subject.last_name = "QQQ"
+        subject2.subject.save()
+
+        visit2 = create_visit(subject2)
+
+        self.check_visit_ordered("last_name", [self.visit, visit2])
+
+    def test_visits_sort_default_location(self):
+        self.check_visit_ordered("default_location", [self.visit])
+
+    def test_visits_sort_flying_team(self):
+        self.check_visit_ordered("flying_team", [self.visit])
+
+    def test_visits_sort_is_finished(self):
+        self.check_visit_ordered("is_finished", [self.visit])
+
+    def test_visits_sort_post_mail_sent(self):
+        self.check_visit_ordered("post_mail_sent", [self.visit])
+
+    def test_visits_sort_datetime_begin(self):
+        visit1 = self.visit
+        visit2 = create_visit(self.study_subject)
+        visit1.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=1)
+        visit2.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=2)
+        visit1.save()
+        visit2.save()
+
+        self.check_visit_ordered("datetime_begin", [visit1, visit2])
+
+    def test_visits_sort_datetime_end(self):
+        visit1 = self.visit
+        visit2 = create_visit(self.study_subject)
+        visit1.datetime_end = get_today_midnight_date() + datetime.timedelta(days=1)
+        visit2.datetime_end = get_today_midnight_date() + datetime.timedelta(days=2)
+        visit1.save()
+        visit2.save()
+
+        self.check_visit_ordered("datetime_end", [visit1, visit2])
+
+    def test_visits_sort_visit_number(self):
+        visit1 = self.visit
+        visit2 = create_visit(self.study_subject)
+
+        visit2.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=2)
+        visit2.save()
+        visit1.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=1)
+        visit1.save()
+
+        # visit_number is adjusted automatically according to start date
+        self.check_visit_ordered("visit_number", [visit1, visit2])
+
+    def test_visits_sort_unknown(self):
+        self.check_visit_ordered("some_unknown", [self.visit])
+        self.check_visit_ordered("", [self.visit])
+        self.check_visit_ordered(None, [self.visit])
+
+    def test_visits_filter_last_name(self):
+        subject = self.study_subject
+        subject.subject.last_name = "XXX"
+        subject.subject.save()
+
+        self.check_visit_filtered([["last_name", "Q"]], [])
+        self.check_visit_filtered([["last_name", "xx"]], [self.visit])
+
+    def test_visits_filter_flying_team(self):
+        subject = self.study_subject
+        subject.flying_team = create_flying_team()
+        subject.save()
+
+        self.check_visit_filtered([["flying_team", "-1"]], [])
+        self.check_visit_filtered([["flying_team", str(subject.flying_team.id)]], [self.visit])
+
+    def test_visits_filter_default_location(self):
+        self.check_visit_filtered([["default_location", "-1"]], [])
+        self.check_visit_filtered([["default_location", str(self.study_subject.default_location.id)]], [self.visit])
+
+    def test_visits_filter_is_finished(self):
+        self.check_visit_filtered([["is_finished", str(not self.visit.is_finished).lower()]], [])
+        self.check_visit_filtered([["is_finished", str(self.visit.is_finished).lower()]], [self.visit])
+
+    def test_visits_filter_post_mail_sent(self):
+        self.check_visit_filtered([["post_mail_sent", str(not self.visit.post_mail_sent).lower()]], [])
+        self.check_visit_filtered([["post_mail_sent", str(self.visit.post_mail_sent).lower()]], [self.visit])
+
+    def test_visits_filter_visit_number(self):
+        self.check_visit_filtered([["visit_number", "-1"]], [])
+        self.check_visit_filtered([["visit_number", str(self.visit.visit_number)]], [self.visit])
+
+    def test_visits_filter_visible_appointment_types(self):
+        appointment_type_1 = create_appointment_type()
+        appointment_type_2 = create_appointment_type()
+        self.visit.appointment_types.add(appointment_type_1)
+        self.visit.save()
+
+        self.check_visit_filtered([["visible_appointment_types", str(appointment_type_2.id)]], [])
+        self.check_visit_filtered([["visible_appointment_types", str(appointment_type_1.id)]], [self.visit])
+
+    def test_visits_filter_visible_appointment_types_missing(self):
+        appointment_type_1 = create_appointment_type()
+        appointment_type_2 = create_appointment_type()
+        self.visit.appointment_types.add(appointment_type_1)
+        self.visit.save()
+        appointment = create_appointment(self.visit)
+
+        AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=appointment_type_2)
+
+        self.check_visit_filtered([["visible_appointment_types_missing", str(appointment_type_2.id)]], [])
+        self.check_visit_filtered([["visible_appointment_types_missing", str(appointment_type_1.id)]], [self.visit])
+
+    def test_visits_filter_visible_appointment_types_done(self):
+        appointment_type_1 = create_appointment_type()
+        appointment_type_2 = create_appointment_type()
+        self.visit.appointment_types.add(appointment_type_1)
+        self.visit.save()
+        appointment = create_appointment(self.visit)
+        appointment.status = Appointment.APPOINTMENT_STATUS_FINISHED
+        appointment.save()
+
+        AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=appointment_type_1)
+
+        self.check_visit_filtered([["visible_appointment_types_done", str(appointment_type_2.id)]], [])
+        self.check_visit_filtered([["visible_appointment_types_done", str(appointment_type_1.id)]], [self.visit])
+
+    def test_visits_filter_visible_appointment_types_in_progress(self):
+        appointment_type_1 = create_appointment_type()
+        appointment_type_2 = create_appointment_type()
+        self.visit.appointment_types.add(appointment_type_1)
+        self.visit.save()
+        appointment = create_appointment(self.visit)
+
+        AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=appointment_type_1)
+
+        self.check_visit_filtered([["visible_appointment_types_in_progress", str(appointment_type_2.id)]], [])
+        self.check_visit_filtered([["visible_appointment_types_in_progress", str(appointment_type_1.id)]], [self.visit])
+
+    def test_visits_filter_unknown(self):
+        self.check_visit_filtered([["some_unknown", "unknown data"]], [self.visit])
+        self.check_visit_filtered([["", ""]], [self.visit])
+        self.check_visit_filtered([[None, None]], [self.visit])
diff --git a/smash/web/tests/view/test_notifications.py b/smash/web/tests/view/test_notifications.py
index d25525fa92f0d4d459b39ea8a174c8f129154221..8bc8200467724641b13de55bc0c6d33f1a914dbd 100644
--- a/smash/web/tests/view/test_notifications.py
+++ b/smash/web/tests/view/test_notifications.py
@@ -144,7 +144,7 @@ class NotificationViewTests(LoggedInTestCase):
         visit.save()
 
         visits = get_unfinished_visits(self.user)
-        self.assertEquals(3, len(visits))
+        self.assertEquals(3, visits.count())
 
         # check sort order
         self.assertTrue(visits[0].datetime_begin < visits[1].datetime_begin)
diff --git a/smash/web/urls.py b/smash/web/urls.py
index 6376c8d2a6300e3a9beaea96528ec2e919ec5d5d..1e97079064f8895f1a7b55dc7651153fe2ba7b72 100644
--- a/smash/web/urls.py
+++ b/smash/web/urls.py
@@ -133,7 +133,6 @@ urlpatterns = [
     #       MAIL       #
     ####################
 
-    url(r'^mail_templates_old$', views.mails.mail_templates, name='web.views.mail_templates_old'),
     url(r'^mail_templates$', views.mails.MailTemplatesListView.as_view(), name='web.views.mail_templates'),
     url(r'^mail_templates/add$', views.mails.MailTemplatesCreateView.as_view(), name='web.views.mail_template_add'),
     url(r'^mail_templates/(?P<pk>\d+)/delete$', views.mails.MailTemplatesDeleteView.as_view(),
diff --git a/smash/web/views/mails.py b/smash/web/views/mails.py
index 8ca773008e269e26c2c9a03010ff25ad5f8df05a..78d33d4189374f9776058715650dec560b13d0d9 100644
--- a/smash/web/views/mails.py
+++ b/smash/web/views/mails.py
@@ -12,7 +12,7 @@ from django.views.generic import DeleteView
 from django.views.generic import ListView
 from django.views.generic import UpdateView
 
-from . import wrap_response, WrappedView
+from . import WrappedView
 from ..models import StudySubject, Visit, Appointment, MailTemplate
 from ..models.constants import MAIL_TEMPLATE_CONTEXT_SUBJECT, MAIL_TEMPLATE_CONTEXT_VISIT, \
     MAIL_TEMPLATE_CONTEXT_APPOINTMENT
@@ -26,10 +26,6 @@ CONTEXT_TYPES_MAPPING = {
 }
 
 
-def mail_templates(request):
-    return wrap_response(request, "mail_templates/index.html", {})
-
-
 class MailTemplatesListView(ListView, WrappedView):
     model = MailTemplate
     context_object_name = "mail_templates"
diff --git a/smash/web/views/notifications.py b/smash/web/views/notifications.py
index d6e8d62ff461acfbe5b7be0d758bb757adb75962..c698553b8ba8f1cb4b8e8ebedda0d4f45d0810eb 100644
--- a/smash/web/views/notifications.py
+++ b/smash/web/views/notifications.py
@@ -2,7 +2,7 @@
 import datetime
 
 from django.contrib.auth.models import User, AnonymousUser
-from django.db.models import Count, Case, When
+from django.db.models import Count, Case, When, Q, F
 from django.utils import timezone
 
 from web.models import Study
@@ -71,7 +71,7 @@ def get_subject_with_no_visit_notifications_count(user):
 def get_visits_without_appointments_count(user):
     notification = NotificationCount(
         title="unfinished visits",
-        count=len(get_unfinished_visits(user)),
+        count=get_unfinished_visits(user).count(),
         style="fa fa-user-times text-yellow",
         type='web.views.unfinished_visits')
     return notification
@@ -80,7 +80,7 @@ def get_visits_without_appointments_count(user):
 def get_visits_with_missing_appointments_count(user):
     notification = NotificationCount(
         title="visits with missing appointments",
-        count=len(get_active_visits_with_missing_appointments(user)),
+        count=get_active_visits_with_missing_appointments(user).count(),
         style="fa fa-user-times text-yellow",
         type='web.views.visits_with_missing_appointments')
     return notification
@@ -169,19 +169,13 @@ def get_subjects_with_reminder(user):
 
 
 def get_active_visits_with_missing_appointments(user):
-    result = []
-    for visit in get_active_visits_without_appointments(user):
-        if waiting_for_appointment(visit):
-            result.append(visit)
-    return result
+    visits_without_appointments = get_active_visits_without_appointments(user)
+    return visits_without_appointments.filter(types_in_system_count__lt=F("types_expected_in_system_count"))
 
 
 def get_unfinished_visits(user):
-    result = []
-    for visit in get_active_visits_without_appointments(user):
-        if not waiting_for_appointment(visit):
-            result.append(visit)
-    return result
+    visits_without_appointments = get_active_visits_without_appointments(user)
+    return visits_without_appointments.filter(types_in_system_count__gte=F("types_expected_in_system_count"))
 
 
 def get_approaching_visits_without_appointments(user):
@@ -259,7 +253,16 @@ def get_active_visits_without_appointments(user):
         datetime_end__gt=today,
         is_finished=False,
         subject__default_location__in=get_filter_locations(user),
-        my_count=0).order_by('datetime_begin')
+        my_count=0).order_by('datetime_begin').annotate(
+        # types_in_system_count annotation counts how many different appointment types were scheduled or already
+        # performed
+        types_in_system_count=Count(Case(When(
+            Q(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED) |
+            Q(appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED)
+            , then="appointment__appointment_types")), distinct=True)).annotate(
+        # types_expected_in_system_count annotation counts how many different appointment types should be performed in
+        # the visit
+        types_expected_in_system_count=Count('appointment_types', distinct=True))
 
 
 def get_filter_locations(user):
diff --git a/smash/web/views/visit.py b/smash/web/views/visit.py
index 0dd67c3b3f67cabb631f9b08ee00359081b1694a..7a1e821b71fe0b36b4b49d0ac2de19eaadefa8af 100644
--- a/smash/web/views/visit.py
+++ b/smash/web/views/visit.py
@@ -3,9 +3,10 @@ import logging
 
 from django.shortcuts import get_object_or_404, redirect
 
-from notifications import get_active_visits_with_missing_appointments, get_unfinished_visits, \
-    get_approaching_visits_without_appointments, get_approaching_visits_for_mail_contact, get_exceeded_visits, \
-    waiting_for_appointment
+from notifications import waiting_for_appointment
+from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_MISSING_APPOINTMENTS, \
+    VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT, VISIT_LIST_EXCEEDED_TIME, \
+    VISIT_LIST_UNFINISHED
 from . import wrap_response
 from ..forms import VisitDetailForm, VisitAddForm, SubjectDetailForm, StudySubjectDetailForm
 from ..models import Visit, Appointment, StudySubject, MailTemplate
@@ -13,35 +14,32 @@ from ..models import Visit, Appointment, StudySubject, MailTemplate
 logger = logging.getLogger(__name__)
 
 
-def visits(request):
-    visit_list = Visit.objects.order_by('-datetime_begin')
-    context = {
-        'visit_list': visit_list
-    }
+def show_visits(request, visit_list_type):
+    return wrap_response(request, 'visits/index.html', {'visit_list_type': visit_list_type})
 
-    return wrap_response(request, 'visits/index.html', context)
 
+def visits(request):
+    return show_visits(request, VISIT_LIST_GENERIC)
 
-def visits_with_missing_appointments(request):
-    context = {
-        'visit_list': get_active_visits_with_missing_appointments(request.user)
-    }
 
-    return wrap_response(request, 'visits/index.html', context)
+def visits_with_missing_appointments(request):
+    return show_visits(request, VISIT_LIST_MISSING_APPOINTMENTS)
 
 
 def approaching_visits_without_appointments(request):
-    context = {
-        'visit_list': get_approaching_visits_without_appointments(request.user)
-    }
-    return wrap_response(request, 'visits/index.html', context)
+    return show_visits(request, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS)
 
 
 def approaching_visits_for_mail_contact(request):
-    context = {
-        'visit_list': get_approaching_visits_for_mail_contact(request.user)
-    }
-    return wrap_response(request, 'visits/index.html', context)
+    return show_visits(request, VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT)
+
+
+def exceeded_visits(request):
+    return show_visits(request, VISIT_LIST_EXCEEDED_TIME)
+
+
+def unfinished_visits(request):
+    return show_visits(request, VISIT_LIST_UNFINISHED)
 
 
 def visit_details(request, id):
@@ -105,18 +103,3 @@ def visit_add(request, subject_id=-1):
         form = VisitAddForm(initial={'subject': subject})
 
     return wrap_response(request, 'visits/add.html', {'form': form})
-
-
-def exceeded_visits(request):
-    context = {
-        'visit_list': get_exceeded_visits(request.user)
-    }
-    return wrap_response(request, 'visits/index.html', context)
-
-
-def unfinished_visits(request):
-    context = {
-        'visit_list': get_unfinished_visits(request.user)
-    }
-
-    return wrap_response(request, 'visits/index.html', context)