diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_action_buttons.html b/scipost_django/production/templates/production/_hx_productionstream_change_action_buttons.html
new file mode 100644
index 0000000000000000000000000000000000000000..70891420566ae5899ea220c8a87dc783b27b25f0
--- /dev/null
+++ b/scipost_django/production/templates/production/_hx_productionstream_change_action_buttons.html
@@ -0,0 +1,18 @@
+{% comment %} Only show something if the option is about to change {% endcomment %}
+{% if not current_option == new_option %}
+    {% comment %} If selected option is None, it is being removed {% endcomment %}
+    {% if new_option == "None" %}
+    <button type="submit" class="btn btn-danger">
+        Remove
+    </button>
+    {% comment %} Otherwise differentiate between changing or adding based on current option {% endcomment %}
+    {% else %}
+    <button type="submit" class="btn btn-primary">
+        {% if current_option == "None" %}
+            Add
+        {% else %}
+            Change
+        {% endif %}
+    </button>
+    {% endif %}
+{% endif %}
diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_invofficer.html b/scipost_django/production/templates/production/_hx_productionstream_change_invofficer.html
new file mode 100644
index 0000000000000000000000000000000000000000..e97de052f2746ec458ea099f770e9ea6d95b494d
--- /dev/null
+++ b/scipost_django/production/templates/production/_hx_productionstream_change_invofficer.html
@@ -0,0 +1,15 @@
+{% load bootstrap %}
+
+{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.invitations_officer.choices|length > 0 %}
+<div id="productionstream-update-invitations_officer">
+    <form hx-post="{% url 'production:update_invitations_officer' stream.id %}" hx-target="#productionstream-update-invitations_officer" hx-swap="outerHTML" class="row">
+        {% csrf_token %}
+        <div class="col">
+            {{form|bootstrap_purely_inline}}
+        </div>
+        <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'invitations_officer' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="change from:select#id_invitations_officer" hx-target="this">
+        </div>
+        <div class="text-primary">{{message}}</div>
+    </form>
+</div>
+{% endif %}
\ No newline at end of file
diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_prodofficer.html b/scipost_django/production/templates/production/_hx_productionstream_change_prodofficer.html
new file mode 100644
index 0000000000000000000000000000000000000000..ea1d130cf2c66ee49778729f172e0ed167a80a47
--- /dev/null
+++ b/scipost_django/production/templates/production/_hx_productionstream_change_prodofficer.html
@@ -0,0 +1,15 @@
+{% load bootstrap %}
+
+{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.officer.choices|length > 0 %}
+<div id="productionstream-update-officer">
+    <form hx-post="{% url 'production:update_officer' stream.id %}" hx-target="#productionstream-update-officer" hx-swap="outerHTML" class="row">
+        {% csrf_token %}
+        <div class="col">
+            {{form|bootstrap_purely_inline}}
+        </div>
+        <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'officer' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="change from:select#id_officer" hx-target="this">
+        </div>
+        <div class="text-primary">{{message}}</div>
+    </form>
+</div>
+{% endif %}
\ No newline at end of file
diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_status.html b/scipost_django/production/templates/production/_hx_productionstream_change_status.html
new file mode 100644
index 0000000000000000000000000000000000000000..4458c29f034cee7d5ce024f49b2c5440fb6fc834
--- /dev/null
+++ b/scipost_django/production/templates/production/_hx_productionstream_change_status.html
@@ -0,0 +1,15 @@
+{% load bootstrap %}
+
+{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.status.choices|length > 0 %}
+<div id="productionstream-update-status">
+    <form hx-post="{% url 'production:update_status' stream.id %}" hx-target="#productionstream-update-status" hx-swap="outerHTML" class="row">
+        {% csrf_token %}
+        <div class="col">
+            {{form|bootstrap_purely_inline}}
+        </div>
+        <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'status' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="load, change from:select#id_status" hx-target="this">
+        </div>
+        <div class="text-primary">{{message}}</div>
+    </form>
+</div>
+{% endif %}
\ No newline at end of file
diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html b/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html
new file mode 100644
index 0000000000000000000000000000000000000000..91a9c8cb37219aae03b9de10d283a05752484d32
--- /dev/null
+++ b/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html
@@ -0,0 +1,15 @@
+{% load bootstrap %}
+
+{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.supervisor.choices|length > 0 %}
+<div id="productionstream-update-supervisor">
+    <form hx-post="{% url 'production:update_supervisor' stream.id %}" hx-target="#productionstream-update-supervisor" hx-swap="outerHTML" class="row">
+        {% csrf_token %}
+        <div class="col">
+            {{form|bootstrap_purely_inline}}
+        </div>
+        <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'supervisor' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="load, change from:select#id_supervisor" hx-target="this">
+        </div>
+        <div class="text-primary">{{message}}</div>
+    </form>
+</div>
+{% endif %}
\ No newline at end of file
diff --git a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html
index 29b10ba51dd279f89fd129e020e02f7f99c06e58..4000c331b15c6871dc89a120bb928484045cb915 100644
--- a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html
+++ b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html
@@ -1,18 +1,43 @@
+{% load guardian_tags %}
+
+{% get_obj_perms request.user for productionstream as "sub_perms" %}
 
 <p>
   <strong class="text-warning">While the new production page is being built</strong>: go the the <a href="{{ productionstream.get_absolute_url }}" target="_blank">(old) stream detail page</a> to manage this stream.
 </p>
 
