# Analiza systemu targetów / progów oceny metryk biznesowych

**Data:** 2026-04-17  
**Projekty:** `api`, `admin`, `consultant`, `panel`  
**Dotyczy:** `panel/views/analysis/revenue_dashboard.twig`

---

## 1. Analiza modelu i podziału odpowiedzialności

### Gdzie należy logika?

| Warstwa | Odpowiedzialność |
|---|---|
| `api` | Persystencja targetów, endpointy CRUD, logika priorytetu (admin > konsultant), wyliczanie poziomu oceny dla danej wartości |
| `admin` | Widok definiowania targetów przez administratora |
| `consultant` | Widok definiowania targetów przez konsultanta |
| `panel` | Pobieranie i wizualizacja targetów w `revenue_dashboard.twig` |

### Kluczowa obserwacja — hardcoded progi w dashboardzie

W `revenue_dashboard.twig` (linie 349–356) istnieją już statyczne progi:
```js
foodThreshold:   40,   // Food cost ≤ 40% = OK
labourThreshold: 20,   // Labour cost ≤ 20% = OK
marginMin: 0.05,
marginTarget: 0.15,
marginMax: 0.20,
```
Oraz w `costInline()` (linia 588) jest logika: `warn = over && pct <= threshold + 4` — czyli nieformalne 4-poziomowe kolorowanie już istnieje. System targetów formalizuje i dynamizuje ten mechanizm.

---

## 2. Sprzeczności, luki i ryzyka

### 2.1 Jak definiować progi dla 4 poziomów?

**Problem:** 4 poziomy wymagają tylko **3 wartości granicznych**, nie 4. Np.:
- dobry: < 30%
- średni: 30%–40%
- średnio zły: 40%–50%
- zły: ≥ 50%

Oznacza to, że w bazie przechowujemy **3 progi** (`threshold_1`, `threshold_2`, `threshold_3`), a nie wartości dla każdego poziomu oddzielnie.

**Rekomendacja:** Trzy progi graniczne + kierunek oceny. Algorytm klasyfikacji jest po stronie `api`.

### 2.2 Kierunek oceny — globalny per metryka czy per target?

**Ryzyko:** Jeśli kierunek jest ustawiany per rekord targetu (przez admina lub konsultanta niezależnie), mogą powstać sprzeczne definicje tej samej metryki dla tego samego sklepu i miesiąca.

**Rekomendacja:** Kierunek oceny (`lower_is_better`) powinien być **globalnie przypisany do metryki** (w konfiguracji systemowej lub enum), a nie ustawiany przez użytkownika w formularzu. Koszt pracownika zawsze będzie "mniej = lepiej". Zmiana tego przez użytkownika byłaby błędem semantycznym.

Wyjątek: jeśli w przyszłości pojawią się metryki o nieoczywistym kierunku, można go zdefiniować w konfiguracji PHP, nie w DB.

### 2.3 Brak targetu — co wyświetlić?

**Luka:** Wymagania nie definiują zachowania gdy brak targetu admina I brak targetu konsultanta dla danego sklepu i miesiąca.

**Rekomendacja:** Trzy strategie — wybrać jedną:
- **A) Puste / neutralne** — brak kolorowania, nie wyświetlaj badge'a oceny (zalecane na start)
- **B) Fallback na poprzedni miesiąc** — ryzykowne (ukrywa brak danych)
- **C) Domyślne globalne wartości** — stałe wbudowane w kod jako ostateczny fallback

Zalecam **A** dla iteracji pierwszej, z opcją dodania **C** w kolejnej iteracji.

### 2.4 Granularność zakresu: miesiąc czy okres?

**Problem:** Dashboard filtruje dane po roku lub dowolnym zakresie (`filter: 'current_year'`). Jeśli target jest per miesiąc, to przy widoku rocznym należy zdecydować: czy wyświetlać target dla każdego miesiąca osobno, czy zagregować (np. średnia roczna)?

**Rekomendacja:** Target jest per miesiąc. W widoku rocznym: dla KPI zbiorczych pokazuj target za aktualny miesiąc lub ostatni dostępny. W tabeli miesięcznej — porównuj per wiersz.

### 2.5 Kopiowanie a zmiana targetu w trakcie miesiąca

