diff --git a/aleksis/apps/kort/forms.py b/aleksis/apps/kort/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..6036332b97911d095c50df5b7fec2078f5f5435f
--- /dev/null
+++ b/aleksis/apps/kort/forms.py
@@ -0,0 +1,21 @@
+from django import forms
+
+from django_select2.forms import ModelSelect2Widget
+
+from aleksis.apps.kort.models import Card
+
+
+class CardForm(forms.ModelForm):
+    class Meta:
+        model = Card
+        fields = ["person", "valid_until"]
+        widgets = {
+            "person": ModelSelect2Widget(
+                search_fields=[
+                    "first_name__icontains",
+                    "last_name__icontains",
+                    "short_name__icontains",
+                ],
+                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            ),
+        }
diff --git a/aleksis/apps/kort/menus.py b/aleksis/apps/kort/menus.py
index 4ba211e7c0d4ebb96480114bd3b7b030e52601f7..a28ff3e279d238bd3d3bfe6447015beb11be7bb6 100644
--- a/aleksis/apps/kort/menus.py
+++ b/aleksis/apps/kort/menus.py
@@ -3,14 +3,27 @@ from django.utils.translation import ugettext_lazy as _
 MENUS = {
     "NAV_MENU_CORE": [
         {
-            "name": _("Kort"),
-            "url": "test_pdf",
+            "name": _("Student ID Cards"),
+            "url": "#",
             "root": True,
+            "icon": "credit_card",
             "validators": [
                 "menu_generator.validators.is_authenticated",
                 "aleksis.core.util.core_helpers.has_person",
             ],
-            "submenu": [],
+            "submenu": [
+                {
+                    "name": _("All Cards"),
+                    "url": "cards",
+                    "icon": "list",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_cards_rule",
+                        )
+                    ],
+                },
+            ],
         }
     ]
 }
diff --git a/aleksis/apps/kort/migrations/0003_auto_20220308_2023.py b/aleksis/apps/kort/migrations/0003_auto_20220308_2023.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f756afa38d142aed0e775db89ee7eff84022d8d
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0003_auto_20220308_2023.py
@@ -0,0 +1,45 @@
+# Generated by Django 3.2.12 on 2022-03-08 19:23
+
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('kort', '0002_card_printer'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='card',
+            name='print_finished_at',
+        ),
+        migrations.RemoveField(
+            model_name='card',
+            name='print_started_at',
+        ),
+        migrations.RemoveField(
+            model_name='card',
+            name='printed_with',
+        ),
+        migrations.CreateModel(
+            name='CardLayout',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=255, verbose_name='Name')),
+                ('template', models.TextField(verbose_name='Template')),
+                ('css', models.TextField(blank=True, verbose_name='Custom CSS')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+            ],
+            options={
+                'abstract': False,
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+    ]
diff --git a/aleksis/apps/kort/migrations/0004_alter_card_chip_number.py b/aleksis/apps/kort/migrations/0004_alter_card_chip_number.py
new file mode 100644
index 0000000000000000000000000000000000000000..e99dd1d3d5cc7e9da3915ab3e5068d0aeef14208
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0004_alter_card_chip_number.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.12 on 2022-03-08 19:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('kort', '0003_auto_20220308_2023'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='card',
+            name='chip_number',
+            field=models.CharField(blank=True, max_length=255, verbose_name='Chip Number'),
+        ),
+    ]
diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index 478acd97e5b63ec9b3a1a231b0ff011cdc1ad081..a81d6ed37fecd18dc24b1e58917f163579147a31 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -1,10 +1,12 @@
+from django.core.exceptions import ValidationError
 from django.db import models
+from django.template import Context, Template
+from django.utils import timezone
 from django.utils.translation import gettext as _
 
 from aleksis.core.mixins import ExtensibleModel
 from aleksis.core.models import Person
-from django.template import Template, Context
-from django.core.exceptions import ValidationError
+
 
 class CardPrinterStatus(models.TextChoices):
     ONLINE = "online", _("Online")
@@ -42,20 +44,15 @@ class CardLayout(ExtensibleModel):
     template = models.TextField(verbose_name=_("Template"))
     css = models.TextField(verbose_name=_("Custom CSS"), blank=True)
 
-
     def get_template(self) -> Template:
         return Template(self.template)
 
-
-
     def render(self, card: "Card"):
         t = self.get_template()
         context = card.get_context()
 
         return t.render(Context(context))
 
-
-
     def validate_template(self):
         try:
             t = Template(self.template)
@@ -64,36 +61,23 @@ class CardLayout(ExtensibleModel):
             raise ValidationError(_("Template is invalid: {}").format(e))
 
 
-
-
-
 class Card(ExtensibleModel):
     person = models.ForeignKey(
         Person, models.CASCADE, verbose_name=_("Person"), related_name="cards"
     )