+<div class="row overflow-hidden" style="height: min(50vh, 40em)">
+  <div class="col-lg-6">
+    <h3>Actions</h3>
+    {% if "can_perform_supervisory_actions" in sub_perms %}
+      <div class="container">
+        <h4>Change:</h4>
+        <div class="border-primary border-start ps-2">
+          {% include "production/_hx_productionstream_change_status.html" with form=status_form stream=productionstream %}
+          {% include "production/_hx_productionstream_change_supervisor.html" with form=supervisor_form stream=productionstream %}
+          {% include "production/_hx_productionstream_change_prodofficer.html" with form=prod_officer_form stream=productionstream %}
+          {% include "production/_hx_productionstream_change_invofficer.html" with form=inv_officer_form stream=productionstream %}
+        </div>
+      </div>
+    {% endif %}
+    <ul>
+      <li>Worklog</li>
+      <li>Completed</li>
+      <li>Upload proofs</li>
+      <li>Accessibility</li>
+      <li>Send Proofs</li>
+    </ul>
+  </div>
 
-<h3>Events</h3>
-{% include 'production/_productionstream_events.html' with productionstream=productionstream events=productionstream.events.all_without_duration %}
-
-
-<button
-    hx-get="{% url 'production:_hx_event_form' productionstream_id=productionstream.id %}"
-    hx-target="#productionstream-{{ productionstream.id }}-event-form"
-    hx-trigger="click"
->
-  Add a comment to this stream
-</button>
-<div id="productionstream-{{ productionstream.id }}-event-form"></div>
+  <div id="productionstream-{{ productionstream.id }}-event-container"
+       class="col-lg-6 h-100 overflow-scroll">
+    <h3>Events</h3>
+    {% include "production/_productionstream_events.html" with productionstream=productionstream events=productionstream.events.all_without_duration %}
+    <div id="productionstream-{{ productionstream.id }}-event-new-comment-form">
+      <button hx-get="{% url 'production:_hx_event_form' productionstream_id=productionstream.id %}"
+              hx-target="#productionstream-{{ productionstream.id }}-event-new-comment-form"
+              hx-trigger="click"
+              hx-swap="outerHTML">Add a comment to this stream</button>
+    </div>
+  </div>
+</div>
diff --git a/scipost_django/production/templates/production/_stream_status_changes.html b/scipost_django/production/templates/production/_stream_status_changes.html
index 07efba92e800094e7e5bfe7ed3821df0ca0b6f98..832317d264936565a4a55187503e9ca2a9cac0c8 100644
--- a/scipost_django/production/templates/production/_stream_status_changes.html
+++ b/scipost_django/production/templates/production/_stream_status_changes.html
@@ -1,14 +1,11 @@
 {% load bootstrap %}
 
 {% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.status.choices|length > 0 %}
