Lekcja 20. Projekt 1: Kalkulator BMI z historią pomiarów (Angular + ASP.NET Core)
Trudny / egzaminacyjnyPo co się tego uczymy?
Czas na pierwszy z czterech pełnych projektów łączących wszystko, czego się nauczyłeś. Pamiętasz kalkulator BMI z lekcji 4? Tam był to prosty, samodzielny komponent Angular liczący wynik lokalnie. Teraz rozbudujemy go o prawdziwy backend ASP.NET Core — każdy pomiar będzie zapisywany w bazie danych, więc historia przetrwa zamknięcie przeglądarki.
Teoria
Wymagania projektu:
- Formularz Angular (Reactive Forms, lekcja 10) z polami wzrost/waga i walidacją (obie wartości muszą być dodatnie);
- Po obliczeniu BMI, wynik jest wysyłany do backendu ASP.NET Core i zapisywany w bazie SQLite (Entity Framework Core, lekcje 14-16);
- Lista poniżej formularza pokazuje HISTORIĘ wszystkich dotychczasowych pomiarów (data, wzrost, waga, wynik, kategoria), pobieraną z backendu przez
HttpClient(lekcja 11); - Stylowanie całości Bootstrapem (lekcja 7).
Model danych (backend, C#):
public class PomiarBmi
{
public int Id { get; set; }
public double WzrostCm { get; set; }
public double WagaKg { get; set; }
public double Wynik { get; set; }
public string Kategoria { get; set; } = "";
public DateTime DataPomiaru { get; set; } = DateTime.Now;
}
Przepływ danych całego projektu: użytkownik wypełnia formularz Angular → walidacja Reactive Forms (lekcja 11) → kliknięcie "Oblicz i zapisz" → Angular oblicza BMI LOKALNIE (żeby natychmiast pokazać wynik, bez czekania na serwer) I RÓWNOCZEŚNIE wysyła dane przez HttpClient.post() do kontrolera PomiaryController → backend oblicza kategorię, zapisuje przez EF Core, zwraca zapisany rekord (z nadanym Id) → Angular dodaje nowy wpis do lokalnie wyświetlanej listy historii, bez przeładowania całej strony.
Ten projekt ćwiczy WSZYSTKIE elementy z bloku Angular (formularz z walidacją, serwis, HttpClient) połączone z WSZYSTKIMI elementami backendu (kontroler API, EF Core, model danych) — to twoja pierwsza PEŁNA, kompletna aplikacja pełnego stosu (full-stack) w tym kursie.
Schemat
Angular: formularz-bmi.component
[wzrost, waga] → Reactive Forms + Validators (lekcja 10)
│ klik "Oblicz i zapisz"
▼
obliczBmiLokalnie() → natychmiastowy wynik na ekranie
│
▼
this.http.post<PomiarBmi>(".../api/pomiary", {wzrostCm, wagaKg})
│
▼ (CORS, lekcja 11)
ASP.NET Core: PomiaryController
[HttpPost] Dodaj(PomiarBmi pomiar)
│
├── oblicz Kategoria (serwerowo, "źródło prawdy")
├── context.Pomiary.Add(pomiar); SaveChangesAsync(); (EF Core, lekcja 14-15)
│
▼
return CreatedAtAction(..., pomiar); → JSON z powrotem do Angular
│
▼
Angular: dopisuje nowy wpis do tablicy historiaPomiarow (bez przeładowania)
│
▼
@for (p of historiaPomiarow) { <tr>...</tr> } ← lista historii (lekcja 8)
Przykład z życia
Aplikacja fitness śledząca postępy użytkownika (waga, BMI w czasie) zapisuje KAŻDY pomiar z datą, żeby po miesiącach móc pokazać wykres zmian — dokładnie ten sam mechanizm "oblicz i zapisz z historią" realizuje ten projekt, tylko w uproszczonej, edukacyjnej wersji.
bmi.service.ts
// bmi.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface PomiarBmi {
id?: number;
wzrostCm: number;
wagaKg: number;
wynik?: number;
kategoria?: string;
dataPomiaru?: string;
}
@Injectable({ providedIn: 'root' })
export class BmiService {
private adresApi = 'https://localhost:7042/api/pomiary';
constructor(private http: HttpClient) {}
pobierzHistorie(): Observable<PomiarBmi[]> {
return this.http.get<PomiarBmi[]>(this.adresApi);
}
zapisz(pomiar: PomiarBmi): Observable<PomiarBmi> {
return this.http.post<PomiarBmi>(this.adresApi, pomiar);
}
}
Controllers/PomiaryController.cs
// Controllers/PomiaryController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BmiApp.Server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class PomiaryController : ControllerBase
{
private readonly BmiContext _context;
public PomiaryController(BmiContext context) => _context = context;
[HttpGet]
public async Task<ActionResult<List<PomiarBmi>>> GetHistorie()
=> Ok(await _context.Pomiary.OrderByDescending(p => p.DataPomiaru).ToListAsync());
[HttpPost]
public async Task<ActionResult<PomiarBmi>> Dodaj(PomiarBmi pomiar)
{
if (pomiar.WzrostCm <= || pomiar.WagaKg <= )
{
return BadRequest("Wzrost i waga muszą być dodatnie.");
}
double wzrostM = pomiar.WzrostCm / 100.0;
pomiar.Wynik = pomiar.WagaKg / (wzrostM * wzrostM);
pomiar.Kategoria = pomiar.Wynik switch
{
< 18.5 => "niedowaga",
< 25.0 => "waga prawidłowa",
< 30.0 => "nadwaga",
_ => "otyłość"
};
pomiar.DataPomiaru = DateTime.Now;
_context.Pomiary.Add(pomiar);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetHistorie), pomiar);
}
}
public class PomiarBmi
{
public int Id { get; set; }
public double WzrostCm { get; set; }
public double WagaKg { get; set; }
public double Wynik { get; set; }
public string Kategoria { get; set; } = "";
public DateTime DataPomiaru { get; set; }
}
Komentarz i wyjaśnienie kodu
Zwróć uwagę na WAŻNĄ zasadę bezpieczeństwa: kategoria BMI (Kategoria) jest wyliczana PONOWNIE na BACKENDZIE, mimo że Angular też potrafi to policzyć lokalnie (dla natychmiastowego podglądu) — nigdy nie ufaj wyliczeniom przesłanym z przeglądarki jako "gotowym wynikom"; ktoś mógłby wysłać żądanie POST bezpośrednio (np. przez Postmana) z dowolnie spreparowanym wynikiem. Backend jest zawsze "źródłem prawdy" dla danych faktycznie zapisywanych.
pomiar.Id w interfejsie TypeScript PomiarBmi jest oznaczone jako opcjonalne (id?: number) — bo PRZED zapisaniem (wysłaniem POST) obiekt jeszcze nie ma Id (nadawane dopiero przez bazę danych po stronie backendu); Angular wysyła obiekt bez Id, a backend zwraca go już z nadanym numerem.
Ćwiczenie samodzielne
Zbuduj kompletny projekt: model PomiarBmi, BmiContext (EF Core, lekcja 15) z migracją, kontroler jak w przykładzie, oraz komponent Angular z formularzem i tabelą historii. Przetestuj: dodaj kilka pomiarów, sprawdź, że pojawiają się na liście od razu, i że po ODŚWIEŻENIU CAŁEJ STRONY historia nadal tam jest (dowód, że dane faktycznie trafiły do bazy, a nie tylko do pamięci przeglądarki).
Zadania do pracy własnej
Dodaj kolorowanie wiersza tabeli historii w zależności od kategorii (np. czerwone tło dla "otyłość"), korzystając z dyrektywy
[ngClass]Angulara.Dodaj przycisk usuwania pojedynczego pomiaru z historii — akcję
[HttpDelete("{id}")]w kontrolerze i odpowiednią metodę wBmiService, z potwierdzeniem przed usunięciem po stronie Angulara.Dodaj do formularza pole "Imię" (opcjonalne) i rozbuduj projekt o filtrowanie historii — pole wyszukiwania nad tabelą pozwalające pokazać tylko pomiary dla wpisanego imienia (filtrowanie może być wykonane PO STRONIE ANGULARA na już pobranych danych, przez metodę tablicy
filter, albo — trudniejsza wersja — jako parametr zapytania do backendu:GET /api/pomiary?imie=Jan).
Typowe błędy
Ufanie wynikowi BMI przesłanemu z przeglądarki bez ponownego przeliczenia na serwerze — poważna luka pozwalająca zapisać w bazie dowolne, sfałszowane dane.
Brak walidacji dodatnich wartości PO STRONIE BACKENDU (poleganie wyłącznie na walidacji Angulara) — walidację front-endową da się ominąć (np. przez bezpośrednie żądanie POST), więc backend musi sprawdzać dane NIEZALEŻNIE.
Zapominanie o odświeżeniu lokalnej listy historiaPomiarow w Angularze po udanym zapisie — bez tego nowy pomiar zniknąłby z widoku do czasu ręcznego odświeżenia całej strony przeglądarki.
Nawiązanie do egzaminu zawodowego
To pierwszy z ośmiu projektów realizujących INF.04.7.3 w pełni — łączy Angular (formularze, serwisy, HttpClient) z ASP.NET Core (Web API, EF Core) w jedną, kompletną, przetestowaną aplikację. Kolejny projekt (lekcja 21) jest lżejszy — gra w zgadywanie kolorów, skupiona głównie na logice front-endowej.