-    chip_number = models.IntegerField(verbose_name=_("Chip Number"))
+    chip_number = models.CharField(verbose_name=_("Chip Number"), blank=True, max_length=255)
     valid_until = models.DateField(verbose_name=_("Valid until"))
     deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False)
 
-    # Print status
-    printed_with = models.ForeignKey(
-        CardPrinter,
-        on_delete=models.SET_NULL,
-        blank=True,
-        null=True,
-        verbose_name=_("Printed with"),
-    )
-    print_started_at = models.DateTimeField(verbose_name=_("Printed at"), blank=True, null=True)
-    print_finished_at = models.DateTimeField(verbose_name=_("Printed at"), blank=True, null=True)
-
     @property
-    def print_status(self) -> PrintStatus:
-        if self.print_finished_at:
-            return PrintStatus.FINISHED
-        elif self.print_started_at:
-            return PrintStatus.IN_PROGRESS
-        else:
-            return PrintStatus.REGISTERED
+    def is_valid(self):
+        return (
+            self.valid_until <= timezone.now().date() and not self.deactivated and self.chip_number
+        )
+
+    def deactivate(self):
+        self.deactivated = True
+        self.save()
 
     def get_context(self):
         return {
diff --git a/aleksis/apps/kort/rules.py b/aleksis/apps/kort/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..706449103aa3f0c4e729fb8f5a46814ba84bd769
--- /dev/null
+++ b/aleksis/apps/kort/rules.py
@@ -0,0 +1 @@
+# TBD
diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py
new file mode 100644
index 0000000000000000000000000000000000000000..635102ccf613f0bca82da4ab8b39077bb36bfb3e
--- /dev/null
+++ b/aleksis/apps/kort/tables.py
@@ -0,0 +1,30 @@
+from django.template.loader import render_to_string
+from django.utils.translation import gettext as _
+
+from django_tables2 import A, BooleanColumn, Column, RelatedLinkColumn, Table
+
+
+class CardTable(Table):
+    """Table to list cards."""
+
+    class Meta:
+        attrs = {"class": "highlight"}
+
+    person = RelatedLinkColumn()
+    chip_number = Column(verbose_name=_("Chip number"))
+    current_status = Column(verbose_name=_("Current status"), accessor=A("pk"))
+    valid_until = Column(verbose_name=_("Valid until"))
+    deactivated = BooleanColumn(verbose_name=_("Deactivated"))
+    actions = Column(verbose_name=_("Actions"), accessor=A("pk"))
+
+    def render_current_status(self, value, record):
+        return render_to_string(
+            "components/materialize-chips.html",
+            dict(
+                content=_("Valid") if record.is_valid else _("Not valid"),
+                classes="white-text " + ("green" if record.is_valid else "red"),
+            ),
+        )
+
+    def render_actions(self, value, record):
+        return render_to_string("kort/card/actions.html", dict(pk=value, card=record))
diff --git a/aleksis/apps/kort/templates/kort/card/actions.html b/aleksis/apps/kort/templates/kort/card/actions.html
new file mode 100644
index 0000000000000000000000000000000000000000..1c39b5daffc6cc26d456c1d4c05c44f97214bde7
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/actions.html
@@ -0,0 +1,50 @@
+{% load i18n %}
+<!-- Modal Structure -->
+<div id="deactivate-modal-{{ card.pk }}" class="modal">
+  <div class="modal-content">
+    <h4>{% trans "Do you really want to deactivate the following card?" %}</h4>
+    {% include "kort/card/short.html" %}
+  </div>
+  <div class="modal-footer">
+    <a href="#!" class="modal-close waves-effect waves-green btn-flat">
+      <i class="material-icons left">close</i>
+      {% trans "Close" %}
+    </a>
+    <a href="{% url "deactivate_card" card.pk %}" class="modal-close waves-effect waves-light orange btn">
+      <i class="material-icons left">timer_off</i>
+      {% trans "Deactivate" %}
+    </a>
+  </div>
+</div>
+<div id="delete-modal-{{ card.pk }}" class="modal">
+  <div class="modal-content">
+    <h4>{% trans "Do you really want to delete the following card?" %}</h4>
+    {% include "kort/card/short.html" %}
+    <figure class="alert warning">
+      <i class="material-icons left">warning</i>
+      {% blocktrans %}
+        Please pay attention that a deletion of a card is irreversible and should be only used to clean up misprints.
+        If you just want to make a card unusable because a student has lost his card or left the school,
+        please deactivate the card instead.
+      {% endblocktrans %}
+    </figure>
+  </div>
+  <div class="modal-footer">
+    <a href="#!" class="modal-close waves-effect waves-green btn-flat">
+      <i class="material-icons left">close</i>
+      {% trans "Close" %}
+    </a>
+    <a href="{% url "delete_card" card.pk %}" class="modal-close waves-effect waves-light red btn">
+      <i class="material-icons left">delete</i>
+      {% trans "Delete" %}
+    </a>
+  </div>
+</div>
+<a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="#deactivate-modal-{{ card.pk }}">
+  <i class="material-icons left">timer_off</i>
+  {% trans "Deactivate" %}
+</a>
+<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="#delete-modal-{{ card.pk }}">
+  <i class="material-icons left">delete</i>
+  {% trans "Delete" %}
+</a>
\ No newline at end of file
diff --git a/aleksis/apps/kort/templates/kort/card/create.html b/aleksis/apps/kort/templates/kort/card/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..086400b8641fa9bf9c1d6118c8a161f266456254
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/create.html
@@ -0,0 +1,24 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n any_js %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Create card{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Create card{% endblocktrans %}{% endblock %}
+
+
+{% block content %}
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+  {% include_js "select2-materialize" %}
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/delete.html b/aleksis/apps/kort/templates/kort/card/delete.html
new file mode 100644
index 0000000000000000000000000000000000000000..f15a2df3f7ebdaec360450b8d149f5f24de3a9e5
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/delete.html
@@ -0,0 +1,33 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules material_form %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Delete Card{% endblocktrans %}{% endblock %}
+{% block no_page_title %}{% endblock %}
+
+{% block content %}
+  <p class="flow-text">{% trans "Do you really want to delete the following card?" %}</p>
+  {% include "kort/card/short.html" %}
+  <figure class="alert warning">
+    <i class="material-icons left">warning</i>
+    {% blocktrans %}
+      Please pay attention that a deletion of a card is irreversible and should be only used to clean up misprints.
+      If you just want to make a card unusable because a student has lost his card or left the school,
+      please deactivate the card instead.
+    {% endblocktrans %}
+  </figure>
+  <form method="post" action="">
+    {% csrf_token %}
+    <a href="{% url "cards" %}" class="modal-close waves-effect waves-green btn">
+      <i class="material-icons left">arrow_back</i>
+      {% trans "Go back" %}
+    </a>
+    <button type="submit" name="delete" class="modal-close waves-effect waves-light red btn">
+      <i class="material-icons left">delete</i>
+      {% trans "Delete" %}
+    </button>
+  </form>
+{% endblock %}
\ No newline at end of file
diff --git a/aleksis/apps/kort/templates/kort/card/edit.html b/aleksis/apps/kort/templates/kort/card/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..51e1cdbfbbc1245eea4f06a96c4da22deeeed3ca
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/edit.html
@@ -0,0 +1,24 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n any_js %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Edit card{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit card{% endblocktrans %}{% endblock %}
+
+
+{% block content %}
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+  {% include_js "select2-materialize" %}
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/list.html b/aleksis/apps/kort/templates/kort/card/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..2a6524b30b7ea87dd108806c21ce27360e2011ea
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/list.html
@@ -0,0 +1,40 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules material_form %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <script>
+
+  </script>
+
+
+  {% has_perm 'core.create_card_rule' user person as can_create_person %}
+
+  {% if can_create_person %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_card' %}">
+      <i class="material-icons left">add</i>
+      {% trans "Issue new card" %}
+    </a>
+  {% endif %}
+
+  {#  <h2>{% trans "Filter persons" %}</h2>#}
+  {#  <form method="get">#}
+  {#    {% form form=persons_filter.form %}{% endform %}#}
+  {#    {% trans "Search" as caption %}#}
+  {#    {% include "core/partials/save_button.html" with caption=caption icon="search" %}#}
+  {#    <button type="reset" class="btn red waves-effect waves-light">#}
+  {#      <i class="material-icons left">clear</i>#}
+  {#      {% trans "Clear" %}#}
+  {#    </button>#}
+  {#  </form>#}
+
+  {#  <h2>{% trans "Selected persons" %}</h2>#}
+  {% render_table table %}
+{% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/short.html b/aleksis/apps/kort/templates/kort/card/short.html
new file mode 100644
index 0000000000000000000000000000000000000000..4d533c4ec1980ffe319641aba483dc6081462376
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/short.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+<table>
+  <tr>
+    <th>{% trans "Person" %}</th>
+    <td>{{ card.person }}</td>
+  </tr>
+  <tr>
+    <th>{% trans "Chip number" %}</th>
+    <td>{{ card.chip_number }}</td>
+  </tr>
+  <tr>
+    <th>{% trans "Valid until" %}</th>
+    <td>{{ card.valid_until }}</td>
+  </tr>
+</table>
\ No newline at end of file
diff --git a/aleksis/apps/kort/templatetags/barcode.py b/aleksis/apps/kort/templatetags/barcode.py
index eced25e90ff18297964825f8a00e86aaaae2727d..9468d1dc8a32aea23371afc27f39fe9a844a7364 100644
--- a/aleksis/apps/kort/templatetags/barcode.py
+++ b/aleksis/apps/kort/templatetags/barcode.py
@@ -1,8 +1,9 @@
+from io import BytesIO
+
 from django import template
+from django.utils.safestring import mark_safe
 
-from io import BytesIO
 import barcode
-from django.utils.safestring import mark_safe
 
 register = template.Library()
 
@@ -11,8 +12,10 @@ register = template.Library()
 def generate_barcode(uid):
     rv = BytesIO()
     writer = barcode.writer.SVGWriter()
-    code = barcode.get('code128', uid,    writer=writer)
-    code.write(rv, options={"module_height": 5, "module_width": 0.3, "text_distance": 2, "font_size": 6})
+    code = barcode.get("code128", uid, writer=writer)
+    code.write(
+        rv, options={"module_height": 5, "module_width": 0.3, "text_distance": 2, "font_size": 6}
+    )
 
     rv.seek(0)
     # get rid of the first bit of boilerplate
@@ -23,4 +26,3 @@ def generate_barcode(uid):
     # read the svg tag into a string
     svg = rv.read()
     return mark_safe(svg.decode("utf-8"))
-
diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py
index 3a6cacd6bbca01f7527ba4db5e9839e86b98cdda..8aa00d73f70c3c65d37b862b93d9c9b75f52db55 100644
--- a/aleksis/apps/kort/urls.py
+++ b/aleksis/apps/kort/urls.py
@@ -4,4 +4,8 @@ from . import views
 
 urlpatterns = [
     path("test", views.TestPDFView.as_view(), name="test_pdf"),
+    path("cards/", views.CardListView.as_view(), name="cards"),
+    path("cards/create/", views.CardCreateView.as_view(), name="create_card"),
+    path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"),
+    path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"),
 ]
diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py
index f16addae7999e9b395aa28850eae9070ff4abd1d..bfe584c9663db438e57725eb1fb1488ec92a24a4 100644
--- a/aleksis/apps/kort/views.py
+++ b/aleksis/apps/kort/views.py
@@ -1,14 +1,20 @@
-from aleksis.core.views import RenderPDFView
-from django.contrib.auth.decorators import login_required
+from django.contrib import messages
 from django.http import HttpRequest, HttpResponse
-from django.shortcuts import render
-
+from django.shortcuts import redirect, render
+from django.urls import reverse_lazy
+from django.utils.translation import gettext as _
+from django.views import View
+from django.views.generic.detail import SingleObjectMixin
 
-@login_required
-def empty(request: HttpRequest) -> HttpResponse:
-    context = {}
+from django_tables2 import SingleTableView
+from reversion.views import RevisionMixin
+from rules.contrib.views import PermissionRequiredMixin
 
-    return render(request, "kort/empty.html", context)
+from aleksis.apps.kort.forms import CardForm
+from aleksis.apps.kort.models import Card
+from aleksis.apps.kort.tables import CardTable
+from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView
+from aleksis.core.views import RenderPDFView
 
 
 class TestPDFView(RenderPDFView):
@@ -21,4 +27,48 @@ class TestPDFView(RenderPDFView):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         context["title"] = "Test PDF"
-        return context
\ No newline at end of file
+        return context
+
+
+class CardListView(PermissionRequiredMixin, RevisionMixin, SingleTableView):
+    """List view for all cards."""
+
+    permission_required = "core.view_cards_rule"
+    template_name = "kort/card/list.html"
+    model = Card
+    table_class = CardTable
+
+
+class CardCreateView(PermissionRequiredMixin, RevisionMixin, AdvancedCreateView):
+    """View used to create a card."""
+
+    permission_required = "core.create_card_rule"
+    context_object_name = "application"
+    template_name = "kort/card/create.html"
+    form_class = CardForm
+    success_message = _("The card has been created successfully.")
+    success_url = reverse_lazy("cards")
+
+
+class CardDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+    """View used to delete a card."""
+
+    permission_required = "core.delete_card_rule"
+    success_url = reverse_lazy("cards")
+    template_name = "kort/card/delete.html"
+    model = Card
+    success_message = _("The card has been deleted successfully.")
+
+
+class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View):
+    """View used to deactivate a card."""
+
+    permission_required = "core.delete_card_rule"
+    model = Card
+    success_url = reverse_lazy("cards")
+
+    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+        self.object = self.get_object()
+        self.object.deactivate()
+        messages.success(request, _("The card has been deactivated successfully."))
+        return redirect(self.success_url)