Dataczwartek, 13 sierpnia 2026 Czas18:03:43
← Aplikacje webowe

Lekcja 23. Projekt 4: System zamówień restauracji (Angular + EF Core, relacje)

Trudny / egzaminacyjny

Po co się tego uczymy?

Trzeci projekt to pełny system zarządzania menu i zamówieniami restauracji — najbardziej rozbudowany dotąd, łączący WSZYSTKIE elementy backendu z lekcji 13-19 (relacje w EF Core, sesje, przesyłanie plików) z pełnym interfejsem Angular. To dokładnie ten typ zadania (CRUD z powiązanymi tabelami), jaki najczęściej pojawia się w części praktycznej egzaminu INF.04.

Teoria

Wymagania projektu:

  • Menu restauracji podzielone na kategorie (Przystawki, Dania główne, Desery...) — relacja jeden-do-wielu między Kategoria a Danie (EF Core, lekcja 16);
  • Każde danie ma nazwę, cenę, opis i OPCJONALNIE zdjęcie (upload plików, lekcja 17);
  • Klient wybiera dania i tworzy zamówienie — relacja jeden-do-wielu między Zamowienie a PozycjaZamowienia (każda pozycja: danie + ilość);
  • Panel kelnera/administratora (Angular) do zarządzania menu (CRUD dań i kategorii) — pełny zestaw operacji: lista, dodawanie, edycja, usuwanie;
  • Lista złożonych zamówień ze statusem ("Przyjęte" / "W przygotowaniu" / "Gotowe") — aktualizowanym przez personel.

Model danych (4 powiązane klasy, C#):

public class Kategoria { public int Id; public string Nazwa; public List<Danie> Dania; }
public class Danie { public int Id; public string Nazwa; public double Cena; public string? SciezkaZdjecia;
                      public int KategoriaId; public Kategoria? Kategoria; }
public class Zamowienie { public int Id; public DateTime DataZlozenia; public string Status;
                           public List<PozycjaZamowienia> Pozycje; }
public class PozycjaZamowienia { public int Id; public int ZamowienieId; public int DanieId;
                                   public Danie? Danie; public int Ilosc; }

Struktura komponentów Angular (rozłożenie dużej aplikacji na mniejsze części, zgodnie z lekcją 8-9): MenuComponent (lista dań pogrupowana wg kategorii) → używa DanieKartaComponent (potomny, jedna pozycja menu z przyciskiem "Dodaj do zamówienia") → KoszykZamowieniaComponent (aktualnie składane zamówienie, serwis ZamowienieService współdzielony między komponentami) → PanelAdminComponent (CRUD zarządzania menu, chroniony — pełne logowanie wykracza poza zakres tego projektu, ale można dodać prosty ekran z hasłem jako rozszerzenie).

Relacja w praktyce — pobieranie zamówienia z pozycjami. context.Zamowienia.Include(z => z.Pozycje).ThenInclude(p => p.Danie).ToListAsync()ThenInclude (nowość względem Include z lekcji 17) pozwala dociągnąć relację NA DRUGIM POZIOMIE zagnieżdżenia: nie tylko pozycje zamówienia, ale też danie przypisane do KAŻDEJ pozycji.

Schemat

Kategoria (1) ──── (N) Danie                Zamowienie (1) ──── (N) PozycjaZamowienia (N) ──── (1) Danie
"Dania główne"        "Pierogi" 24.99zł         Zamówienie #7           2× Pierogi
                      "Schabowy" 32.00zł        Status: W przygotowaniu 1× Schabowy

Angular:
MenuComponent (rodzic)
  @for (kategoria of kategorie) {
    <h3>{{ kategoria.nazwa }}</h3>
    @for (danie of kategoria.dania; track danie.id) {
      <app-danie-karta [danie]="danie" (dodano)="dodajDoZamowienia($event)">
    }
  }
        │ @Output() dodano
        ▼
ZamowienieService (singleton, przechowuje aktualnie składane zamówienie)
        │
        ▼
KoszykZamowieniaComponent — pokazuje pozycje, sumę, przycisk "Złóż zamówienie"
        │  POST /api/zamowienia
        ▼
ASP.NET Core: ZamowieniaController → EF Core → SQLite

Przykład z życia

Aplikacje do składania zamówień w restauracjach (tablety przy stolikach, systemy dla kelnerów) działają dokładnie na tej zasadzie: menu podzielone na kategorie, możliwość dodania kilku pozycji do jednego zamówienia, i śledzenie statusu przygotowania — to jeden z najbardziej praktycznych, kompletnych przykładów systemu CRUD z relacjami w prawdziwym biznesie.

Controllers/ZamowieniaController.cs

// Controllers/ZamowieniaController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace RestauracjaApp.Server.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ZamowieniaController : ControllerBase
{
    private readonly RestauracjaContext _context;
    public ZamowieniaController(RestauracjaContext context) => _context = context;

    [HttpGet]
    public async Task<ActionResult<List<Zamowienie>>> GetWszystkie()
        => Ok(await _context.Zamowienia
            .Include(z => z.Pozycje)
            .ThenInclude(p => p.Danie)
            .OrderByDescending(z => z.DataZlozenia)
            .ToListAsync());

    [HttpPost]
    public async Task<ActionResult<Zamowienie>> ZlozZamowienie(Zamowienie nowe)
    {
        nowe.DataZlozenia = DateTime.Now;
        nowe.Status = "Przyjęte";

        _context.Zamowienia.Add(nowe);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetWszystkie), nowe);
    }

    [HttpPut("{id}/status")]
    public async Task<IActionResult> ZmienStatus(int id, [FromBody] string nowyStatus)
    {
        var zamowienie = await _context.Zamowienia.FindAsync(id);
        if (zamowienie is null) return NotFound();

        zamowienie.Status = nowyStatus;
        await _context.SaveChangesAsync();
        return NoContent();
    }
}