**Ryzyko:** Użytkownik kopiuje target ze stycznia do lutego, następnie edytuje go. Czy historia zmian jest potrzebna?

**Rekomendacja:** Na etapie MVP — brak historii. Tabela przechowuje jeden rekord per (metryka, sklep, miesiąc, autor). Edycja nadpisuje istniejący rekord (upsert).

### 2.6 Czy "ogólna marża sklepu" to marża netto czy brutto?

**Luka:** W dashboardzie istnieje `netMargin = totalNet / totalRevenue`. Należy doprecyzować, czy target marży dotyczy tej samej wartości.

**Rekomendacja:** Przyjąć `net_margin` jako definicję marży — spójnie z istniejącym kodem dashboardu.

### 2.7 Klienci B2B per sklep

**Luka:** Nie wiadomo czy ta metryka jest już dostępna w `revenue_dashboard`. Weryfikacja na etapie implementacji.

---

## 3. Propozycja modelu danych

### Tabela: `shop_metric_targets`

```sql
CREATE TABLE shop_metric_targets (
    id              INT UNSIGNED     NOT NULL AUTO_INCREMENT,
    id_shop         INT UNSIGNED     NOT NULL,
    metric_key      VARCHAR(50)      NOT NULL,
    year            SMALLINT UNSIGNED NOT NULL,
    month           TINYINT UNSIGNED NOT NULL,   -- 1–12
    author_type     ENUM('admin','consultant') NOT NULL,
    author_id       INT UNSIGNED     NOT NULL,
    threshold_1     DECIMAL(12,4)   NOT NULL,   -- granica dobry/średni
    threshold_2     DECIMAL(12,4)   NOT NULL,   -- granica średni/średnio zły
    threshold_3     DECIMAL(12,4)   NOT NULL,   -- granica średnio zły/zły
    created_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY (id),

    -- Jeden rekord per metryka / sklep / miesiąc / autor
    UNIQUE KEY uq_target (id_shop, metric_key, year, month, author_type),

    KEY idx_shop_period (id_shop, year, month),
    KEY idx_author (author_type, author_id)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

### Konfiguracja metryk (PHP enum lub stała tablica w `api`)

```php
// api/app/Support/MetricDefinitions.php

const METRICS = [
    'employee_cost_pct'   => ['label' => 'Koszt pracownika',           'unit' => 'pct',    'lower_is_better' => true],
    'shop_cost_pct'       => ['label' => 'Koszt sklepu',               'unit' => 'pct',    'lower_is_better' => true],
    'material_cost_pct'   => ['label' => 'Koszt surowca',              'unit' => 'pct',    'lower_is_better' => true],
    'revenue'             => ['label' => 'Przychód',                   'unit' => 'amount', 'lower_is_better' => false],
    'transactions_count'  => ['label' => 'Liczba klientów',            'unit' => 'count',  'lower_is_better' => false],
    'avg_ticket'          => ['label' => 'Średni koszyk',              'unit' => 'amount', 'lower_is_better' => false],
    'b2b_clients_count'   => ['label' => 'Liczba klientów B2B',        'unit' => 'count',  'lower_is_better' => false],
    'net_margin_pct'      => ['label' => 'Ogólna marża sklepu',        'unit' => 'pct',    'lower_is_better' => false],
];
```

**Uzasadnienie:**
- Kierunek oceny (`lower_is_better`) jest stały per metryka — nie jest konfigurowalny przez użytkownika (eliminuje ryzyko 2.2)
- `unit` służy do formatowania wartości progów w UI
- `threshold_1` < `threshold_2` < `threshold_3` — walidacja po stronie `api`
- `DECIMAL(12,4)` pokrywa zarówno procenty (np. `30.5000`) jak i kwoty (np. `150000.0000`) i ilości

### Algorytm klasyfikacji (po stronie `api`)

```
jeśli lower_is_better:
    wartość < threshold_1 → dobry (zielony)
    wartość < threshold_2 → średni (żółty)
    wartość < threshold_3 → średnio zły (pomarańczowy)
    wartość ≥ threshold_3 → zły (czerwony)

