Likertskala

Selbst- und Fremdeinschätzung

AssessmentType bestimmt, welcher der beiden Werte bearbeitet wird und welcher nur als Markierung danebensteht. Der Umschalter zeigt dieselben Daten in allen drei Rollen; das Bewertete bleibt beim Wechsel erhalten.

@model LernzielViewModel
@{
    // AssessmentLegend liefert die CSS-Klassen und die übersetzten Texte der Legende
    // («Meine Einschätzung» / «Definitive Einschätzung»); die Auszeichnung macht die Seite.
    // fmh-lk-skala füllt das leere div, damit die Farbe überhaupt sichtbar wird.
    var legende = new AssessmentLegend();

    // Dieselben Daten, drei Rollen: Der Umschalter lädt die Seite mit einem anderen Editable-Wert
    // neu. In einem Projekt kommt die Rolle aus der Berechtigung und nicht aus der Adresse – hier
    // ist sie umschaltbar, damit der Unterschied nebeneinander zu sehen ist.
    var rollen = new[] { Editable.SelfAssessment, Editable.ExternalAssessment, Editable.ExternalAssessmentReadOnly };
}

<div class="btn-group btn-group-sm mb-3" role="group" aria-label="Rolle wechseln">
    @foreach (var rolle in rollen)
    {
        <a class="btn @(rolle == Model.Rolle ? "btn-primary" : "btn-outline-primary")"
           asp-area="Kompetenzen" asp-controller="Demo" asp-action="Einschaetzung" asp-route-rolle="@rolle">
            @Rollen.Kurzname(rolle)
        </a>
    }
</div>

<div class="alert alert-secondary py-2 px-3 mb-3">
    <strong>Rolle: @Model.RolleName</strong> <span class="text-muted small">([email protected])</span><br />
    <span class="small">@Model.RolleHinweis</span>
</div>

<div class="d-flex align-items-center gap-2 mb-3 small">
    <div class="@legende.SelfAssessmentColorCssClass fmh-lk-skala" style="width: 50px; height: 15px;"></div>
    <span class="me-3">@legende.SelfAssessmentLegentText</span>
    <div class="@legende.ExternalAssessmentColorCssClass fmh-lk-skala" style="width: 50px; height: 15px;"></div>
    <span>@legende.ExternalAssessmentLegendText</span>
</div>

@* Eine Skala, drei Rollen. Der jeweils andere Wert verschwindet nicht: Er steht als farbige
   Markierung neben dem Radiobutton und geht als Hidden-Feld unverändert mit zurück.

   Bei ExternalAssessmentReadOnly sind die Radiobuttons gesperrt; gesperrte Felder gehen bei einem
   POST wie üblich nicht mit. Die Absenden-Schaltfläche entfällt deshalb in dieser Rolle. *@
<form method="post" asp-area="Kompetenzen" asp-controller="LikertSkalaBeispiel" asp-action="Einschaetzung">
    @Html.HiddenFor(m => m.Id)
    @Html.HiddenFor(m => m.Rolle)

    @Html.Fmh(Fmh<int>.LikertSkala()
        .Id("lk-einschaetzung")
        .DescriptionColumn("Kompetenz")
        .AssessmentType(Model.Rolle)
        .Columns(Model.Spalten)
        .Bind(Model.Likertskala, Html.NameFor(m => m.Likertskala).ToString()))

    @if (Rollen.IstBearbeitbar(Model.Rolle))
    {
        @Html.Fmh(Fmh.Button()
            .Text("Bewerten")
            .Size(ButtonSize.Sm)
            .Attributes(new { @class = "mt-3" }))
    }
</form>

@* Die Demo merkt sich den erfassten Stand über den Rollenwechsel hinweg – so ist zu sehen, dass die
   Selbsteinschätzung aus der einen Rolle in der anderen als Markierung ankommt. *@
