diff --git a/rv_voting_calc/templates/rv_voting_calc/index.html b/rv_voting_calc/templates/rv_voting_calc/index.html
index 7858b29113a067eb2adcd8e47386c77000ff8fc7..919785c438075676bf560c5e24961ec3d0e4fdae 100644
--- a/rv_voting_calc/templates/rv_voting_calc/index.html
+++ b/rv_voting_calc/templates/rv_voting_calc/index.html
@@ -43,13 +43,15 @@
                 {% endfor %}
             </ul>
 
-            <div>
+            <div id="result">
                 
             </div>
         </div>
     </main>
 
+    {{ keyed_rv_members|json_script:"rv-members" }}
+    
     <script>
-        const RV_MEMBERS = {{ json_rv_members|safe }};
+        const VOTE_CALCULATION_ENDPOINT = "{% url "rv_voting_calc:get_calculated_votes" %}"
     </script>
 {% endblock %}
diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html b/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html
new file mode 100644
index 0000000000000000000000000000000000000000..44de8e4893ef7a817d6e60c4238cae76e3921f21
--- /dev/null
+++ b/rv_voting_calc/templates/rv_voting_calc/steps/initial_sort.html
@@ -0,0 +1,3 @@
+CIRNO CHIRUMIRU
+
+{{ options }}
diff --git a/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html b/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html
new file mode 100644
index 0000000000000000000000000000000000000000..44de8e4893ef7a817d6e60c4238cae76e3921f21
--- /dev/null
+++ b/rv_voting_calc/templates/rv_voting_calc/steps/with_support.html
@@ -0,0 +1,3 @@
+CIRNO CHIRUMIRU
+
+{{ options }}
diff --git a/rv_voting_calc/urls.py b/rv_voting_calc/urls.py
index 89f309aab2eddd8d5b04b0c2e8d50fe53fb1f33d..7f3f506150c7e427ab27b88ba35abad1f15f5bf6 100644
--- a/rv_voting_calc/urls.py
+++ b/rv_voting_calc/urls.py
@@ -5,4 +5,9 @@ from . import views
 app_name = "rv_voting_calc"
 urlpatterns = [
     path("", views.index, name="index"),
+    path(
+        "calculated-votes",
+        views.get_calculated_votes,
+        name="get_calculated_votes"
+    )
 ]
diff --git a/rv_voting_calc/views.py b/rv_voting_calc/views.py
index 4bd47b308983d16825e38631357810a7425f231c..210acb746c162e90c4c636fba021754f3eac5e9b 100644
--- a/rv_voting_calc/views.py
+++ b/rv_voting_calc/views.py
@@ -1,5 +1,5 @@
 import json
-import typing
+import math
 
 import gql
 import requests
@@ -7,9 +7,9 @@ import requests
 from gql.transport.requests import RequestsHTTPTransport
 
 from django.conf import settings
-from django.http import HttpResponseBadRequest
+from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
 from django.shortcuts import render
-from django.views.decorators.http import require_POST
+from django.template.loader import render_to_string
 
 
 def get_rv_members() -> list:
@@ -64,45 +64,200 @@ def convert_rv_members_to_dict(source: list) -> dict:
 
 def index(request):
     rv_members = get_rv_members()
-    json_rv_members = json.dumps(convert_rv_members_to_dict(rv_members))
+    keyed_rv_members = convert_rv_members_to_dict(rv_members)
 
     return render(
         request,
         "rv_voting_calc/index.html",
         {
             "rv_members": rv_members,
-            # JS-Readable format
-            "json_rv_members": json.dumps(json_rv_members),
+            "keyed_rv_members": keyed_rv_members,
         }
     )
 
 
-@require_POST
 def get_calculated_votes(request):
-    votes = request.GET.get("votes")
+    options_by_member = request.GET.get("votes")
 
-    if votes is None:
+    # BEGIN Input validation
+
+    if options_by_member is None:
         return HttpResponseBadRequest("Nebyly zadány žádné hlasy")
 
     try:
-        votes = json.loads(votes)
+        options_by_member = json.loads(options_by_member)
 
-        if not isinstance(votes, dict):
+        if not isinstance(options_by_member, dict):
             raise ValueError
     except ValueError:
         return HttpResponseBadRequest("Formát hlasů není validní")
 
     rv_members = convert_rv_members_to_dict(get_rv_members())
 
-    rv_member_names_to_clear = rv_members.keys()
-
     integrity_check_failed = False
 
-    for key, value in votes.items():
-        if key not in rv_member_names_to_clear:
+    for member, options in options_by_member.items():
+        # `member` is not in the list of known members
+        if member not in options_by_member:
+            integrity_check_failed = True
+            break
+
+        # `options` is a list
+        if not isinstance(options, list):
             integrity_check_failed = True
             break
 
-        if not isinstance(
+        # all items in `options` are strings
+        for item in options:
+            if not isinstance(item, str):
+                integrity_check_failed = True
+                break
+
+    if integrity_check_failed:
+        return HttpResponseBadRequest("Formát hlasů není validní")
+
+    # END Input validation
+
+    # BEGIN Sorting
+
+    steps = []
+
+    options = {}
+    total_ticket_count = len(options_by_member)  # 1 member = 1 ticket
+    max_vote_position = 0
+
+    for member, selected_options in options_by_member.items():
+        option_is_ticket = True
+
+        for position, option in enumerate(selected_options):
+            if position > max_vote_position:
+                max_vote_position = position
+
+            if option not in options:
+                options[option] = {
+                    "ticket_count": 0,
+                    "vote_count": 0,
+                    "points": 0,
+                    "votes": [],
+                    "has_support": False
+                }
+
+            options[option]["votes"].append({
+                "position": position,
+                "member": member
+            })
+
+            if option_is_ticket:
+                options[option]["ticket_count"] += 1
+            else:
+                options[option]["vote_count"] += 1
+
+            option_is_ticket = False
+
+    steps.append(
+        render_to_string(
+            "rv_voting_calc/steps/initial_sort.html",
+            {
+                "options": options,
+                "options_by_member": options_by_member,
+                "rv_members": rv_members
+            },
+            request=request,
+        )
+    )
+
+    # END Sorting
+
+    # BEGIN Filtering out options without enough support and figuring out
+    # the most popular options
+
+    options_to_remove = []
+
+    for option_key, option in options.items():
+        if (
+            (option["vote_count"] + option["ticket_count"])
+            >= math.ceil(total_ticket_count * 0.5)
+        ):
+            option["has_support"] = True
+
+        if not option["has_support"]:
+            options_to_remove.append(option_key)
+            continue
+
+        option["points"] += option["ticket_count"] * (max_vote_position + 1)
+
+        for vote in option["votes"]:
+            option["points"] += (
+                max_vote_position
+                - vote["position"]
+                + 1
+            )
+
+    for option_key in options_to_remove:
+        options.pop(option_key)
+
+    options = {
+        key: value
+        for key, value
+        in sorted(options.items(), key=lambda option: option[1]["points"])
+    }
+
+    steps.append(
+        render_to_string(
+            "rv_voting_calc/steps/with_support.html",
+            {
+                "options": options,
+                "options_by_member": options_by_member,
+                "rv_members": rv_members
+            },
+            request=request,
+        )
+    )
+
+    # END Filtering out options without enough support and figuring out
+    # the most popular options
+
+    return HttpResponse("\n".join(steps))
+
+    #acceptable_options = []
+    #voting_member_count = len(options_by_member)
+
+    #for option, members in members_by_option.items():
+        #if len(members) >= math.floor(voting_member_count * 0.5):
+            #acceptable_options.append(option)
+
+    #steps = []
+
+    #if len(acceptable_options) == 0:
+        #result = {
+            #"type": "final:no_acceptable_options",
+            #"voting_member_count": voting_member_count,
+            #"options": [],
+        #}
+
+        #for option, members in members_by_option.items():
+            #result["options"].append({
+                #"name": option,
+                #"vote_count": len(members),
+            #})
+
+        #steps.append(result)
+
+        #return JsonResponse(steps, safe=False)
+
+    #acceptable_options_step = {
+        #"type": "progress:acceptable_options",
+        #"voting_member_count": voting_member_count,
+        #"options": []
+    #}
+
+    #for option, members in members_by_option.items():
+        #if option in acceptable_options:
+            #acceptable_options_step["options"].append({
+                #"name": option,
+                #"vote_count": len(members),
+            #})
+
+    #steps.append(acceptable_options_step)
 
-        rv_member_names_to_clear.pop(key)
+    # END   Acceptable options
diff --git a/rybicka/urls.py b/rybicka/urls.py
index d2110e6239e7777744927a270052f2f4afd9b2b5..c5add90411d59cf57d2885ad5030aaccffb7ba41 100644
--- a/rybicka/urls.py
+++ b/rybicka/urls.py
@@ -16,7 +16,7 @@ Including another URLconf
 from django.urls import include, path
 
 urlpatterns = [
-    path("vypocet-skupiny-clenu", include("member_group_size_calc.urls")),
-    path("hlasovani-rv", include("rv_voting_calc.urls")),
+    path("vypocet-skupiny-clenu/", include("member_group_size_calc.urls")),
+    path("hlasovani-rv/", include("rv_voting_calc.urls")),
     path("", include("shared.urls")),
 ]
diff --git a/static_src/rv_voting_calc.js b/static_src/rv_voting_calc.js
index 7d21f59c7528e60a2ab8ece22f1396e47caa5c5a..881051eff0c78afa54200be6e2f0bc0ef630bbe3 100644
--- a/static_src/rv_voting_calc.js
+++ b/static_src/rv_voting_calc.js
@@ -8,6 +8,8 @@ import "select2/dist/css/select2.min.css";
 
 $(window).ready(
     () => {
+        const RV_MEMBERS = JSON.parse($("#rv-members")[0].textContent);
+
         $(".__vote-selection").select2({
             tags: true,
             tokenSeparators: [",", " "],
@@ -34,39 +36,47 @@ $(window).ready(
         $(".__vote-selection").on(
             "select2:select",
             event => {
-                //// Sync the tag option with other selectors.
-
-                // If the tag isn't new for this selection do nothing.
-                if (!event.params.data.isNew) {
-                    return;
-                }
-
-                const tagName = event.params.data.id;
-
-                const addedTag = new Option(
-                    tagName,
-                    tagName,
-                    false,
-                    false
-                );
-
-                // Get all other selections.
-                const unfilteredSelections = $(".__vote-selection").not(event.target);
-                let filteredSelections = [];
-
-                // Check if they contain the tag. If they do, ignore them.
-                for (const selection of unfilteredSelections) {
-                    if (
-                        $(selection).
-                        children(`option[value=${$.escapeSelector(tagName)}]`).
-                        length === 0
-                    ) {
-                        filteredSelections.push(selection);
-                    }
-                }
-
-                // Add the new tag to all selections that don't have it yet.
-                $(filteredSelections).append(addedTag).trigger("change");
+//                 //// Sync the tag option with other selectors.
+// 
+//                 // If the tag isn't new for this selection, do nothing.
+//                 if (!event.params.data.isNew) {
+//                     return;
+//                 }
+// 
+//                 const tagName = event.params.data.id;
+// 
+//                 const addedTag = new Option(
+//                     tagName,
+//                     tagName,
+//                     false,
+//                     false
+//                 );
+// 
+//                 // Get all other selections.
+//                 const unfilteredSelections = $(".__vote-selection").not(event.target);
+//                 let filteredSelections = [];
+// 
+//                 // Check if they contain the tag. If they do, ignore them.
+//                 for (const selection of unfilteredSelections) {
+//                     if (
+//                         $(selection).
+//                         children(`option[value=${$.escapeSelector(tagName)}]`).
+//                         length === 0
+//                     ) {
+//                         filteredSelections.push(selection);
+//                     }
+//                 }
+// 
+//                 // Add the new tag to all selections that don't have it yet.
+//                 $(filteredSelections).append(addedTag).trigger("change");
+
+                // Keep user-defined ordering. This is imperative!
+                // http://github.com/select2/select2/issues/3106 - Thanks to kevin-brown!
+                const element = $(event.params.data.element);
+                
+                element.detach();
+                $(this).append(element);
+                $(this).trigger("change");
             }
         );
 
@@ -75,50 +85,65 @@ $(window).ready(
             event => {
                 //// Remove the tag option if it's not selected anywhere.
 
-                const tagName = event.params.data.id;
-
-                // Get all other selections.
-                const selections = $(".__vote-selection");
-
-                // Check if any of them have the tag selected. If they do, end the function.
-                for (const selection of selections) {
-                    for (const data of $(selection).select2("data")) {
-                        if (data.id == tagName) {
-                            // TODO - Don't remove the tag from this select.
-                            // I'm not sure select2 has a good way of doing this.
-
-                            return;
-                        }
-                    }
-                }
-
-                // If the function has not ended by now, we can remove the tag.
-                for (const selection of selections) {
-                    (
-                        $(selection).
-                        children(`option[value=${$.escapeSelector(tagName)}]`).
-                        remove()
-                    );
-
-                    $(selection).trigger("change");
-                }
+//                 const tagName = event.params.data.id;
+// 
+//                 // Get all other selections.
+//                 const selections = $(".__vote-selection");
+// 
+//                 // Check if any of them have the tag selected. If they do, end the function.
+//                 for (const selection of selections) {
+//                     for (const data of $(selection).select2("data")) {
+//                         if (data.id == tagName) {
+//                             // TODO - Don't remove the tag from this select.
+//                             // I'm not sure select2 has a good way of doing this.
+// 
+//                             return;
+//                         }
+//                     }
+//                 }
+// 
+//                 // If the function has not ended by now, we can remove the tag.
+//                 for (const selection of selections) {
+//                     (
+//                         $(selection).
+//                         children(`option[value=${$.escapeSelector(tagName)}]`).
+//                         remove()
+//                     );
+// 
+//                     $(selection).trigger("change");
+//                 }
             }
         );
 
         $("#count-votes").on(
             "click",
-            event => {
+            async (event) => {
                 let votes = {};
 
                 for (const username of Object.keys(RV_MEMBERS)) {
+                    const selectedOptions = $(`#${$.escapeSelector(username)}-selection`).select2("data");
+
+                    if (selectedOptions.length === 0) {
+                        continue;
+                    }
+
                     votes[username] = [];
 
-                    for (const data of $(`#${$.escapeSelector(username)}-selection`).select2("data")) {
-                        votes[username].push(data.id);
+                    for (const option of selectedOptions) {
+                        votes[username].push(option.id);
                     }
                 }
 
-                console.log(votes);
+                let response = await fetch(
+                    `${VOTE_CALCULATION_ENDPOINT}?votes=${encodeURIComponent(JSON.stringify(votes))}`
+                );
+
+                if (!response.ok) {
+                    alert("Chyba při získávání výsledků hlasování.");
+                    return;
+                }
+
+                $("#result").html(await response.text());
             }
         );
     }
diff --git a/tailwind.config.js b/tailwind.config.js
index 6d9541ca09b55c509684300c618cf6c106419607..882ff8fc6f21218aac61604077d8e0cdd47ff91e 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,4 +1,4 @@
-const defaultTheme = require('tailwindcss/defaultTheme')
+const defaultTheme = require("tailwindcss/defaultTheme");
 
 /** @type {import('tailwindcss').Config} */
 module.exports = {