jeśli !lower_is_better:
    wartość ≥ threshold_1 → dobry (zielony)
    wartość ≥ threshold_2 → średni (żółty)   (threshold_1 > threshold_2 > threshold_3)
    wartość ≥ threshold_3 → średnio zły (pomarańczowy)
    wartość < threshold_3 → zły (czerwony)
```

Uwaga: dla metryk `higher_is_better` progi są malejące (threshold_1 > threshold_2 > threshold_3). Walidacja API musi sprawdzać poprawność kolejności progów względem kierunku metryki.

---

## 4. Propozycja UX — widok definiowania targetów (`admin` i `consultant`)

### Struktura widoku

```
[Nagłówek: "Targety sklepu"]

[Filtr: Sklep ▼]  [Rok ▼]  [Miesiąc ▼]  [Pobierz]

[Przycisk: "Kopiuj z poprzedniego miesiąca"]

─────────────────────────────────────────────────────────────────
SEKCJA: Targety kosztowe (wartości w %)
─────────────────────────────────────────────────────────────────

Metryka              | Dobry      | Średni     | Śr. zły    | Zły
                     | (< próg 1) | (< próg 2) | (< próg 3) | (≥ próg 3)
---------------------|------------|------------|------------|----------
Koszt pracownika     | [_____%]   | [_____%]   | [_____%]   | (auto)
Koszt sklepu         | [_____%]   | [_____%]   | [_____%]   | (auto)
Koszt surowca        | [_____%]   | [_____%]   | [_____%]   | (auto)

─────────────────────────────────────────────────────────────────
SEKCJA: Targety biznesowe
─────────────────────────────────────────────────────────────────

Metryka              | Dobry        | Średni       | Śr. zły      | Zły
---------------------|--------------|--------------|--------------|------
Przychód (zł)        | [________]   | [________]   | [________]   | (auto)
Liczba klientów      | [________]   | [________]   | [________]   | (auto)
Średni koszyk (zł)   | [________]   | [________]   | [________]   | (auto)
Klienci B2B          | [________]   | [________]   | [________]   | (auto)
Marża netto (%)      | [_____%]     | [_____%]     | [_____%]     | (auto)