<div class="mt-3">
    @Html.Fmh(Fmh.Button()
        .Text("Ausgangslage wiederherstellen")
        .Size(ButtonSize.Sm)
        .Outline()
        .Action("Zuruecksetzen", "LikertSkalaBeispiel", new { area = "Kompetenzen", beispiel = "einschaetzung", rolle = Model.Rolle }))
</div>
using System.Collections.Generic;
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala.Daten;

namespace FMH.Komponente.WebUI.Areas.Kompetenzen.Models
{
    /// <summary>
    /// Das ViewModel einer einzelnen Likertskala. Es enthält nur, was die Skala braucht: die Zeilen
    /// und – je nach Beispiel – die Spaltendefinition.
    /// </summary>
    /// <remarks>
    /// Die Zeilen sind vom Typ <see cref="LikertSkalaRow{T}" /> der Komponente. Fachliche Daten
    /// werden beim Aufbau der Seite darauf abgebildet; ein eigener Zeilentyp ist nicht vorgesehen,
    /// weil der Builder genau diesen Typ bindet.
    /// </remarks>
    public class LernzielViewModel
    {
        /// <summary>
        /// Holt oder setzt die fachliche Id, an der der POST hängt (Zeugnis, Revision, …). Sie
        /// gehört nicht zur Skala, wird aber wie in einem Projekt als Hidden-Feld mitgeführt.
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Holt oder setzt die Zeilen der Skala. Der Name dieser Eigenschaft ist der zweite
        /// Parameter von <c>Bind(...)</c> – daraus entstehen die Feldnamen
        /// <c>Likertskala[0].SelfAssessmentValue</c>, an denen das Model-Binding die Antworten
        /// wiederfindet.
        /// </summary>
        public IList<LikertSkalaRow<int>> Likertskala { get; set; } = new List<LikertSkalaRow<int>>();

        /// <summary>
        /// Holt oder setzt die Rolle, in der die Skala gerade ausgefüllt wird. Sie steuert die
        /// Skala (<c>AssessmentType</c>), den Hinweis über ihr und – nach dem Absenden – welchen
        /// der beiden Werte der Controller übernimmt. Als Hidden-Feld geht sie mit dem POST mit.
        /// </summary>
        public Editable Rolle { get; set; } = Editable.SelfAssessment;

        /// <summary>
        /// Holt den Hinweistext zur aktuellen Rolle.
        /// </summary>
        public string RolleHinweis => Rollen.Hinweis(this.Rolle);

        /// <summary>
        /// Holt den Kurznamen der aktuellen Rolle.
        /// </summary>
        public string RolleName => Rollen.Kurzname(this.Rolle);

        /// <summary>
        /// Holt oder setzt die Spalten der Skala. Nur die Beispiele, die ihre Spalten aus den Daten
        /// beziehen, füllen sie – sonst stehen die Spalten im Markup (<c>Columns(spalten => …)</c>).
        /// </summary>
        public IList<LikertSkalaHeaderColumn<int>> Spalten { get; set; } = new List<LikertSkalaHeaderColumn<int>>();
    }
}
using System.Collections.Generic;
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala;
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala.Daten;

namespace FMH.Komponente.WebUI.Areas.Kompetenzen.Models
{
    /// <summary>
    /// Die Testdaten der Demo. Ohne Datenbank, dafür fachlich nahe an einem Zeugnis: Lernziele mit
    /// Code, Text und den bereits erfassten Einschätzungen.
    /// </summary>
    /// <remarks>
    /// Jede Methode liefert bewusst eine **neue** Liste. Die Zeilen sind veränderlich, und der POST
    /// eines Beispiels bindet direkt auf sie – eine prozessweit geteilte Liste würde sich zwischen
    /// den Seiten gegenseitig überschreiben.
    /// </remarks>
    public static class LernzielDaten
    {
        /// <summary>
        /// Die Skalenwerte als Spaltendefinition, wie sie aus einer Konfigurationstabelle käme.
        /// </summary>
        /// <remarks>
        /// **Die Reihenfolge ist fachlich bedeutsam**: Der Erfüllungsgrad vergleicht die Position
        /// der Spalten, nicht deren Werte. Die beste Stufe steht deshalb zuvorderst, «nicht
        /// beurteilbar» als Nicht-Stufe zuhinterst.
        /// </remarks>
        /// <returns>Die Spalten der Skala.</returns>
        public static IList<LikertSkalaHeaderColumn<int>> Stufen()
        {
            return new List<LikertSkalaHeaderColumn<int>>
                   {
                       new LikertSkalaHeaderColumn<int> { Value = 4, Text = "Stufe 4", Width = 130 },
                       new LikertSkalaHeaderColumn<int> { Value = 3, Text = "Stufe 3", Width = 130 },
                       new LikertSkalaHeaderColumn<int> { Value = 2, Text = "Stufe 2", Width = 130 },
                       new LikertSkalaHeaderColumn<int> { Value = 1, Text = "Stufe 1", Width = 130 },
                       new LikertSkalaHeaderColumn<int> { Value = 99, Text = "nicht beurteilbar", Width = 150 },
                   };
        }

