Likertskala

Erfüllungsgrad

ShowDegreeOfAchievement stellt der Skala eine zweite Kopfzeile mit Prozentwerten voran und hebt je Zeile die geforderte Stufe hervor. Die Werte werden bei jedem Rendern aus dem aktuellen Stand gerechnet – eine Bewertung ändert sie sofort.

@model LernzielViewModel

<div class="alert alert-secondary py-2 px-3 mb-3">
    <strong>Rolle: @Model.RolleName</strong><br />
    <span class="small">@Model.RolleHinweis Die geforderte Stufe ist Konfiguration und nicht beurteilbar.</span>
</div>

@* ShowDegreeOfAchievement setzt eine zweite Kopfzeile über die Skala: links der Erfüllungsgrad der
   ganzen Kompetenzgruppe, darunter je Spalte der Anteil der Zeilen, die genau diese Stufe fordern.
   Gerechnet wird gegen DegreeOfAchievementValue der Zeile – die geforderte Stufe. In der Skala
   selbst wird die geforderte Stufe je Zeile farblich hinterlegt (fmh-degree-of-achievement).

   Die Prozentwerte ändern sich mit jeder Bewertung: Sie werden beim Rendern aus dem aktuellen Stand
   gerechnet, nicht aus den Ausgangsdaten.

   Die Spalten kommen hier aus dem Modell statt aus dem Markup: Stufen und Breiten stehen in einer
   Konfigurationstabelle, und Columns nimmt die fertige Liste direkt entgegen. *@
<form method="post" asp-area="Kompetenzen" asp-controller="LikertSkalaBeispiel" asp-action="Erfuellungsgrad">
    @Html.HiddenFor(m => m.Id)
    @Html.HiddenFor(m => m.Rolle)

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

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

<div class="mt-3">
    @Html.Fmh(Fmh.Button()
        .Text("Ausgangslage wiederherstellen")
        .Size(ButtonSize.Sm)
        .Outline()
        .Action("Zuruecksetzen", "LikertSkalaBeispiel", new { area = "Kompetenzen", beispiel = "erfuellungsgrad" }))
</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: definitive assessment
Erfasst wird «definitive assessment»; «Self-assessment» steht als Markierung daneben. Die geforderte Stufe ist Konfiguration und nicht beurteilbar.
Kompetenz Stufe 4 Stufe 3 Stufe 2 Stufe 1 nicht beurteilbar
Degree of achievement 60% 100% 50% 50%
Akutes Lungenödem «A»
Lungenembolie «A»
Akuter Myokardinfarkt ACS «A»
Hypertensive Krise «A»
Arrhythmien und Rhythmusstörungen «A»
Aortendissektion (Erwachsene) «B»

Für einzelne Fachgebiete gibt es zu jeder Kompetenz eine geforderte Stufe. Sie steht als DegreeOfAchievementValue an der Zeile; ShowDegreeOfAchievement() macht daraus eine zweite Kopfzeile mit Prozentwerten und hebt die geforderte Stufe in jeder Zeile farblich hervor (fmh-degree-of-achievement).

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

Wie gerechnet wird

Verglichen wird nicht der Wert, sondern die Position der Spalte. Für jede Zeile, deren DegreeOfAchievementValue einem Spaltenwert entspricht:

Fall Zählt als
Einschätzung steht in derselben Spalte wie die Forderung erfüllt
Einschätzung steht weiter links übererfüllt
Einschätzung steht weiter rechts oder fehlt nicht erfüllt

Prozent = (erfüllt + übererfüllt) × 100 ÷ Anzahl der Zeilen mit Anforderung. Herangezogen wird der Wert des aktiven Modus – bei ExternalAssessment also die Fremdeinschätzung.

Die Spalten sind deshalb von der besten zur schwächsten Stufe zu ordnen, eine Nicht-Stufe wie «nicht beurteilbar» ans Ende. Genau so sind sie im Beispiel und in den bestehenden Fachgebieten angelegt.

Im Beispiel: sechs Zeilen, davon fünf mit Anforderung. Eine ist übererfüllt, zwei sind erfüllt, zwei nicht – das ergibt die 60 % im Kopf der Skala. Je Spalte steht derselbe Anteil, bezogen nur auf die Zeilen, die genau diese Stufe fordern; fordert keine Zeile eine Stufe, bleibt die Zelle leer.

Gerechnet wird bei jedem Rendern aus den übergebenen Zeilen: Wer im Beispiel «Akuter Myokardinfarkt» von Stufe 2 auf Stufe 3 hebt und absendet, sieht anschliessend 80 %.

Was zu beachten ist

  • Die geforderte Stufe kommt nicht zurück. Sie ist Konfiguration und steckt in keinem Formularfeld – nach dem POST wird sie wie die Spalten neu gesetzt, sonst fehlt die Kopfzeile.
  • Die Prozentwerte stehen zusätzlich in den data-title-Attributen der Zellen – daraus baut die schmale Darstellung ihre Beschriftungen.