Dataniedziela, 9 sierpnia 2026 Czas07:14:19
← Aplikacje webowe

Lekcja 20. Projekt 1: Kalkulator BMI z historią pomiarów (Angular + ASP.NET Core)

Trudny / egzaminacyjny

Po 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

  1. Dodaj kolorowanie wiersza tabeli historii w zależności od kategorii (np. czerwone tło dla "otyłość"), korzystając z dyrektywy [ngClass] Angulara.

  2. Dodaj przycisk usuwania pojedynczego pomiaru z historii — akcję [HttpDelete("{id}")] w kontrolerze i odpowiednią metodę w BmiService, z potwierdzeniem przed usunięciem po stronie Angulara.

  3. 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.