        /// <summary>
        /// Lernziele mit einer angefangenen Selbsteinschätzung – die Ausgangslage des Grundfalls.
        /// </summary>
        /// <returns>Die Zeilen der Skala.</returns>
        public static IList<LikertSkalaRow<int>> Lernziele()
        {
            return new List<LikertSkalaRow<int>>
                   {
                       new LikertSkalaRow<int> { Code = 5, Text = "Patientinnen und Patienten umfassend beraten und begleiten", SelfAssessmentValue = 1 },
                       new LikertSkalaRow<int> { Code = 7, Text = "Befunde strukturiert dokumentieren", SelfAssessmentValue = 2 },
                       new LikertSkalaRow<int> { Code = 9, Text = "Im interprofessionellen Team zusammenarbeiten" },
                       new LikertSkalaRow<int> { Code = 11, Text = "Eigene Grenzen erkennen und Hilfe anfordern", SelfAssessmentValue = 1 },
                       new LikertSkalaRow<int> { Code = 13, Text = "Regelmässig an Weiterbildungen teilnehmen" },
                   };
        }

        /// <summary>
        /// Lernziele mit Überschrift je Zeile – Grundlage des Beispiels «Texte um die Skala».
        /// </summary>
        /// <returns>Die Zeilen der Skala.</returns>
        public static IList<LikertSkalaRow<int>> LernzieleMitUeberschrift()
        {
            return new List<LikertSkalaRow<int>>
                   {
                       new LikertSkalaRow<int>
                       {
                           Code = 5,
                           Header = "Betreuung",
                           Text = "Kann Patientinnen und Patienten umfassend beraten, begleiten und betreuen und dabei deren Autonomie gebührend respektieren",
                           SelfAssessmentValue = 2,
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 7,
                           Header = "Kommunikation",
                           Text = "Kann komplexe Sachverhalte gegenüber Angehörigen verständlich darstellen",
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 9,
                           Header = "Zusammenarbeit",
                           Text = "Kann die eigene Rolle im interprofessionellen Team einnehmen und Aufgaben delegieren",
                           SelfAssessmentValue = 1,
                       },
                   };
        }