-  <h3>Change current stream status:</h3>
-  <form method="post" action="{% url 'production:update_status' stream.id %}" class="form-inline">
+  <form method="post" action="{% url 'production:update_status' stream.id %}" class="row">
     {% csrf_token %}
-    {{ form|bootstrap_inline }}
-    <div class="form-group row">
-      <div class="col-form-label col ms-2">
-        <button type="submit" class="btn btn-primary">Change</button>
-      </div>
+    {{ form|bootstrap_purely_inline }}
+    <div class="col-auto">
+      <button type="submit" class="btn btn-primary">Change</button>
     </div>
   </form>
 {% endif %}
diff --git a/scipost_django/production/urls.py b/scipost_django/production/urls.py
index dd60eae857f5d1226349dffd72e640a3bd082f26..2ce5894c34e2e62441f273a5c8682d208734adbe 100644
--- a/scipost_django/production/urls.py
+++ b/scipost_django/production/urls.py
@@ -71,7 +71,6 @@ urlpatterns = [
         production_views.delete_officer,
         name="delete_officer",
     ),
-    # streams
     path(
         "streams/<int:stream_id>/",
         include(
@@ -122,37 +121,82 @@ urlpatterns = [
                 ),
                 path("events/add", production_views.add_event, name="add_event"),
                 path("logs/add", production_views.add_work_log, name="add_work_log"),
-                path("officer/add", production_views.add_officer, name="add_officer"),
                 path(
-                    "officer/<int:officer_id>/remove",
-                    production_views.remove_officer,
-                    name="remove_officer",
-                ),
-                path(
-                    "invitations_officer/add",
-                    production_views.add_invitations_officer,
-                    name="add_invitations_officer",
-                ),
-                path(
-                    "invitations_officer/<int:officer_id>/remove",
-                    production_views.remove_invitations_officer,
-                    name="remove_invitations_officer",
+                    "officer",
+                    include(
+                        [
+                            path(
+                                "add",
+                                production_views.add_officer,
+                                name="add_officer",
+                            ),
+                            path(
+                                "<int:officer_id>/remove",
+                                production_views.remove_officer,
+                                name="remove_officer",
+                            ),
+                            path(
+                                "update",
+                                production_views.update_officer,
+                                name="update_officer",
+                            ),
+                        ]
+                    ),
                 ),
                 path(
-                    "supervisor/add",
-                    production_views.add_supervisor,
-                    name="add_supervisor",
+                    "invitations_officer",
+                    include(
+                        [
+                            path(
+                                "add",
+                                production_views.add_invitations_officer,
+                                name="add_invitations_officer",
+                            ),
+                            path(
+                                "<int:officer_id>/remove",
+                                production_views.remove_invitations_officer,
+                                name="remove_invitations_officer",
+                            ),
+                            path(
+                                "update",
+                                production_views.update_invitations_officer,
+                                name="update_invitations_officer",
+                            ),
+                        ]
+                    ),
                 ),
                 path(
-                    "supervisor/<int:officer_id>/remove",
-                    production_views.remove_supervisor,
-                    name="remove_supervisor",
+                    "supervisor",
+                    include(
+                        [
+                            path(
+                                "add",
+                                production_views.add_supervisor,
+                                name="add_supervisor",
+                            ),
+                            path(
+                                "<int:officer_id>/remove",
+                                production_views.remove_supervisor,
+                                name="remove_supervisor",
+                            ),
+                            path(
+                                "update",
+                                production_views.update_supervisor,
+                                name="update_supervisor",
+                            ),
+                        ]
+                    ),
                 ),
                 path(
                     "mark_completed",
                     production_views.mark_as_completed,
                     name="mark_as_completed",
                 ),
+                path(
+                    "render_action_buttons/<str:key>",
+                    production_views.render_action_buttons,
+                    name="render_action_buttons",
+                ),
             ]
         ),
     ),
