Lekcja 23. Projekt 4: System zamówień restauracji (Angular + EF Core, relacje)
Trudny / egzaminacyjnyPo 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
KategoriaaDanie(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
ZamowienieaPozycjaZamowienia(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
Dodaj do koszyka zamówienia przycisk usuwania pojedynczej pozycji i zmiany ilości (+/-), z automatyczną aktualizacją sumy.
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.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
setIntervalwywoł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.