        /// <summary>
        /// Kompetenzen mit Selbst- und Fremdeinschätzung – Grundlage der Beispiele
        /// «Selbst- und Fremdeinschätzung» und «Erfüllungsgrad».
        /// </summary>
        /// <remarks>
        /// <c>DegreeOfAchievementValue</c> ist die **geforderte** Stufe. Bewusst gemischt: erfüllt,
        /// übererfüllt, nicht erreicht, ohne Anforderung und «nicht beurteilbar» – nur so lässt sich
        /// die Prozentzahl im Kopf der Skala nachrechnen.
        /// </remarks>
        /// <returns>Die Zeilen der Skala.</returns>
        public static IList<LikertSkalaRow<int>> Kompetenzen()
        {
            return new List<LikertSkalaRow<int>>
                   {
                       new LikertSkalaRow<int>
                       {
                           Code = 5,
                           Text = "Akutes Lungenödem «A»",
                           SelfAssessmentValue = 4,
                           ExternalAssessmentValue = 4,
                           DegreeOfAchievementValue = 4,
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 7,
                           Text = "Lungenembolie «A»",
                           SelfAssessmentValue = 3,
                           ExternalAssessmentValue = 4,
                           DegreeOfAchievementValue = 3,
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 9,
                           Text = "Akuter Myokardinfarkt ACS «A»",
                           SelfAssessmentValue = 3,
                           ExternalAssessmentValue = 2,
                           DegreeOfAchievementValue = 3,
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 11,
                           Text = "Hypertensive Krise «A»",
                           SelfAssessmentValue = 3,
                           ExternalAssessmentValue = 1,
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 13,
                           Text = "Arrhythmien und Rhythmusstörungen «A»",
                           SelfAssessmentValue = 3,
                           ExternalAssessmentValue = 99,
                           DegreeOfAchievementValue = 2,
                       },
                       new LikertSkalaRow<int>
                       {
                           Code = 15,
                           Text = "Aortendissektion (Erwachsene) «B»",
                           SelfAssessmentValue = 2,
                           ExternalAssessmentValue = 2,
                           DegreeOfAchievementValue = 2,
                       },
                   };
        }

        /// <summary>
        /// Zwei Skalen mit unterschiedlichen Spalten als eine Gruppe – Grundlage des Beispiels
        /// «Mehrere Skalen gruppieren».
        /// </summary>
        /// <returns>Das Modell der Gruppe.</returns>
        public static LikertSkalaGroupModel Gruppe()
        {
            var gruppe = new LikertSkalaGroupModel
                         {
                             // Der Bearbeitungsmodus steht an der Gruppe, nicht an der einzelnen
                             // Skala: Wer eine Gruppe ausfüllt, tut das durchgehend in einer Rolle.
                             AssessmentType = Editable.ExternalAssessment,
                             GroupHeader = "Zwischenevaluation 2026",
                             Header = "Kompetenzen und Verhalten",
                             Introduction = "Bitte beide Tabellen vollständig ausfüllen.",
                             Footer = "Die Einschätzung wird mit der Kandidatin oder dem Kandidaten besprochen.",
                         };

            var fachlich = new LikertSkalaModel
                           {
                               Id = 10,
                               LikerstSkalaHeader = "<h5>Fachliche Kompetenzen</h5>",
                               Header = "Kompetenz",
                               Introduction = "Vier Stufen, dazu «nicht beurteilbar».",
                               ShowDegreeOfAchievement = true,
                               Columns = Stufen(),
                               Likertskala = Kompetenzen(),
                           };

            var verhalten = new LikertSkalaModel
                            {
                                Id = 12,
                                LikerstSkalaHeader = "<h5>Verhalten im Team</h5>",
                                Header = "Merkmal",
                                Footer = "Erfüllt bedeutet: im Alltag durchgehend beobachtbar.",

                                // Kein Skalenwert 0: Eine noch nicht beurteilte Zeile hat als int
                                // ebenfalls 0 und sähe sonst aus wie eine bewusste Antwort.
                                Columns = new List<LikertSkalaHeaderColumn<int>>
                                          {
                                              new LikertSkalaHeaderColumn<int> { Value = 1, Text = "Erfüllt", Width = 150 },
                                              new LikertSkalaHeaderColumn<int> { Value = 2, Text = "Nicht erfüllt", Width = 150 },
                                          },
                                Likertskala = new List<LikertSkalaRow<int>>
                                              {
                                                  new LikertSkalaRow<int> { Code = 21, Text = "Pünktlichkeit", ExternalAssessmentValue = 1 },
                                                  new LikertSkalaRow<int> { Code = 23, Text = "Übergaben vollständig", ExternalAssessmentValue = 2 },
                                                  new LikertSkalaRow<int> { Code = 25, Text = "Rückfragen rechtzeitig gestellt" },
                                              },
                            };

            gruppe.LikertSkalaGroup.Add(fachlich);
            gruppe.LikertSkalaGroup.Add(verhalten);

            return gruppe;
        }
    }
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala;
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala.Daten;

namespace FMH.Komponente.WebUI.Areas.Kompetenzen.Models
{
    /// <summary>
    /// Der Stand der Beispiele. Ohne ihn zeigte die Seite nach dem Absenden wieder die
    /// Ausgangsdaten – die Wirkung einer Änderung wäre nicht zu sehen.
    /// </summary>
    /// <remarks>
    /// Statt einer Datenbank eine prozessweite Ablage je Beispiel; nach einem Neustart der
    /// Anwendung steht wieder die Ausgangslage. In einem Projekt steht an dieser Stelle das
    /// Repository – der Ablauf ist derselbe.
    /// <para>
    /// <b>Übernommen wird über den <c>Code</c>, nicht die Position.</b> Aus dem Formular kommen nur
    /// Code und die Einschätzung der aktuellen Rolle zurück; die geforderte Stufe, die Überschriften
    /// und die Spalten stammen weiterhin aus der eigenen Quelle. Genau so gehört es auch in einem
    /// Projekt gemacht: Der POST liefert Antworten, nicht die Konfiguration.
    /// </para>
    /// </remarks>
    public static class LernzielZustand
    {
        private static readonly ConcurrentDictionary<string, LikertSkalaGroupModel> Gruppen =
            new ConcurrentDictionary<string, LikertSkalaGroupModel>();