zamowienie.service.ts

// zamowienie.service.ts - serwis przechowujący SKŁADANE zamówienie (przed wysłaniem)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface Danie {
  id: number;
  nazwa: string;
  cena: number;
  kategoriaId: number;
}

export interface PozycjaWKoszyku {
  danie: Danie;
  ilosc: number;
}

@Injectable({ providedIn: 'root' })
export class ZamowienieService {
  private pozycje: PozycjaWKoszyku[] = [];
  private adresApi = 'https://localhost:7042/api/zamowienia';

  constructor(private http: HttpClient) {}

  dodaj(danie: Danie): void {
    const istniejaca = this.pozycje.find(p => p.danie.id === danie.id);
    if (istniejaca) {
      istniejaca.ilosc++;
    } else {
      this.pozycje.push({ danie, ilosc: 1 });
    }
  }

  pobierzPozycje(): PozycjaWKoszyku[] {
    return this.pozycje;
  }

  obliczSume(): number {
    return this.pozycje.reduce((suma, p) => suma + p.danie.cena * p.ilosc, 0);
  }

  zlozZamowienie() {
    const daneDoWyslania = {
      pozycje: this.pozycje.map(p => ({ danieId: p.danie.id, ilosc: p.ilosc }))
    };
    return this.http.post(this.adresApi, daneDoWyslania);
  }

  wyczysc(): void {
    this.pozycje = [];
  }
}

Komentarz i wyjaśnienie kodu

ThenInclude(p => p.Danie) to rozszerzenie Include na DRUGI poziom relacji — Include(z => z.Pozycje) dociąga pozycje zamówienia, a ThenInclude DODATKOWO dociąga danie przypisane do KAŻDEJ z tych pozycji; bez tego drugiego kroku, pozycja.Danie pozostałoby puste, mimo że same pozycje (z samym DanieId) już by się załadowały.

W ZamowienieService.dodaj(), sprawdzenie this.pozycje.find(p => p.danie.id === danie.id) realizuje logikę "jeśli to danie jest już w koszyku, zwiększ ilość zamiast dodawać duplikat" — dokładnie ten sam wzorzec, którego oczekuje się od każdego prawdziwego koszyka zakupowego.

this.pozycje.reduce((suma, p) => suma + p.danie.cena * p.ilosc, 0) to metoda tablicy reduce — "redukuje" całą tablicę do JEDNEJ wartości (tu: sumy), zaczynając od 0 i dodając za każdym razem cena × ilość kolejnej pozycji; to funkcyjny odpowiednik pętli foreach z sumowaniem w zmiennej pomocniczej.

Ćwiczenie samodzielne

Zbuduj model danych (4 klasy z relacjami), migrację EF Core, i podstawowy kontroler API zwracający menu pogrupowane po kategoriach. W Angularze zbuduj komponent listy menu z możliwością dodania dania do (na razie tylko lokalnego, w serwisie) koszyka zamówienia.

Zadania do pracy własnej

  1. Dodaj do koszyka zamówienia przycisk usuwania pojedynczej pozycji i zmiany ilości (+/-), z automatyczną aktualizacją sumy.

  2. Zbuduj panel administratora (osobny komponent Angular) z pełnym CRUD-em dań — dodawanie nowego dania z uploadem zdjęcia (lekcja 18), edycja ceny/opisu, usuwanie — połączony z odpowiednimi akcjami w kontrolerze DaniaController.

  3. Dodaj widok "Panel kuchni" pokazujący WSZYSTKIE złożone zamówienia posortowane od najstarszego, z przyciskami zmiany statusu ("Przyjęte" → "W przygotowaniu" → "Gotowe") — rozbuduj o automatyczne odświeżanie listy co kilka sekund (np. przez setInterval wywołujące ponowne pobranie danych z API), symulując "żywy" podgląd dla kuchni bez ręcznego odświeżania strony.

Typowe błędy

Zapomnienie o ThenInclude przy pobieraniu zamówień z zagnieżdżonymi relacjami — prowadzi do pustych/brakujących danych o daniach wewnątrz pozycji zamówienia, mimo poprawnego pierwszego Include.

Przechowywanie CAŁEGO stanu składanego zamówienia w komponencie zamiast w serwisie — utrudnia to dostęp do tych danych z INNYCH komponentów (np. licznika w nagłówku, osobnego komponentu koszyka), łamiąc zasadę współdzielenia stanu przez serwisy z lekcji 9.

Brak sprawdzenia po stronie backendu, czy przesłane DanieId faktycznie istnieje w bazie przed zapisaniem zamówienia — może prowadzić do zapisania "osieroconej" pozycji zamówienia wskazującej na nieistniejące danie.

Nawiązanie do egzaminu zawodowego

To czwarty, najbardziej rozbudowany dotąd z ośmiu projektów — syntetyzuje relacje EF Core (lekcja 17), upload plików (lekcja 18), architekturę komponentową Angulara (lekcje 9-11) i komunikację z API (lekcja 12) w jeden, realistyczny system klasy biznesowej. Kolejny projekt (lekcja 24) przechodzi do zupełnie innej domeny — portalu społecznościowego.