diff --git a/scipost_django/production/views.py b/scipost_django/production/views.py
index f546f01b61bf9db50f28d14164be0b9634e6f25f..8d4cb0fb8402123bd75581c6c5e71138316ff31d 100644
--- a/scipost_django/production/views.py
+++ b/scipost_django/production/views.py
@@ -55,7 +55,9 @@ from .utils import proofs_slug_to_id, ProductionUtils
 @permission_required("scipost.can_view_production", raise_exception=True)
 def production_new(request):
     form = ProductionStreamSearchForm(user=request.user)
-    context = {"search_productionstreams_form": form,}
+    context = {
+        "search_productionstreams_form": form,
+    }
     return render(request, "production/production_new.html", context)
 
 
@@ -72,7 +74,11 @@ def _hx_productionstream_list(request):
     page_obj = paginator.get_page(page_nr)
     count = paginator.count
     start_index = page_obj.start_index
-    context = {"count": count, "page_obj": page_obj, "start_index": start_index,}
+    context = {
+        "count": count,
+        "page_obj": page_obj,
+        "start_index": start_index,
+    }
     return render(request, "production/_hx_productionstream_list.html", context)
 
 
@@ -80,8 +86,25 @@ def _hx_productionstream_list(request):
 @permission_required("scipost.can_view_production", raise_exception=True)
 def _hx_productionstream_details_contents(request, productionstream_id):
     productionstream = get_object_or_404(ProductionStream, pk=productionstream_id)