        private static readonly ConcurrentDictionary<string, IList<LikertSkalaRow<int>>> Skalen =
            new ConcurrentDictionary<string, IList<LikertSkalaRow<int>>>();

        /// <summary>
        /// Der aktuelle Stand einer einzelnen Skala; beim ersten Aufruf die Ausgangslage.
        /// </summary>
        /// <param name="beispiel">Der Schlüssel des Beispiels.</param>
        /// <param name="ausgangslage">Liefert die Ausgangsdaten, wenn noch nichts erfasst wurde.</param>
        /// <returns>Die Zeilen der Skala.</returns>
        public static IList<LikertSkalaRow<int>> Skala(string beispiel, Func<IList<LikertSkalaRow<int>>> ausgangslage)
        {
            return Skalen.GetOrAdd(beispiel, _ => ausgangslage());
        }

        /// <summary>
        /// Der aktuelle Stand einer Gruppe; beim ersten Aufruf die Ausgangslage.
        /// </summary>
        /// <param name="beispiel">Der Schlüssel des Beispiels.</param>
        /// <param name="ausgangslage">Liefert die Ausgangsdaten, wenn noch nichts erfasst wurde.</param>
        /// <returns>Das Modell der Gruppe.</returns>
        public static LikertSkalaGroupModel Gruppe(string beispiel, Func<LikertSkalaGroupModel> ausgangslage)
        {
            return Gruppen.GetOrAdd(beispiel, _ => ausgangslage());
        }

        /// <summary>
        /// Übernimmt die Antworten eines POST in den Stand einer einzelnen Skala.
        /// </summary>
        /// <param name="beispiel">Der Schlüssel des Beispiels.</param>
        /// <param name="gepostet">Die Zeilen aus dem Formular.</param>
        /// <param name="rolle">Die Rolle, in der erfasst wurde.</param>
        /// <returns>Die Anzahl geänderter Zeilen.</returns>
        public static int Uebernehmen(string beispiel, IList<LikertSkalaRow<int>> gepostet, Editable rolle)
        {
            if (Skalen.TryGetValue(beispiel, out var bestand) == false)
            {
                return 0;
            }

            return Uebernehmen(bestand, gepostet, rolle);
        }

