Likertskala

Mehrere Skalen gruppieren

LikertSkalaGroup rendert mehrere Skalen als einen Block. Konfiguriert wird nicht mehr im Markup, sondern am LikertSkalaGroupModel – dafür ist der Aufruf in der View immer derselbe.

@model KompetenzGruppeViewModel

@* Bei einer Gruppe steht die Rolle am LikertSkalaGroupModel und gilt für alle Skalen gemeinsam. *@
<div class="alert alert-secondary py-2 px-3 mb-3">
    <strong>Rolle: @Model.RolleName</strong><br />
    <span class="small">@Model.RolleHinweis Sie gilt für alle Skalen der Gruppe gemeinsam.</span>
</div>

@* Mehrere Skalen als ein Block: LikertSkalaGroup rendert für jeden Eintrag in LikertSkalaGroup eine
   eigene Skala. Konfiguriert wird hier nichts mehr im Markup – Spalten, Texte, Bearbeitungsmodus und
   Erfüllungsgrad stehen am LikertSkalaGroupModel, also dort, wo auch die Daten herkommen.

   Der Builder ist nicht generisch: Die Gruppe arbeitet fest mit int-Skalenwerten. *@
<form method="post" asp-area="Kompetenzen" asp-controller="LikertSkalaBeispiel" asp-action="Gruppe">
    @Html.HiddenFor(m => m.Id)

    @Html.Fmh(Fmh.LikertSkalaGroup()
        .Id("lk-gruppe")
        .Bind(Model.Tabellen, Html.NameFor(m => m.Tabellen).ToString()))

    @Html.Fmh(Fmh.Button()
        .Text("Speichern")
        .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 = "gruppe" }))
</div>
using FMH.Komponente.Kompetenzen.Ui.Controls.LikertSkala;

namespace FMH.Komponente.WebUI.Areas.Kompetenzen.Models
{
    /// <summary>
    /// Das ViewModel mehrerer Likertskalas, die als ein Block erfasst werden.
    /// </summary>
    /// <remarks>
    /// Die Gruppe bringt ihr Modell selbst mit: <see cref="LikertSkalaGroupModel" /> hält die
    /// einzelnen Skalen samt deren Spalten und Texten. Das ViewModel der Seite trägt es nur.
    /// </remarks>
    public class KompetenzGruppeViewModel
    {
        /// <summary>
        /// Holt oder setzt die fachliche Id, an der der POST hängt.
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Holt oder setzt die Gruppe der Skalen. Der Name dieser Eigenschaft ist der zweite
        /// Parameter von <c>Bind(...)</c>; die Feldnamen der Radiobuttons lauten daraus
        /// <c>Tabellen.LikertSkalaGroup[0].Likertskala[0].SelfAssessmentValue</c>.
        /// </summary>
        public LikertSkalaGroupModel Tabellen { get; set; } = new LikertSkalaGroupModel();

        /// <summary>
        /// Holt den Hinweistext zur Rolle. Sie steht bei einer Gruppe am
        /// <see cref="LikertSkalaGroupModel.AssessmentType" /> und gilt für alle Skalen gemeinsam.
        /// </summary>
        public string RolleHinweis => Rollen.Hinweis(this.Tabellen.AssessmentType);

        /// <summary>
        /// Holt den Kurznamen der Rolle.
        /// </summary>
        public string RolleName => Rollen.Kurzname(this.Tabellen.AssessmentType);
    }
}
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. Sie gilt für alle Skalen der Gruppe gemeinsam.
Zwischenevaluation 2026
Kompetenzen und Verhalten
Bitte beide Tabellen vollständig ausfüllen.
Fachliche Kompetenzen
Vier Stufen, dazu «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»
Verhalten im Team
Merkmal Erfüllt Nicht erfüllt
Pünktlichkeit
Übergaben vollständig
Rückfragen rechtzeitig gestellt
Die Einschätzung wird mit der Kandidatin oder dem Kandidaten besprochen.

Ein Zeugnis besteht selten aus einer Skala. Fmh.LikertSkalaGroup() rendert mehrere untereinander, mit gemeinsamer Überschrift und gemeinsamem Bearbeitungsmodus:

@Html.Fmh(Fmh.LikertSkalaGroup()
    .Id("lk-gruppe")
    .Bind(Model.Tabellen, Html.NameFor(m => m.Tabellen).ToString()))

In der View steht damit nichts mehr über Spalten oder Texte. Alles davon liegt am LikertSkalaGroupModel, also dort, wo auch die Daten herkommen – der übliche Fall, wenn Stufen und Beschriftungen je Fachgebiet aus der Datenbank stammen.

Am Gruppenmodell Wirkung
GroupHeader, Header, Introduction Blöcke über allen Skalen
Footer Block unter allen Skalen
AssessmentType gilt für alle Skalen; je Skala einstellbar ist er nicht
LikertSkalaGroup die Skalen selbst, je als LikertSkalaModel

Die einzelne Skala trägt ihre eigenen Spalten und Texte – im Beispiel eine fünfstufige Skala mit Erfüllungsgrad und darunter eine zweistufige ohne. Bei den Texten aufpassen: LikerstSkalaHeader ist der Titel über der Skala, Header die Überschrift der ersten Spalte.

Was zu beachten ist

  • Die Element-Ids vergibt der Gruppen-Builder aus seinem Präfix (lk-gruppe-0, lk-gruppe-1). Wer eine Skala fachlich identifizieren muss, führt ihre Id in einem eigenen Hidden-Feld mit.
  • Die Konfiguration überlebt den POST nicht. Zurück kommen nur Zeilen mit Code, Text und Einschätzung – Spalten, Überschriften, Einleitung, Fusstext und AssessmentType sind weg und müssen vor der nächsten Ausgabe neu gesetzt werden. Bei einer Gruppe fällt das stärker ins Gewicht als bei einer einzelnen Skala.
  • Auch AssessmentType kommt nicht zurück. Beim Übernehmen der Antworten ist deshalb die Rolle aus dem eigenen Bestand massgebend, nicht die des geposteten Modells – dort steht der Standardwert.
  • Die Feldnamen werden entsprechend tief: Tabellen.LikertSkalaGroup[0].Likertskala[0].Code. Das Model-Binding kommt damit zurecht, solange die Eigenschaftsnamen aus NameFor stammen.