3. Řešení úkolu

Abstraktní třída PracovniUkol
namespace Komplexni_ukol_Kanban.Models
{
    public abstract class PracovniUkol
    {
        public int Id { get; set; }
        public string Nazev { get; set; } = "";

        // Výchozí stav pro Kanban nástěnku
        public string Stav { get; set; } = "Nové";
        public virtual bool JeKriticky => false;

        // Abstraktní metoda, kterou musí každý potomek přepsat
        public abstract string ZiskejTypUkolu();
    }
}

Použitím klíčového slova abstract u třídy PracovniUkol vytváříme obecnou šablonu, ze které nelze přímo tvořit instance. Nastavením výchozí hodnoty "Nové" u vlastnosti Stav zajišťujeme automatické zařazení každého nového úkolu do prvního sloupce nástěnky. Pomocí virtuální vlastnosti JeKriticky a abstraktní metody ZiskejTypUkolu() navíc připravujeme datový model pro polymorfismus, čímž si vynucujeme specifické chování u všech budoucích odvozených tříd.


Třída ExterniUkol
namespace Komplexni_ukol_Kanban.Models
{
    // Odvozená třída pro úkoly stažené z API
    public class ExterniUkol : PracovniUkol
    {
        public override string ZiskejTypUkolu() => "Získáno z API";
    }
}

Třída ExterniUkol přímo rozšiřuje bázovou třídu PracovniUkol a získává tak přístup ke všem jejím definovaným vlastnostem. Pomocí klíčového slova override dochází k polymorfnímu přepsání povinné abstraktní metody ZiskejTypUkolu(), jejíž tělo je pro maximální čistotu kódu zapsáno stručně pomocí lambda výrazu (=>) vracejícího statický text.


Třída KritickaChyba
namespace Komplexni_ukol_Kanban.Models
{
    // Odvozená třída pro manuálně zadané chyby
    public class KritickaChyba : PracovniUkol
    {
        public string Priorita { get; set; } = "Vysoká";
        public override bool JeKriticky => true;
        public override string ZiskejTypUkolu() => $"Kritická chyba ({Priorita})";
    }
}

Třída KritickaChyba rozšiřuje datový model o novou vlastnost Priorita a pomocí klíčového slova override přepisuje původní virtuální vlastnost JeKriticky z bázové třídy na hodnotu true.


Třída ApiZaznam
using System.Text.Json.Serialization;

namespace Komplexni_ukol_Kanban.Models
{
    // Pomocná třída pro zachycení surových dat z JSONu
    public class ApiZaznam
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; } = "";

        [JsonPropertyName("completed")]
        public bool Completed { get; set; }
    }
}

Pomocná třída ApiZaznam slouží jako dočasná struktura pro prvotní zachycení surových dat ze vzdáleného rozhraní REST API. Aby bylo zajištěno bezchybné napojení na přijímaný formát JSON, jsou jednotlivé vlastnosti opatřeny atributem [JsonPropertyName]. Tento přístup umožňuje zachovat v kódu C# standardní jmenné konvence a frameworku jasně specifikuje, pod jakými textovými klíči (např. "id", "title", "completed") má reálná data v odpovědi ze serveru hledat.


Razor komponenta
@page "/"
@inject HttpClient Http

<PageTitle>Nástěnka</PageTitle>

<h2>Kanban Nástěnka</h2>