        /// <summary>
        /// Übernimmt die Antworten eines POST in den Stand einer Gruppe.
        /// </summary>
        /// <param name="beispiel">Der Schlüssel des Beispiels.</param>
        /// <param name="gepostet">Die Gruppe aus dem Formular.</param>
        /// <returns>Die Anzahl geänderter Zeilen über alle Skalen.</returns>
        public static int UebernehmenGruppe(string beispiel, LikertSkalaGroupModel gepostet)
        {
            if (Gruppen.TryGetValue(beispiel, out var bestand))
            {
                // Die Rolle steht am Bestand: AssessmentType ist Konfiguration und wird nicht
                // gepostet – am geposteten Modell stünde der Standardwert.
                var anzahl = 0;

                for (var idx = 0; idx < bestand.LikertSkalaGroup.Count; idx++)
                {
                    var geposteteSkala = gepostet?.LikertSkalaGroup.ElementAtOrDefault(idx);

                    anzahl += Uebernehmen(
                        bestand.LikertSkalaGroup[idx].Likertskala,
                        geposteteSkala?.Likertskala,
                        bestand.AssessmentType);
                }

                return anzahl;
            }

            return 0;
        }

        /// <summary>
        /// Verwirft den erfassten Stand eines Beispiels; der nächste Aufruf zeigt wieder die
        /// Ausgangslage.
        /// </summary>
        /// <param name="beispiel">Der Schlüssel des Beispiels.</param>
        public static void Zuruecksetzen(string beispiel)
        {
            Skalen.TryRemove(beispiel, out _);
            Gruppen.TryRemove(beispiel, out _);
        }