+    status_form = StreamStatusForm(
+        instance=productionstream, production_user=request.user.production_user
+    )
+    supervisor_form = AssignSupervisorForm(
+        instance=productionstream,
+    )
+    inv_officer_form = AssignInvitationsOfficerForm(
+        instance=productionstream,
+    )
+    prod_officer_form = AssignOfficerForm(
+        instance=productionstream,
+    )
+
     context = {
-        "productionstream": productionstream,
+        "" "productionstream": productionstream,
+        "status_form": status_form,
+        "supervisor_form": supervisor_form,
+        "inv_officer_form": inv_officer_form,
+        "prod_officer_form": prod_officer_form,
     }
     return render(
         request,
@@ -105,10 +128,14 @@ def _hx_event_form(request, productionstream_id, event_id=None):
         form = ProductionEventForm(request.POST, instance=productionevent)
         if form.is_valid():
             form.save()
-            return redirect(reverse(
-                "production:_hx_productionstream_details_contents",
-                kwargs={"productionstream_id": productionstream.id,},
-            ))
+            return redirect(
+                reverse(
+                    "production:_hx_productionstream_details_contents",
+                    kwargs={
+                        "productionstream_id": productionstream.id,
+                    },
+                )
+            )
     elif productionevent:
         form = ProductionEventForm(instance=productionevent)
     else:
@@ -130,10 +157,14 @@ def _hx_event_form(request, productionstream_id, event_id=None):
 def _hx_event_delete(request, productionstream_id, event_id):
     productionstream = get_object_or_404(ProductionStream, pk=productionstream_id)
     ProductionEvent.objects.filter(pk=event_id).delete()
-    return redirect(reverse(
-        "production:_hx_productionstream_details_contents",
-        kwargs={"productionstream_id": productionstream.id,},
-    ))
+    return redirect(
+        reverse(
+            "production:_hx_productionstream_details_contents",
+            kwargs={
+                "productionstream_id": productionstream.id,
+            },
+        )
+    )
 
 
 ################################
@@ -215,7 +246,7 @@ def stream(request, stream_id):
     if not request.user.has_perm("scipost.can_view_all_production_streams"):
         # Restrict stream queryset if user is not supervisor
         streams = streams.filter_for_user(request.user.production_user)
-    stream = get_object_or_404(streams, id=stream_id)
+    productionstream = get_object_or_404(streams, id=stream_id)
     prodevent_form = ProductionEventForm_deprec()
     assign_officer_form = AssignOfficerForm()
     assign_invitiations_officer_form = AssignInvitationsOfficerForm()
@@ -228,11 +259,13 @@ def stream(request, stream_id):
         types = constants.PRODUCTION_OFFICERS_WORK_LOG_TYPES
     work_log_form = WorkLogForm(log_types=types)
     status_form = StreamStatusForm(
-        instance=stream, production_user=request.user.production_user
+        instance=productionstream,
+        production_user=request.user.production_user,
+        auto_id=f"productionstream_{productionstream.id}_id_%s",
     )
 
     context = {
-        "stream": stream,
+        "stream": productionstream,
         "prodevent_form": prodevent_form,
         "assign_officer_form": assign_officer_form,
         "assign_supervisor_form": assign_supervisor_form,
@@ -264,44 +297,6 @@ def user_to_officer(request):
     return redirect(reverse("production:production"))
 
 
-@is_production_user()
-@permission_required("scipost.can_promote_user_to_production_officer")
-def delete_officer(request, officer_id):
-    production_user = get_object_or_404(ProductionUser.objects.active(), id=officer_id)
-    production_user.name = "{first_name} {last_name}".format(
-        first_name=production_user.user.first_name,
-        last_name=production_user.user.last_name,
-    )
-    production_user.user = None
-    production_user.save()
-
-    messages.success(
-        request, "{user} removed as Production Officer".format(user=production_user)
-    )
-    return redirect(reverse("production:production"))
-
-
-@is_production_user()
-@permission_required(
-    "scipost.can_take_decisions_related_to_proofs", raise_exception=True
-)
-def update_status(request, stream_id):
-    stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
-    checker = ObjectPermissionChecker(request.user)
-    if not checker.has_perm("can_perform_supervisory_actions", stream):
-        return redirect(reverse("production:production", args=(stream.id,)))
-
-    p = request.user.production_user
-    form = StreamStatusForm(request.POST or None, instance=stream, production_user=p)
-
-    if form.is_valid():
-        stream = form.save()
-        messages.warning(request, "Production Stream succesfully changed status.")
-    else:
-        messages.warning(request, "The status change was invalid.")
-    return redirect(stream.get_absolute_url())
-
-
 @is_production_user()
 @permission_required("scipost.can_view_production", raise_exception=True)
 def add_event(request, stream_id):
@@ -347,6 +342,47 @@ def add_work_log(request, stream_id):
     return redirect(stream.get_absolute_url())
 
 
+@is_production_user()
+@permission_required(
+    "scipost.can_take_decisions_related_to_proofs", raise_exception=True
+)
+def update_status(request, stream_id):
+    productionstream = get_object_or_404(
+        ProductionStream.objects.ongoing(), pk=stream_id
+    )
+    checker = ObjectPermissionChecker(request.user)
+    if not checker.has_perm("can_perform_supervisory_actions", productionstream):
+        context = {
+            "stream": productionstream,
+            "message": "You do not have permission to perform this action. ",
+        }
+    else:
+        status_form = StreamStatusForm(
+            request.POST or None,
+            instance=productionstream,
+            production_user=request.user.production_user,
+        )
+
+        if status_form.is_valid():
+            stream = status_form.save()
+            status_form.fields["status"].choices = status_form.get_available_statuses()
+            message = "Production Stream succesfully changed status."
+        else:
+            message = "\\n".join(status_form.errors.values())
+
+        context = {
+            "stream": productionstream,
+            "form": status_form,
+            "message": message,
+        }
+
+    return render(
+        request,
+        "production/_hx_productionstream_change_status.html",
+        context,
+    )
+
+
 @is_production_user()
 @permission_required("scipost.can_assign_production_officer", raise_exception=True)
 @transaction.atomic
@@ -384,6 +420,109 @@ def add_officer(request, stream_id):
     return redirect(reverse("production:production", args=(stream.id,)))
 
 
+@is_production_user()
+@permission_required("scipost.can_assign_production_officer", raise_exception=True)
+@transaction.atomic
+def remove_officer(request, stream_id, officer_id):
+    stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
+    checker = ObjectPermissionChecker(request.user)
+    if not checker.has_perm("can_perform_supervisory_actions", stream):
+        return redirect(reverse("production:production", args=(stream.id,)))
+
+    if getattr(stream.officer, "id", 0) == int(officer_id):
+        officer = stream.officer
+        stream.officer = None
+        stream.save()
+        if officer not in [stream.invitations_officer, stream.supervisor]:
+            # Remove Officer from stream if not assigned anymore
+            remove_perm("can_work_for_stream", officer.user, stream)
+        messages.success(
+            request, "Officer {officer} has been removed.".format(officer=officer)
+        )
+
+    return redirect(reverse("production:production", args=(stream.id,)))
+
+
+@is_production_user()
+@permission_required("scipost.can_assign_production_officer", raise_exception=True)
+@transaction.atomic
+def update_officer(request, stream_id):
+    productionstream = get_object_or_404(
+        ProductionStream.objects.ongoing(), pk=stream_id
+    )
+    prev_officer = productionstream.officer
+    checker = ObjectPermissionChecker(request.user)
+
+    if not checker.has_perm("can_perform_supervisory_actions", productionstream):
+        context = {
+            "stream": productionstream,
+            "message": "You do not have permission to perform this action. ",
+        }
+    else:
+        prod_officer_form = AssignOfficerForm(
+            request.POST or None, instance=productionstream
+        )
+        if prod_officer_form.is_valid():
+            prod_officer_form.save()
+            officer = prod_officer_form.cleaned_data.get("officer")
+
+            # Add officer to stream if they exist.
+            if officer is not None:
+                assign_perm("can_work_for_stream", officer.user, productionstream)
+                message = f"Officer {officer} has been assigned."
+
+                event = ProductionEvent(
+                    stream=productionstream,
+                    event="assignment",
+                    comments=" tasked Production Officer with proofs production:",
+                    noted_to=officer,
+                    noted_by=request.user.production_user,
+                )
+                event.save()
+
+                # Temp fix.
+                # TODO: Implement proper email
+                ProductionUtils.load({"request": request, "stream": productionstream})
+                ProductionUtils.email_assigned_production_officer()
+
+            # Remove old officer.
+            else:
+                remove_perm("can_work_for_stream", prev_officer.user, productionstream)
+                message = f"Officer {prev_officer} has been removed."
+
+        else:
+            message = "\\n".join(prod_officer_form.errors.values())
+
+        context = {
+            "stream": productionstream,
+            "form": prod_officer_form,
+            "message": message,
+        }
+
+    return render(
+        request,
+        "production/_hx_productionstream_change_prodofficer.html",
+        context,
+    )
+
+
+@is_production_user()
+@permission_required("scipost.can_promote_user_to_production_officer")
+def delete_officer(request, officer_id):
+    production_user = get_object_or_404(ProductionUser.objects.active(), id=officer_id)
+    production_user.name = "{first_name} {last_name}".format(
+        first_name=production_user.user.first_name,
+        last_name=production_user.user.last_name,
+    )
+    production_user.user = None
+    production_user.save()
+
+    messages.success(
+        request, "{user} removed as Production Officer".format(user=production_user)
+    )
+    return redirect(reverse("production:production"))
+
+
 @is_production_user()
 @permission_required("scipost.can_assign_production_officer", raise_exception=True)
 @transaction.atomic
@@ -424,21 +563,22 @@ def add_invitations_officer(request, stream_id):
 @is_production_user()
 @permission_required("scipost.can_assign_production_officer", raise_exception=True)
 @transaction.atomic
-def remove_officer(request, stream_id, officer_id):
+def remove_invitations_officer(request, stream_id, officer_id):
     stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
     checker = ObjectPermissionChecker(request.user)
     if not checker.has_perm("can_perform_supervisory_actions", stream):
         return redirect(reverse("production:production", args=(stream.id,)))
 
-    if getattr(stream.officer, "id", 0) == int(officer_id):
-        officer = stream.officer
-        stream.officer = None
+    if getattr(stream.invitations_officer, "id", 0) == int(officer_id):
+        officer = stream.invitations_officer
+        stream.invitations_officer = None
         stream.save()
-        if officer not in [stream.invitations_officer, stream.supervisor]:
+        if officer not in [stream.officer, stream.supervisor]:
             # Remove Officer from stream if not assigned anymore
             remove_perm("can_work_for_stream", officer.user, stream)
         messages.success(
-            request, "Officer {officer} has been removed.".format(officer=officer)
+            request,
+            "Invitations Officer {officer} has been removed.".format(officer=officer),
         )
 
     return redirect(reverse("production:production", args=(stream.id,)))
@@ -447,25 +587,66 @@ def remove_officer(request, stream_id, officer_id):
 @is_production_user()
 @permission_required("scipost.can_assign_production_officer", raise_exception=True)
 @transaction.atomic
-def remove_invitations_officer(request, stream_id, officer_id):
-    stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
+def update_invitations_officer(request, stream_id):
+    productionstream = get_object_or_404(
+        ProductionStream.objects.ongoing(), pk=stream_id
+    )
+    prev_inv_officer = productionstream.invitations_officer
     checker = ObjectPermissionChecker(request.user)
-    if not checker.has_perm("can_perform_supervisory_actions", stream):
-        return redirect(reverse("production:production", args=(stream.id,)))
 
-    if getattr(stream.invitations_officer, "id", 0) == int(officer_id):
-        officer = stream.invitations_officer
-        stream.invitations_officer = None
-        stream.save()
-        if officer not in [stream.officer, stream.supervisor]:
-            # Remove Officer from stream if not assigned anymore
-            remove_perm("can_work_for_stream", officer.user, stream)
-        messages.success(
-            request,
-            "Invitations Officer {officer} has been removed.".format(officer=officer),
+    if not checker.has_perm("can_perform_supervisory_actions", productionstream):
+        context = {
+            "stream": productionstream,
+            "message": "You do not have permission to perform this action. ",
+        }
+    else:
+        inv_officer_form = AssignInvitationsOfficerForm(
+            request.POST or None, instance=productionstream
         )
+        if inv_officer_form.is_valid():
+            inv_officer_form.save()
+            inv_officer = inv_officer_form.cleaned_data.get("invitations_officer")
+
+            # Add invitations officer to stream if they exist.
+            if inv_officer is not None:
+                assign_perm("can_work_for_stream", inv_officer.user, productionstream)
+                message = f"Invitations Officer {inv_officer} has been assigned."
+
+                event = ProductionEvent(
+                    stream=productionstream,
+                    event="assignment",
+                    comments=" tasked Invitations Officer with invitations:",
+                    noted_to=inv_officer,
+                    noted_by=request.user.production_user,
+                )
+                event.save()
+
+                # Temp fix.
+                # TODO: Implement proper email
+                ProductionUtils.load({"request": request, "stream": productionstream})
+                ProductionUtils.email_assigned_invitation_officer()
+
+            # Remove old invitations officer.
+            else:
+                remove_perm(
+                    "can_work_for_stream", prev_inv_officer.user, productionstream
+                )
+                message = f"Invitations Officer {prev_inv_officer} has been removed."
 
-    return redirect(reverse("production:production", args=(stream.id,)))
+        else:
+            message = "\\n".join(inv_officer_form.errors.values())
+
+        context = {
+            "stream": productionstream,
+            "form": inv_officer_form,
+            "message": message,
+        }
+
+    return render(
+        request,
+        "production/_hx_productionstream_change_invofficer.html",
+        context,
+    )
 
 
 @is_production_user()
@@ -542,6 +723,71 @@ class UpdateEventView(UpdateView):
         return super().form_valid(form)
 
 
+@is_production_user()
+@permission_required("scipost.can_assign_production_supervisor", raise_exception=True)
+@transaction.atomic
+def update_supervisor(request, stream_id):
+    productionstream = get_object_or_404(
+        ProductionStream.objects.ongoing(), pk=stream_id
+    )
+    supervisor_form = AssignSupervisorForm(
+        request.POST or None, instance=productionstream
+    )
+    prev_supervisor = productionstream.supervisor
+
+    if supervisor_form.is_valid():
+        supervisor_form.save()
+        supervisor = supervisor_form.cleaned_data.get("supervisor")
+
+        # Add supervisor to stream if they exist.
+        if supervisor is not None:
+            message = f"Supervisor {supervisor} has been assigned."
+
+            assign_perm("can_work_for_stream", supervisor.user, productionstream)
+            assign_perm(
+                "can_perform_supervisory_actions", supervisor.user, productionstream
+            )
+
+            event = ProductionEvent(
+                stream=productionstream,
+                event="assignment",
+                comments=" assigned Production Supervisor:",
+                noted_to=supervisor,
+                noted_by=request.user.production_user,
+            )
+            event.save()
+
+            # Temp fix.
+            # TODO: Implement proper email
+            ProductionUtils.load({"request": request, "stream": productionstream})
+            ProductionUtils.email_assigned_supervisor()
+
+        # Remove old supervisor.
+        else:
+            remove_perm("can_work_for_stream", prev_supervisor.user, productionstream)
+            remove_perm(
+                "can_perform_supervisory_actions",
+                prev_supervisor.user,
+                productionstream,
+            )
+            message = f"Supervisor {supervisor} has been removed."
+
+    else:
+        message = "\\n".join(supervisor_form.errors.values())
+
+    context = {
+        "stream": productionstream,
+        "form": supervisor_form,
+        "message": message,
+    }
+
+    return render(
+        request,
+        "production/_hx_productionstream_change_supervisor.html",
+        context,
+    )
+
+
 @method_decorator(is_production_user(), name="dispatch")
 @method_decorator(
     permission_required("scipost.can_view_production", raise_exception=True),
@@ -848,3 +1094,27 @@ def send_proofs(request, stream_id, version):
 
     messages.success(request, "Proofs have been sent.")
     return redirect(stream.get_absolute_url())
+
+
+def render_action_buttons(request, stream_id, key):
+    productionstream = get_object_or_404(ProductionStream, pk=stream_id)
+
+    # Get either the id, or the id of the object and convert it to a string
+    # If this fails, set to "None"
+    current_option = getattr(productionstream, key, None)
+    current_option = getattr(current_option, "id", current_option)
+    current_option_str = str(current_option)
+
+    # Get the new option from the POST request, which is a string
+    # Set to "None" if the string is empty
+    new_option = request.POST.get(key, None)
+    new_option_str = str(new_option) or "None"
+
+    return render(
+        request,
+        "production/_hx_productionstream_change_action_buttons.html",
+        {
+            "current_option": current_option_str,
+            "new_option": new_option_str,
+        },
+    )
diff --git a/scipost_django/scipost/templatetags/bootstrap.py b/scipost_django/scipost/templatetags/bootstrap.py
index f255ebd9d8061546d689dc421151155d7156347c..ac204b325c8822d5a52b1ca623d1c9b3d1cb9943 100644
--- a/scipost_django/scipost/templatetags/bootstrap.py
+++ b/scipost_django/scipost/templatetags/bootstrap.py
@@ -60,6 +60,19 @@ def bootstrap_inline(element, args="2,10"):
     return render(element, markup_classes)
 
 
+@register.filter
+def bootstrap_purely_inline(element, args="2,10"):
+    args = [arg.strip() for arg in args.split(",")]
+    markup_classes = {
+        "label": "col-auto fs-6",
+        "value": "col",
+        "single_value": "",
+    }
+    markup_classes["form_control"] = ""
+
+    return render(element, markup_classes)
+
+
 @register.filter
 def bootstrap_grouped(element, args="2,10"):
     return bootstrap(element, args, "grouped")