─────────────────────────────────────────────────────────────────
[Zapisz targety]
```

### Uwagi UX:
- **Kolory nagłówków kolumn:** zielony / żółty / pomarańczowy / czerwony — użytkownik natychmiast rozumie mapowanie
- **Pole "Zły" jest read-only** — wyliczane automatycznie (> próg 3), nie wymaga inputa
- **Walidacja inline:** jeśli próg 2 < próg 1 — podświetlenie błędu i blokada zapisu
- **Tooltip przy kierunku oceny:** "(niższy koszt = lepiej)" lub "(wyższy przychód = lepiej)" — informacja readonly, nie edytowalna
- **Kopiowanie z poprzedniego miesiąca:** przycisk u góry, po kliknięciu — modal potwierdzenia z informacją "Zostaną załadowane Twoje targety z [miesiąc poprzedni]. Czy chcesz kontynuować?"
- **Stan pusty:** jeśli brak targetów na dany miesiąc — formularz pokazuje puste pola, nie blokuje widoku

### Różnice między `admin` a `consultant`

Widok jest identyczny, ale:
- W `admin` — widoczna jest tylko sekcja "Moje targety (Admin)"
- W `consultant` — widoczna jest tylko sekcja "Moje targety (Konsultant)"
- Żaden z użytkowników nie widzi targetów drugiej strony w widoku edycji

---

## 5. Rekomendacje prezentacji targetów w `panel`

### Punkt integracji

Główny punkt integracji to `revenue_dashboard.twig`. Aktualnie hardcoded progi w obiekcie `RD` (linie 349–356) powinny zostać zastąpione przez dane pobierane z API.

### Strategia ładowania

Targety ładujemy razem z innymi danymi przy inicjalizacji dashboardu — oddzielne wywołanie AJAX do endpointu:
```
GET /api/v1/panel/shops/{id_shop}/targets?year={year}&month={month}
```

Odpowiedź:
```json
{
  "employee_cost_pct": {
    "admin": { "t1": 25, "t2": 35, "t3": 45 },
    "consultant": { "t1": 30, "t2": 40, "t3": 50 },
    "active": "admin"
  },
  "material_cost_pct": { ... },
  ...
}
```

### Wyróżnienie w UI

W tabeli miesięcznej (funkcja `costInline` w dashboardzie) — zamiast stałego `threshold` przekazujemy dynamiczne progi z załadowanego obiektu targets.

**Prezentacja dwóch targetów jednocześnie:**

```
Koszt pracownika: 32,4%
┌─────────────────────────────────┐
│ Target Admin:     <25% 🟢       │  ← pogrubiony, zielona ramka
│ Target Konsultant: <30% 🟡      │  ← szary, pomocniczy
└─────────────────────────────────┘
```

Lub w formie tooltip po najechaniu na badge oceny:
- badge pokazuje ocenę wg aktywnego targetu (admin ma priorytet)
- tooltip ujawnia szczegóły: "Admin: dobry (<25%) | Konsultant: średni (<30%)"

### Jeśli tylko jeden target

- Wyświetlaj normalnie badge oceny bez dodatkowego oznaczenia autora
- Drobny podpis: "wg Admin" lub "wg Konsultant" — subtelny, nie przeszkadzający

### Jeśli brak targetów

- Brak badge'a oceny
- Wartości kosztowe wyświetlane bez kolorowania (neutralne)
- Opcjonalnie: ikona ⚪ z tooltip "Brak targetów dla tego okresu"

---

## 6. Rekomendacja zakresu iteracji pierwszej

### Co wdrożyć w iteracji 1:

**`api`:**
1. Tabela `shop_metric_targets` (migracja SQL)
2. Klasa `MetricDefinitions` — predefiniowana lista metryk z kierunkiem i jednostką
3. Endpoint: `GET /panel/shops/{id}/targets?year=&month=` — zwraca aktywne targety z priorytetem admin > konsultant
4. Endpoint: `PUT /admin/shops/{id}/targets` — zapis targetów admina
5. Endpoint: `PUT /consultant/shops/{id}/targets` — zapis targetów konsultanta
6. Endpoint: `POST /admin/shops/{id}/targets/copy-from-previous` — kopiowanie
7. Endpoint: `POST /consultant/shops/{id}/targets/copy-from-previous` — kopiowanie

**`admin`:**
8. Widok definiowania targetów (formularz per sklep / miesiąc) — tylko metryki kosztowe (3 metryki)

**`consultant`:**
9. Analogiczny widok dla konsultanta

**`panel`:**
10. Zastąpienie hardcoded `foodThreshold` i `labourThreshold` dynamicznymi targetami pobieranymi z API
11. Wizualizacja — badge oceny w tabeli miesięcznej dla 3 kosztowych metryk

### Co odłożyć na iterację 2+:

- Targety biznesowe (przychód, klienci, średni koszyk, B2B, marża) — wymagają weryfikacji dostępności danych B2B w dashboardzie
- Wyróżnienie dwóch targetów jednocześnie w UI — skomplikowane, ale nie krytyczne na start
- Fallback globalny (domyślne wartości gdy brak targetów)
- Historia zmian targetów
- Widok porównawczy target vs. realizacja w kontekście rocznym

### Uzasadnienie priorytetyzacji

3 metryki kosztowe (`employee_cost_pct`, `shop_cost_pct`, `material_cost_pct`) mają:
- Gotową prezentację w dashboardzie (`costInline`, `statusBadge`)
- Hardcoded progi do zastąpienia
- Jasny kierunek oceny (lower_is_better)
- Natychmiastową wartość biznesową

Wdrożenie ich w iteracji 1 pozwala dostarczyć wartość przy minimalnym ryzyku.

---

## Podsumowanie decyzji projektowych

| Kwestia | Decyzja |
|---|---|
| Liczba progów | 3 wartości graniczne (threshold_1/2/3), nie 4 pola |
| Kierunek oceny | Globalny per metryka w konfiguracji PHP — nie edytowalny przez użytkownika |
| Brak targetu | Neutralny brak kolorowania (iteracja 1) |
| Historia zmian | Brak w MVP — upsert nadpisuje |
| Algorytm priorytetu | Po stronie `api` — `panel` nie implementuje logiki |
| Prezentacja dwóch targetów | Tooltip / etykieta w iteracji 2 |
| Pierwsza iteracja | Tylko 3 metryki kosztowe |