        private static int Uebernehmen(IList<LikertSkalaRow<int>> bestand, IList<LikertSkalaRow<int>> gepostet, Editable rolle)
        {
            // In dieser Rolle sind die Radiobuttons gesperrt und senden nichts – ein POST darf den
            // Stand deshalb nicht anfassen.
            if (rolle == Editable.ExternalAssessmentReadOnly || gepostet == null)
            {
                return 0;
            }

            var anzahl = 0;

            foreach (var zeile in gepostet)
            {
                var ziel = bestand.FirstOrDefault(vorhanden => vorhanden.Code == zeile.Code);

                if (ziel == null)
                {
                    continue;
                }

                if (rolle == Editable.ExternalAssessment)
                {
                    if (ziel.ExternalAssessmentValue != zeile.ExternalAssessmentValue)
                    {
                        ziel.ExternalAssessmentValue = zeile.ExternalAssessmentValue;
                        anzahl++;
                    }
                }
                else if (ziel.SelfAssessmentValue != zeile.SelfAssessmentValue)
                {
                    ziel.SelfAssessmentValue = zeile.SelfAssessmentValue;
                    anzahl++;
                }
            }

            return anzahl;
        }
    }
}
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala.Daten;
using FMH.Komponente.WebUI.Areas.Kompetenzen.Models;
using Microsoft.AspNetCore.Mvc;

namespace FMH.Komponente.WebUI.Areas.Kompetenzen.Controllers.Beispiele
{
    /// <summary>
    /// Die Gegenstelle der Beispiele. Die Skala ist ein Eingabe-Steuerelement – ohne einen POST, der
    /// die Antworten übernimmt, bliebe die halbe Erklärung offen.
    /// </summary>
    /// <remarks>
    /// Alle Actions arbeiten nach dem PRG-Muster: übernehmen, Meldung merken, zurück auf die
    /// Demo-Seite. Der Stand liegt in LernzielZustand, damit die Seite nach dem Absenden das
    /// Erfasste zeigt und nicht wieder die Ausgangsdaten.
    /// <para>
    /// Übernommen wird über den Code der Zeile und nur der Wert der aktuellen Rolle. Alles andere –
    /// die geforderte Stufe, die Spalten, die Texte – steht in keinem Formularfeld und kommt
    /// weiterhin aus der eigenen Quelle.
    /// </para>
    /// </remarks>
    [Area("Kompetenzen")]
    public class LikertSkalaBeispielController : Controller
    {
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Einschaetzung(LernzielViewModel modell)
        {
            // Die Rolle kommt als Hidden-Feld mit und entscheidet, welcher der beiden Werte
            // geschrieben wird. Der jeweils andere reist unverändert in seinem Hidden-Feld mit.
            this.MeldeUebernahme(LernzielZustand.Uebernehmen("einschaetzung", modell.Likertskala, modell.Rolle), modell.Rolle);

            return this.RedirectToAction("Einschaetzung", "Demo", new { area = "Kompetenzen", rolle = modell.Rolle });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Erfuellungsgrad(LernzielViewModel modell)
        {
            this.MeldeUebernahme(LernzielZustand.Uebernehmen("erfuellungsgrad", modell.Likertskala, modell.Rolle), modell.Rolle);

            return this.Zurueck(nameof(this.Erfuellungsgrad));
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Grundfall(LernzielViewModel modell)
        {
            this.MeldeUebernahme(LernzielZustand.Uebernehmen("grundfall", modell.Likertskala, modell.Rolle), modell.Rolle);

            return this.Zurueck(nameof(this.Grundfall));
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Gruppe(KompetenzGruppeViewModel modell)
        {
            // Die Rolle steht bei einer Gruppe am Modell und wird nicht gepostet – der Zustand
            // nimmt sie deshalb aus dem eigenen Bestand.
            var anzahl = LernzielZustand.UebernehmenGruppe("gruppe", modell.Tabellen);

            this.AddAlert(
                anzahl == 0
                    ? "Keine Änderung – es wurde nichts anders bewertet als zuvor."
                    : $"{anzahl} Bewertung(en) über alle Skalen übernommen.",
                anzahl == 0 ? AlertType.Info : AlertType.Success,
                autoHide: true);

            return this.Zurueck(nameof(this.Gruppe));
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Texte(LernzielViewModel modell)
        {
            this.MeldeUebernahme(LernzielZustand.Uebernehmen("texte", modell.Likertskala, modell.Rolle), modell.Rolle);

            return this.Zurueck(nameof(this.Texte));
        }

        /// <summary>
        /// Verwirft den erfassten Stand eines Beispiels und zeigt wieder die Ausgangsdaten.
        /// </summary>
        /// <param name="beispiel">Der Schlüssel des Beispiels, zugleich der Name der Demo-Action.</param>
        /// <param name="rolle">Die Rolle, in die zurückgekehrt wird (nur beim Beispiel «einschaetzung»).</param>
        /// <returns>Die Umleitung auf die Demo-Seite.</returns>
        public IActionResult Zuruecksetzen(string beispiel, Editable rolle = Editable.SelfAssessment)
        {
            LernzielZustand.Zuruecksetzen(beispiel);

            this.AddAlert("Ausgangslage wiederhergestellt.", AlertType.Info, autoHide: true);

            return this.RedirectToAction(DemoAction(beispiel), "Demo", new { area = "Kompetenzen", rolle });
        }

        /// <summary>
        /// Der Schlüssel des Beispiels ist der kleingeschriebene Name der Demo-Action.
        /// </summary>
        /// <param name="beispiel">Der Schluessel des Beispiels.</param>
        /// <returns>Der Name der Demo-Action.</returns>
        private static string DemoAction(string beispiel)
        {
            switch (beispiel)
            {
                case "einschaetzung":
                    return "Einschaetzung";
                case "erfuellungsgrad":
                    return "Erfuellungsgrad";
                case "gruppe":
                    return "Gruppe";
                case "texte":
                    return "Texte";
                default:
                    return "Grundfall";
            }
        }

        private void MeldeUebernahme(int anzahl, Editable rolle)
        {
            if (Rollen.IstBearbeitbar(rolle) == false)
            {
                // Gesperrte Radiobuttons senden nichts – ein POST aus dieser Rolle darf den Stand
                // nicht anfassen.
                this.AddAlert("Nur-Ansicht-Rolle – es wurde nichts übernommen.", AlertType.Warning, autoHide: true);

                return;
            }

            this.AddAlert(
                anzahl == 0
                    ? $"Keine Änderung an «{Rollen.Kurzname(rolle)}» – es wurde nichts anders bewertet als zuvor."
                    : $"{anzahl} Bewertung(en) für «{Rollen.Kurzname(rolle)}» übernommen.",
                anzahl == 0 ? AlertType.Info : AlertType.Success,
                autoHide: true);
        }

        private IActionResult Zurueck(string action)
        {
            return this.RedirectToAction(action, "Demo", new { area = "Kompetenzen" });
        }
    }
}

Darstellung

Rolle: Self-assessment (Editable.SelfAssessment)
Erfasst wird «Self-assessment»; «definitive assessment» steht als Markierung daneben.
Self-assessment
definitive assessment
Kompetenz Stufe 4 Stufe 3 Stufe 2 Stufe 1 nicht beurteilbar
Akutes Lungenödem «A»
Lungenembolie «A»
Akuter Myokardinfarkt ACS «A»
Hypertensive Krise «A»
Arrhythmien und Rhythmusstörungen «A»
Aortendissektion (Erwachsene) «B»

Eine Skala trägt zwei Einschätzungen derselben Zeile: die der Kandidatin oder des Kandidaten (SelfAssessmentValue) und die der beurteilenden Person (ExternalAssessmentValue). Welche davon bearbeitet wird, sagt AssessmentType:

@Html.Fmh(Fmh<int>.LikertSkala()
    .DescriptionColumn("Kompetenz")
    .AssessmentType(Editable.ExternalAssessment)
    .Columns(Model.Spalten)
    .Bind(Model.Likertskala, Html.NameFor(m => m.Likertskala).ToString()))

Der jeweils andere Wert verschwindet nicht: Er wird als farbige Markierung neben dem Radiobutton angezeigt (obere Hälfte Selbst-, untere Hälfte Fremdeinschätzung) und als Hidden-Feld unverändert mitgeschickt. Ein Durchlauf Selbsteinschätzung → Fremdeinschätzung verliert deshalb nichts.

Der Rollenumschalter im Beispiel

Die drei Schaltflächen über der Skala laden dieselbe Seite mit einem anderen Editable-Wert. Wer in der Rolle «Meine Einschätzung» etwas bewertet und dann auf «Definitive Einschätzung» wechselt, sieht das eben Erfasste als Markierung wieder – der Stand bleibt über den Wechsel erhalten.

Das ist eine Demo-Freiheit: In einem Projekt kommt die Rolle aus der Berechtigung, nicht aus der Adresse. Wer beurteilen darf, entscheidet der Ablauf; die View bekommt den Wert nur noch übergeben.

Die Legende

Welche Markierung wofür steht, erklärt eine Legende über der Skala. AssessmentLegend liefert dafür die CSS-Klassen und die übersetzten Texte – «Meine Einschätzung» und «Definitive Einschätzung»:

@{ var legende = new AssessmentLegend(); }
<div class="@legende.SelfAssessmentColorCssClass fmh-lk-skala" style="width: 50px; height: 15px;"></div>
@legende.SelfAssessmentLegentText

fmh-lk-skala ist dabei nicht schmückend: Die Farbe kommt aus einem ::before-Pseudoelement, und ohne diese Klasse bleibt das leere div unsichtbar. Die beiden Farben tragen zusätzlich ein Muster (Schraffur bzw. Streifen), damit sie sich auch bei Farbfehlsichtigkeit unterscheiden.

Die Texte lassen sich am AssessmentLegend überschreiben, wenn ein Fachgebiet andere Begriffe verwendet.

Schreibgeschützt anzeigen

Editable.ExternalAssessmentReadOnly setzt disabled an jeden Radiobutton. Wie bei jedem disabled-Feld heisst das: Die Werte gehen bei einem POST nicht mit. Im Beispiel steht die schreibgeschützte Skala deshalb ausserhalb des Formulars.

Was zu beachten ist

  • AssessmentType gehört zum Ablauf, nicht zur Darstellung. Welche Rolle gerade beurteilt, ist eine Berechtigungsfrage; der Wert kommt aus dem Ablauf und nicht aus der View.
  • In einer Gruppe steht der Modus am LikertSkalaGroupModel und gilt für alle Skalen gemeinsam.