<!-- Pokud ještě nebyla stažena data, zobrazí se informace o načítání -->
@if (databazeUkolu == null)
{
    <div class="alert alert-info">Načítám úkoly ze serveru...</div>
}
else
{
    <!-- Formulář pro manuální vytvoření a zařazení objektu "Kritická chyba" -->
    <div class="mb-4 p-3 bg-light border rounded">
        <h5>Nahlásit kritickou chybu</h5>
        <div class="d-flex gap-2">
            <!-- Obousměrná datová vazba pro textový vstup -->
            <input @bind="novyNazevChyby" class="form-control" placeholder="Popis problému..." />
            <button class="btn btn-danger" @onclick="PridatChybu">Vytvořit chybu</button>
        </div>
    </div>

    <div class="row">
        <!-- SLOUPEC: NOVÉ -->
        <div class="col-md-4 kanban-sloupec"
             ondragover="event.preventDefault();"
             @ondrop='() => DokoncitPresun("Nové")'>

            <h4 class="text-center bg-secondary text-white p-2 rounded">Nové</h4>

            <!-- Úkoly s vlastností JeKriticky = true seřadí OrderByDescending na vrchol -->
            @foreach (var ukol in databazeUkolu
                    .Where(u => u.Stav == "Nové")
                    .OrderByDescending(u => u.JeKriticky))
            {
                <div class="card mb-2 shadow-sm @(ukol.JeKriticky ? "border-danger" : "")"
                     draggable="true"
                     @ondragstart="() => ZahajitPresun(ukol)">
                    <div class="card-body">
                        <strong>@ukol.Nazev</strong>
                        <br />
                        <!-- Polymorfní volání metody ZiskejTypUkolu() -->
                        <span class="badge @(ukol.JeKriticky ? "bg-danger" : "bg-primary") mt-2">
                            @ukol.ZiskejTypUkolu()
                        </span>
                    </div>
                </div>
            }
        </div>

        <!-- SLOUPEC: PROBÍHÁ -->
        <div class="col-md-4 kanban-sloupec"
             ondragover="event.preventDefault();"
             @ondrop='() => DokoncitPresun("Probíhá")'>

            <h4 class="text-center bg-warning text-dark p-2 rounded">Probíhá</h4>

            @foreach (var ukol in databazeUkolu.Where(u => u.Stav == "Probíhá"))
            {
                <div class="card mb-2 shadow-sm border-warning" draggable="true" @ondragstart="() => ZahajitPresun(ukol)">
                    <div class="card-body">
                        <strong>@ukol.Nazev</strong>
                        <br />
                        <span class="badge bg-primary mt-2">@ukol.ZiskejTypUkolu()</span>
                    </div>
                </div>
            }
        </div>

        <!-- SLOUPEC: DOKONČENO -->
        <div class="col-md-4 kanban-sloupec"
             ondragover="event.preventDefault();"
             @ondrop='() => DokoncitPresun("Dokončeno")'>

            <h4 class="text-center bg-success text-white p-2 rounded">Dokončeno</h4>

            @foreach (var ukol in databazeUkolu.Where(u => u.Stav == "Dokončeno"))
            {
                <div class="card mb-2 shadow-sm border-success text-muted" draggable="true" @ondragstart="() => ZahajitPresun(ukol)">
                    <div class="card-body">
                        <del>@ukol.Nazev</del>
                        <br />
                        <span class="badge bg-secondary mt-2">@ukol.ZiskejTypUkolu()</span>
                    </div>
                </div>
            }
        </div>
    </div>
}

<style>
    /* CSS pro správné fungování cílových zón */
    .kanban-sloupec {
        min-height: 400px;
        background-color: #f8f9fa;
        border: 2px dashed #dee2e6;
        border-radius: 8px;
        padding: 10px;
    }
    .card[draggable="true"] {
        cursor: grab;
    }
    .card[draggable="true"]:active {
        cursor: grabbing;
    }
</style>

@code {
    // Hlavní databáze uchovávající polymorfní objekty (Externí úkoly i Chyby)
    private List<Models.PracovniUkol>? databazeUkolu;
    private Models.PracovniUkol? prenasenyUkol;
    private string novyNazevChyby = "";

    protected override async Task OnInitializedAsync()
    {
        databazeUkolu = new List<Models.PracovniUkol>();
        var dataZApi = await Http.GetFromJsonAsync<List<Models.ApiZaznam>>("https://jsonplaceholder.typicode.com/todos");

        if (dataZApi != null)
        {
            foreach (var polozka in dataZApi.Take(12))
            {
                databazeUkolu.Add(new Models.ExterniUkol
                {
                    Id = polozka.Id,
                    Nazev = polozka.Title,
                    Stav = polozka.Completed ? "Dokončeno" : "Nové"
                });
            }
        }
    }

    private void ZahajitPresun(Models.PracovniUkol ukol) => prenasenyUkol = ukol;

    private void DokoncitPresun(string novyStav)
    {
        if (prenasenyUkol != null && prenasenyUkol.Stav != novyStav)
        {
            prenasenyUkol.Stav = novyStav;
            prenasenyUkol = null;
        }
    }

    private void PridatChybu()
    {
        if (!string.IsNullOrWhiteSpace(novyNazevChyby) && databazeUkolu != null)
        {
            int noveId = databazeUkolu.Any() ? databazeUkolu.Max(u => u.Id) + 1 : 1;
            databazeUkolu.Add(new Models.KritickaChyba { Id = noveId, Nazev = novyNazevChyby, Stav = "Nové" });
            novyNazevChyby = "";
        }
    }
}

Metoda OnInitializedAsync asynchronně stahuje data z API a transformuje je do výchozího stavu. Výpis úkolů do sloupců řídí LINQ metoda Where, přičemž ve sloupci "Nové" se navíc uplatňuje řazení OrderByDescending(u => u.JeKriticky), což zaručuje prioritní zobrazení kritických chyb na vrcholu seznamu. Samotné přesouvání využívá nativní HTML události @ondragstart a @ondrop; po upuštění prvku dojde v metodě DokoncitPresun pouze k přepsání textové vlastnosti Stav, načež Blazor díky své reaktivitě automaticky zajistí vizuální přesun karty do správného sloupce.