# PROMPT: Moduł Zarządzania Maksymalną Ceną Sprzedaży (Admin Panel)

## Kontekst projektu
Pracujesz w projekcie **admin panel** (monorepo PHP MVC z Twig). Twoim zadaniem jest zaimplementować moduł pozwalający **administratorowi** na:
- przeglądanie produktów dostępnych do sprzedaży globalnie (wszystkie produkty w systemie)
- ustalanie **maksymalnej ceny sprzedaży** dla każdego produktu
- przeglądanie marży (oczekiwanej vs rzeczywistej)
- filtrowanie i wyszukiwanie produktów
- monitoring produktów bez ustalonej maksymalnej ceny

---

## Cel funkcjonalny
- **Admin** widzi wszystkie produkty dostępne w systemie (globalne, nie per sklep).
- Może ustawić **maksymalną cenę sprzedaży** (`max_portion_price`) dla produktu.
- Ta cena będzie **limitem dla franczyzobiorców** — sklep może ustawić swoją cenę, ale nie wyższą niż maksymalna określona przez admina.
- UI pokazuje:
  - **Koszt produkcji** (`recipe_cost_gross`) — cena surowców/receptury
  - **Oczekiwaną marżę** (`expected_margin`) — docelowa marża dla produktu (%)
  - **Maksymalną cenę sprzedaży** (`max_portion_price`) — **edytowalna przez admina**
  - **Marżę na maksymalnej cenie** — obliczoną: `((max_portion_price - recipe_cost_gross) / max_portion_price) * 100`
  - Podpowiedź: sugerowaną cenę dla osiągnięcia oczekiwanej marży
  - Odchylenie marży od oczekiwanej (delta w punktach procentowych)

---

## Wzorzec — istniejący moduł z projektu franczyzowego (panel)

### Ścieżka pliku referencyjnego
`views/shops/pricing.twig` (załączony jako `pricing.twig`)

### Opis istniejącego modułu
W projekcie **panel** (dla franczyzobiorców) istnieje **IDENTYCZNY moduł** z już gotową strukturą UI/UX:

**Główne elementy:**
1. **Nagłówek strony** z ikoną i szczegółami sklepu (adres, telefon, godziny) — **POMIŃ w adminie**
2. **Summary bar** (pasek statystyk):
   - Liczba produktów
   - Produkty bez maksymalnej ceny (z ostrzeżeniem)
   - Średnia marża na cenie maksymalnej
3. **Karta główna** z:
   - Nagłówkiem i opisem
   - **Pasek filtrów** (sticky):
     - Search input (wyszukiwanie po nazwie produktu)
     - Select: filtr marży (wszystkie / bez ceny / niska / dobra / poniżej celu)
   - **Alert** dla produktów bez ceny (warunkowy)
4. **Tabela produktów**:
   - **Sticky header** z grupowaniem kolumn (3 grupy):
     - **Koszt** (2 kolumny): Koszt prod., VAT
     - **Marże** (3 kolumny): Oczek. marża, Cena sprzedaży, Marża sprzedaży — **TE DWA KOLUMNY POMIŃ W ADMINIE**
     - **Cena maks. sklepu** (2 kolumny): Maks. cena (edytowalna), Marża maks.
   - **Wiersze kategorii** (collapsible):
     - Nazwa kategorii + liczba produktów + ikona ostrzeżenia (jeśli są produkty bez ceny)
     - Kliknięcie → toggle show/hide produktów kategorii
   - **Wiersze produktów**:
     - Nazwa produktu (+ ikona wegetariańskie 🌿 jeśli dotyczy)
     - Koszt produkcji (number format)
     - VAT (badge)
     - **Oczekiwana marża** (badge niebieski z tooltipem)
     - **[POMIŃ W ADMINIE]** Obecna cena sprzedaży
     - **[POMIŃ W ADMINIE]** Marża od ceny sprzedaży (z deltą)
     - **Maksymalna cena** — edytowalna inline:
       - Input disabled + przycisk Edit (ołówek)
       - Po kliknięciu Edit → input enabled + Save (check) + Cancel (X)
       - Po zapisie → AJAX call → toast + aktualizacja UI
       - Podpowiedź pod inputem: "Dla X% marży: Y.YY" (wyliczona z `expected_margin`)
     - **Marża od maksymalnej ceny**:
       - Badge z kolorem (zielony ≥50%, żółty 20-49%, czerwony <20%, szary brak)
       - Delta (odchylenie od oczekiwanej w pp): `+Xpp` lub `−Xpp` (zielony/czerwony)
5. **Legenda** (na dole):
   - Objaśnienie kolorów badge marży
   - Objaśnienie delty

**Kluczowe różnice dla admina:**
- **BRAK nagłówka ze szczegółami sklepu** (admin nie wybiera sklepu)
- **USUŃ kolumny**: "Obecna cena sprzedaży" i "Marża sprzedaży" — to są dane specyficzne dla sklepu
- **Zmień tytuł sekcji**: zamiast "Cena maks. sklepu" → "Maksymalna cena sprzedaży"
- **Routing**: `/products/max-price-list` (nie `/shops/{id}/pricing`)
- **Endpoint AJAX**: `/ajax/products/{id}/max-price` (nie `/ajax/shops/{id}/products/{id}/price`)

---

## Wymagania techniczne

### Stack (identyczny jak w panel)
- **PHP 8+** (MVC)
- **Twig** — widoki
- **Bootstrap 5 / motyw Mazer** — UI (zachowaj style z referencji!)
- **Repository Pattern** — warstwa dostępu do API
- **Service Pattern** — logika biznesowa
- **ApiClient** — komunikacja z backendem
- **Tłumaczenia** — wszystkie teksty w plikach JSON (`public/resources/lang/page/{lang}/product.json`)
- **AJAX** — zapisywanie cen

### Routing
```
GET  /products/max-price-list          → PriceController@maxPriceList
POST /ajax/products/{id}/max-price     → PriceController@ajaxUpdateMaxPrice
```

### Kontroler
`controllers/Product/PriceController.php`

**Metoda `maxPriceList()`:**
```php
#[Route('GET', '/products/max-price-list')]
public function maxPriceList(): void
{
    // Pobierz wszystkie produkty (globalne, bez id_shop)
    $products = $this->productService->getAll();
    
    // Grupuj po kategoriach
    $categories = $this->groupProductsByCategory($products);
    
    $data = [
        'categories' => $categories,
    ];
    
    $this->view('product/price/max_price_list', $data);
}

private function groupProductsByCategory(array $products): array
{
    $grouped = [];
    foreach ($products as $product) {
        $catId = $product->getIdCategory();
        $catName = $product->getCategoryName();
        
        if (!isset($grouped[$catId])) {
            $grouped[$catId] = [
                'id' => $catId,
                'name' => $catName,
                'products' => [],
            ];
        }
        $grouped[$catId]['products'][] = $product;
    }
    return array_values($grouped);
}
```

**Metoda `ajaxUpdateMaxPrice(int $id)`:**
```php
#[Route('POST', '/ajax/products/{id}/max-price')]
public function ajaxUpdateMaxPrice(int $id): Response
{
    $request = Request::createFromGlobals();
    $data = $this->getJson($request);
    
    if (empty($data) || !isset($data['max_portion_price'])) {
        return $this->json(['success' => false, 'message' => 'Invalid data'], 400);
    }
    
    $resp = $this->productService->updateMaxPrice($id, $data);
    return $this->json($resp, $resp['code'] ?? 200);
}
```

### Serwis
`services/Product/ProductService.php`

```php
public function getAll(): array
{
    return $this->productRepository->getAll();
}

public function updateMaxPrice(int $idProduct, array $data)
{
    return $this->productRepository->updateMaxPrice($idProduct, $data);
}
```

### Repozytorium
`repositories/Product/ProductRepository.php`

```php
public function getAll(): array
{
    $response = $this->apiClient->get("/products");
    
    $products = [];
    if (isset($response['data'])) {
        foreach ($response['data'] as $data) {
            $products[] = new ProductModel($data);
        }
    }
    return $products;
}

public function updateMaxPrice(int $idProduct, array $data)
{
    return $this->apiClient->patch("/products/{$idProduct}/max-price", $data);
}
```

### Model
`models/ProductModel.php`

**Upewnij się, że model ma:**

```php
class ProductModel
{
    private $id;
    private $name;
    private $id_category;
    private $category_name;
    private $recipe_cost_gross;      // Koszt produkcji
    private $max_portion_price;      // Maksymalna cena (admin ustala)
    private $expected_margin;        // Oczekiwana marża (%)
    private $tax_val_perc;           // VAT (%)
    private $is_vegetarian;          // bool
    
    public function __construct(array $data)
    {
        $this->id = $data['id'] ?? null;
        $this->name = $data['name'] ?? null;
        $this->id_category = $data['id_category'] ?? null;
        $this->category_name = $data['category_name'] ?? null;
        $this->recipe_cost_gross = $data['recipe_cost_gross'] ?? 0;
        $this->max_portion_price = $data['max_portion_price'] ?? 0;
        $this->expected_margin = $data['expected_margin'] ?? 0;
        $this->tax_val_perc = $data['tax_val_perc'] ?? 0;
        $this->is_vegetarian = $data['is_vegetarian'] ?? false;
    }
    
    // Gettery...
    public function getId() { return $this->id; }
    public function getName() { return $this->name; }
    public function getIdCategory() { return $this->id_category; }
    public function getCategoryName() { return $this->category_name; }
    public function getRecipeCostGross() { return $this->recipe_cost_gross; }
    public function getMaxPortionPrice() { return $this->max_portion_price; }
    public function getExpectedMargin() { return $this->expected_margin; }
    public function getTaxValPerc() { return $this->tax_val_perc; }
    public function getIsVegetarian() { return $this->is_vegetarian; }
    
    /**
     * Oblicza marżę na maksymalnej cenie
     * Marża = ((max_portion_price - recipe_cost_gross) / max_portion_price) * 100
     */
    public function calcMargeOnMaxPrice(): ?float
    {
        if ($this->max_portion_price === null || $this->max_portion_price <= 0) {
            return null;
        }
        if ($this->recipe_cost_gross === null) {
            return null;
        }
        return (($this->max_portion_price - $this->recipe_cost_gross) / $this->max_portion_price) * 100;
    }
    
    /**
     * Oblicza sugerowaną cenę dla osiągnięcia oczekiwanej marży
     * target_price = recipe_cost_gross / (1 - expected_margin / 100)
     */
    public function getTargetPriceForExpectedMargin(): ?float
    {
        if ($this->expected_margin === null || $this->expected_margin <= 0) {
            return null;
        }
        if ($this->recipe_cost_gross === null || $this->recipe_cost_gross <= 0) {
            return null;
        }
        return $this->recipe_cost_gross / (1 - $this->expected_margin / 100);
    }
}
```

---

## Widok Twig

### Ścieżka
`views/product/price/max_price_list.twig`

### Struktura (oparta na `pricing.twig` z referencji)

**IDENTYCZNA struktura jak w załączonym `pricing.twig`, ALE:**
1. **USUŃ** nagłówek ze szczegółami sklepu (row z `shop-icon-wrap`, adresem, telefonem)
2. **ZMIEŃ** tytuł strony: "Zarządzanie maksymalnymi cenami produktów"
3. **W tabeli:**
   - **USUŃ kolumny**: "Obecna cena sprzedaży" (portion_price) i "Marża sp." (marge on portion_price)
   - **ZACHOWAJ kolumny**:
     - Nazwa produktu
     - Koszt produkcji (`recipe_cost_gross`)
     - VAT (`tax_val_perc`)
     - Oczekiwana marża (`expected_margin`) — badge niebieski
     - Maksymalna cena (`max_portion_price`) — edytowalna inline
     - Marża maks. (`calcMargeOnMaxPrice()`) — badge z kolorem + delta
4. **Zmień grupowanie kolumn** w sticky header:
   - Grupa 1: **Koszt** (2 kolumny: Koszt prod., VAT)
   - Grupa 2: **Oczekiwana marża** (1 kolumna)
   - Grupa 3: **Maksymalna cena** (2 kolumny: Cena, Marża)
5. **Zachowaj wszystkie style CSS** z referencji (sticky header, category rows, badges, deltas, itp.)
6. **Zachowaj JavaScript** (toggle kategorii, inline edit, filtry, statystyki) — tylko dostosuj endpointy:
   - Wywołanie AJAX: `/ajax/products/${id_product}/max-price` (usuń `id_shop`)
   - Parametry: `{ max_portion_price: newVal }` (bez `id_shop`, bez `id_product` w payload)

### Szablon Twig (skrócony — pełny wzorzec w `pricing.twig`)

```twig
{% extends "layouts/base.twig" %}

{% block title %}{{ translations.max_price_management|default('Zarządzanie maksymalnymi cenami') }}{% endblock %}

{% block head %}
    {# SKOPIUJ CAŁĄ SEKCJĘ STYLE Z pricing.twig #}
    <style>
        /* ── sticky header ── */
        thead.stickyHeader th { position: sticky; top: 0; z-index: 10; }
        /* ... reszta stylów z pricing.twig ... */
    </style>
{% endblock %}

{% block page_heading %}
    <div class="page-heading">
        <h3>{{ translations.max_price_management|default('Zarządzanie maksymalnymi cenami') }}</h3>
        <p class="text-subtitle text-muted">{{ translations.max_price_subtitle|default('Ustaw maksymalną cenę sprzedaży dla każdego produktu') }}</p>
    </div>
{% endblock %}

{% block content %}

{# ─── Pasek statystyk (ZMNIEJSZONY - bez szczegółów sklepu) ─── #}
<div class="row mb-4">
    <div class="col-12">
        <div class="summary-bar d-flex flex-wrap gap-4 align-items-center">
            <div>
                <div class="stat-label">{{ translations.products }}</div>
                <div class="stat-value" id="stat-total">—</div>
            </div>
            <div class="border-start border-white border-opacity-25 ps-4">
                <div class="stat-label">{{ translations.without_max_price }}</div>
                <div class="stat-value text-warning" id="stat-zero">—</div>
            </div>
            <div class="border-start border-white border-opacity-25 ps-4">
                <div class="stat-label">{{ translations.avg_margin_on_max_price|default('Śred. marża (cena maks.)') }}</div>
                <div class="stat-value" id="stat-avg-marge">—</div>
            </div>
        </div>
    </div>
</div>

{# ─── Karta główna ─── #}
<div class="card">
    <div class="card-header bg-white border-0 pb-0">
        <div class="row align-items-center g-2">
            <div class="col-md-6">
                <h4 class="card-title mb-0">{{ translations.product_list|default('Lista produktów') }}</h4>
                <small class="text-muted">{{ translations.pricing_subtitle_hint|default('Ustaw maksymalną cenę sprzedaży dla każdego produktu') }}</small>
            </div>
            <div class="col-md-3">
                <div class="input-group input-group-sm">
                    <span class="input-group-text"><i class="icon dripicons-search"></i></span>
                    <input id="pricing-search" type="search" class="form-control" placeholder="{{ translations.search|default('Szukaj produktu...') }}">
                    <span class="input-group-text" id="clear-search" style="cursor:pointer" title="{{ translations.clear|default('Wyczyść') }}">
                        <i class="icon dripicons-cross"></i>
                    </span>
                </div>
            </div>
            <div class="col-md-3">
                <select id="marge-filter" class="form-select form-select-sm">
                    <option value="all">{{ translations.all_products|default('Wszystkie') }}</option>
                    <option value="zero">{{ translations.without_max_price|default('Bez ceny maks.') }}</option>
                    <option value="below_target">{{ translations.below_target_margin|default('Marża poniżej celu') }}</option>
                    <option value="low">{{ translations.low_margin|default('Niska marża (< 20%)') }}</option>
                    <option value="ok">{{ translations.ok_margin|default('Dobra marża (≥ 20%)') }}</option>
                </select>
            </div>
        </div>
    </div>

    <div class="card-content">
        <div class="card-body p-0">

            <div id="zero-price-alert" class="alert alert-warning mx-3 mt-3 d-none">
                <i class="icon dripicons-warning me-2"></i>
                <span id="zero-price-msg"></span>
            </div>

            {% if categories is defined and categories|length > 0 %}
            <div class="table-responsive">
                <table class="table table-hover table-pricing mb-0">
                    <thead class="stickyHeader">
                        {# ── Wiersz 1: grupy kolumn ── #}
                        <tr class="header-groups">
                            <th style="width:30%" rowspan="2" class="align-bottom">{{ translations.product }}</th>
                            <th style="width:12%" colspan="2" class="text-center group-cost">{{ translations.cost_section|default('Koszt') }}</th>
                            <th style="width:10%" rowspan="2" class="text-center group-margin align-bottom">{{ translations.expected_margin_section|default('Oczek. marża') }}</th>
                            <th style="width:28%" colspan="2" class="text-center group-max">{{ translations.max_price_section|default('Maksymalna cena') }}</th>
                        </tr>
                        {# ── Wiersz 2: nazwy kolumn ── #}
                        <tr class="header-cols">
                            <th class="text-end col-cost"   style="width:10%">{{ translations.production_cost|default('Koszt prod.') }}</th>
                            <th class="text-center col-cost" style="width:8%">{{ translations.vat|default('VAT') }}</th>
                            <th class="text-end   col-max"  style="width:14%">{{ translations.max_sale_price }}</th>
                            <th class="text-center col-max"  style="width:10%">{{ translations.margin_on_max|default('Marża') }}</th>
                        </tr>
                    </thead>
                    <tbody id="pricing-tbody">

                    {% for category in categories %}
                        <tr class="category-header-row" data-cat-id="{{ category.id }}" onclick="toggleCategory('{{ category.id }}')">
                            <td colspan="6">
                                <span class="category-toggle-icon me-2" id="toggle-icon-{{ category.id }}">▼</span>
                                {{ category.name }}
                                <span class="badge bg-secondary ms-2 fw-normal">{{ category.products|length }}</span>
                                <span class="badge bg-danger ms-1 fw-normal d-none" id="cat-warn-{{ category.id }}">
                                    <i class="icon dripicons-warning"></i>
                                </span>
                            </td>
                        </tr>

                        {% for product in category.products %}
                        {%- set raw        = product.getRecipeCostGross()|default(0) -%}
                        {%- set maxP       = product.getMaxPortionPrice()|default(0) -%}
                        {%- set expMargin  = product.getExpectedMargin()|default(0) -%}
                        {%- set margeOnMax = product.calcMargeOnMaxPrice() -%}
                        {%- set targetPrice = product.getTargetPriceForExpectedMargin() -%}

                        <tr class="product-row cat-{{ category.id }}"
                            data-product-id="{{ product.getId() }}"
                            data-category-id="{{ category.id }}"
                            data-name="{{ product.getName()|lower }}"
                            data-max-price="{{ maxP }}"
                            data-raw="{{ raw }}"
                            data-exp-margin="{{ expMargin }}">

                            {# ── Nazwa ── #}
                            <td>
                                <span class="fw-medium">{{ product.getName() }}</span>
                                {% if product.getIsVegetarian() %}
                                    <span class="ms-1" title="{{ translations.vegetarian|default('Wegetariański') }}">🌿</span>
                                {% endif %}
                            </td>

                            {# ── Koszt produkcji ── #}
                            <td class="text-end col-cost">
                                {% if raw > 0 %}
                                    <span class="fw-medium">{{ raw|number_format(2, ',', ' ') }}</span>
                                {% else %}
                                    <span class="text-muted">—</span>
                                {% endif %}
                            </td>

                            {# ── VAT ── #}
                            <td class="text-center col-cost">
                                <span class="badge bg-light text-secondary border">{{ product.getTaxValPerc() }}%</span>
                            </td>

                            {# ── Oczekiwana marża ── #}
                            <td class="text-center col-margin">
                                {% if expMargin > 0 %}
                                    <span class="marge-badge marge-target"
                                          data-bs-toggle="tooltip"
                                          title="{{ translations.expected_margin_tooltip|default('Oczekiwana marża dla tego produktu') }}">
                                        {{ expMargin|number_format(0, ',', ' ') }}%
                                    </span>
                                {% else %}
                                    <span class="text-muted">—</span>
                                {% endif %}
                            </td>

                            {# ── Maks. cena sprzedaży (edytowalna) ── #}
                            <td class="col-max">
                                <div class="input-group input-group-sm price-input-group">
                                    <input type="number"
                                           class="form-control max-price-input"
                                           name="max_portion_price"
                                           value="{{ maxP }}"
                                           min="0" step="0.01"
                                           disabled>
                                    <button type="button" class="btn btn-outline-secondary btn-edit" onclick="enableEdit(this)" title="{{ translations.edit }}">
                                        <i class="icon dripicons-pencil"></i>
                                    </button>
                                    <button type="button" class="btn btn-success btn-save d-none" onclick="saveChanges({{ product.getId() }}, this)" title="{{ translations.save }}">
                                        <i class="icon dripicons-checkmark"></i>
                                    </button>
                                    <button type="button" class="btn btn-outline-danger btn-cancel d-none" onclick="cancelEdit(this)" title="{{ translations.cancel|default('Anuluj') }}">
                                        <i class="icon dripicons-cross"></i>
                                    </button>
                                </div>
                                {# Podpowiedź: cena wyliczona z oczekiwanej marży #}
                                {% if targetPrice is not null %}
                                <div class="price-hint ps-1">
                                    <span class="hint-target">
                                        <i class="icon dripicons-arrow-thin-right"></i>
                                        {{ translations.for_expected_margin|default('Dla') }} {{ expMargin|number_format(0) }}%: {{ targetPrice|number_format(2, ',', ' ') }}
                                    </span>
                                </div>
                                {% endif %}
                            </td>

                            {# ── Marża od ceny maks. ── #}
                            <td class="text-center col-max cell-marge">
                                {% if margeOnMax is not null %}
                                    {%- set margeRounded = margeOnMax|round(0) -%}
                                    {%- set cls = margeRounded >= 50 ? 'marge-good' : (margeRounded >= 20 ? 'marge-ok' : 'marge-bad') -%}
                                    <span class="marge-badge {{ cls }}">{{ margeRounded }}%</span>
                                    {# delta względem oczekiwanej #}
                                    {% if expMargin > 0 %}
                                        {%- set delta = margeRounded - expMargin|round(0) -%}
                                        <div class="{{ delta >= 0 ? 'delta-positive' : 'delta-negative' }}">
                                            {{ delta >= 0 ? '+' : '' }}{{ delta }}pp
                                        </div>
                                    {% endif %}
                                {% else %}
                                    <span class="marge-badge marge-zero">—</span>
                                {% endif %}
                            </td>

                        </tr>
                        {% endfor %}
                    {% endfor %}

                    </tbody>
                </table>
            </div>

            <div id="empty-filter-state" class="text-center py-5 d-none">
                <i class="icon dripicons-search" style="font-size:2rem;color:#cbd5e1"></i>
                <p class="text-muted mt-2">{{ translations.no_results|default('Brak wyników dla podanych filtrów') }}</p>
                <button class="btn btn-light btn-sm mt-1" onclick="resetFilters()">{{ translations.reset_filters|default('Resetuj filtry') }}</button>
            </div>

            {% else %}
            <div class="text-center py-5">
                <i class="icon dripicons-box" style="font-size:3rem;color:#cbd5e1"></i>
                <h5 class="mt-3 text-muted">{{ translations.no_products }}</h5>
            </div>
            {% endif %}

        </div>
    </div>
</div>

{# ── Legenda ── #}
<div class="row mt-3">
    <div class="col-12">
        <div class="d-flex flex-wrap gap-3 align-items-center small text-muted px-1">
            <span class="fw-semibold">{{ translations.legend|default('Legenda:') }}</span>
            <span><span class="marge-badge marge-good me-1">≥50%</span>{{ translations.legend_good|default('Wysoka marża') }}</span>
            <span><span class="marge-badge marge-ok   me-1">20-49%</span>{{ translations.legend_ok|default('Dobra marża') }}</span>
            <span><span class="marge-badge marge-bad  me-1">&lt;20%</span>{{ translations.legend_bad|default('Niska marża') }}</span>
            <span><span class="marge-badge marge-target me-1">—%</span>{{ translations.legend_target|default('Oczekiwana marża') }}</span>
            <span class="delta-positive">+Xpp</span> / <span class="delta-negative">−Xpp</span>
            <span>{{ translations.legend_delta|default('odchylenie od oczekiwanej marży') }}</span>
        </div>
    </div>
</div>

{% endblock %}

{% block scripts %}
<script>
// SKOPIUJ CAŁY JAVASCRIPT Z pricing.twig, ALE ZMIEŃ:
// 1. W funkcji saveChanges() usuń parametr id_shop:
//    function saveChanges(id_product, btn) {
//        const group = btn.closest('.input-group');
//        const input = group.querySelector('input');
//        const newVal = parseFloat(input.value);
//
//        btn.disabled = true;
//        btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
//
//        api(`/ajax/products/${id_product}/max-price`, {
//            method: "PATCH",
//            data: { max_portion_price: newVal }  // bez id_shop, bez id_product
//        })
//        .then(function (res) {
//            showSnack(res.success, `{{ translations.save_was_successful|default('Zapis powiódł się') }}`);
//            btn.disabled = false;
//            btn.innerHTML = '<i class="icon dripicons-checkmark"></i>';
//            if (res.success) {
//                disableEdit(btn);
//                updateRowAfterSave(btn, newVal);
//                updateStats();
//            }
//        })
//        .catch(function () {
//            showSnack(false, `{{ translations.something_went_wrong }}`);
//            btn.disabled = false;
//            btn.innerHTML = '<i class="icon dripicons-checkmark"></i>';
//        });
//    }
//
// 2. Reszta JS bez zmian (toggleCategory, enableEdit, cancelEdit, disableEdit, updateRowAfterSave, updateStats, applyFilters, resetFilters)

document.addEventListener("DOMContentLoaded", function () {
    updateStats();
    document.getElementById('pricing-search').addEventListener('input', applyFilters);
    document.getElementById('marge-filter').addEventListener('change', applyFilters);
    document.getElementById('clear-search').addEventListener('click', function () {
        document.getElementById('pricing-search').value = '';
        applyFilters();
    });
    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el));
});

function toggleCategory(catId) {
    const rows = document.querySelectorAll(`.cat-${catId}`);
    const icon = document.getElementById(`toggle-icon-${catId}`);
    const isVisible = rows.length > 0 && !rows[0].classList.contains('d-none');
    rows.forEach(r => r.classList.toggle('d-none', isVisible));
    if (icon) icon.classList.toggle('collapsed', isVisible);
}

function enableEdit(btn) {
    const group = btn.closest('.input-group');
    const input = group.querySelector('input');
    input._originalValue = input.value;
    input.disabled = false;
    input.focus();
    if (input.select) input.select();
    group.querySelector('.btn-edit').classList.add('d-none');
    group.querySelector('.btn-save').classList.remove('d-none');
    group.querySelector('.btn-cancel').classList.remove('d-none');
}

function cancelEdit(btn) {
    const group = btn.closest('.input-group');
    const input = group.querySelector('input');
    input.value = input._originalValue ?? input.value;
    input.disabled = true;
    group.querySelector('.btn-edit').classList.remove('d-none');
    group.querySelector('.btn-save').classList.add('d-none');
    group.querySelector('.btn-cancel').classList.add('d-none');
}

function disableEdit(btn) {
    const group = btn.closest('.input-group');
    const input = group.querySelector('input');
    input.disabled = true;
    group.querySelector('.btn-edit').classList.remove('d-none');
    group.querySelector('.btn-save').classList.add('d-none');
    group.querySelector('.btn-cancel').classList.add('d-none');
}

function saveChanges(id_product, btn) {
    const group = btn.closest('.input-group');
    const input = group.querySelector('input');
    const newVal = parseFloat(input.value);

    btn.disabled = true;
    btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';

    api(`/ajax/products/${id_product}/max-price`, {
        method: "PATCH",
        data: { max_portion_price: newVal }
    })
    .then(function (res) {
        showSnack(res.success, `{{ translations.save_was_successful|default('Zapis powiódł się') }}`);
        btn.disabled = false;
        btn.innerHTML = '<i class="icon dripicons-checkmark"></i>';
        if (res.success) {
            disableEdit(btn);
            updateRowAfterSave(btn, newVal);
            updateStats();
        }
    })
    .catch(function () {
        showSnack(false, `{{ translations.something_went_wrong }}`);
        btn.disabled = false;
        btn.innerHTML = '<i class="icon dripicons-checkmark"></i>';
    });
}

function updateRowAfterSave(btn, newMaxPrice) {
    const row = btn.closest('tr');
    if (!row) return;
    row.dataset.maxPrice = newMaxPrice;

    const raw       = parseFloat(row.dataset.raw) || 0;
    const expMargin = parseFloat(row.dataset.expMargin) || 0;
    const margeCell = row.querySelector('.cell-marge');
    if (!margeCell) return;

    let badgeClass = 'marge-zero';
    let label = '—';
    let deltaHtml = '';

    if (newMaxPrice > 0) {
        const marge = Math.round((newMaxPrice - raw) / newMaxPrice * 100);
        label = marge + '%';
        badgeClass = marge >= 50 ? 'marge-good' : (marge >= 20 ? 'marge-ok' : 'marge-bad');
        if (expMargin > 0) {
            const delta = marge - Math.round(expMargin);
            const deltaClass = delta >= 0 ? 'delta-positive' : 'delta-negative';
            deltaHtml = `<div class="${deltaClass}">${delta >= 0 ? '+' : ''}${delta}pp</div>`;
        }
    }

    margeCell.innerHTML = `<span class="marge-badge ${badgeClass}">${label}</span>${deltaHtml}`;
    row.classList.toggle('highlight-zero', newMaxPrice === 0);
}

function updateStats() {
    let total = 0, zeroCount = 0, margeSum = 0, margeCount = 0;

    document.querySelectorAll('.product-row').forEach(row => {
        total++;
        const maxP = parseFloat(row.dataset.maxPrice) || 0;
        const raw  = parseFloat(row.dataset.raw) || 0;

        if (maxP === 0) {
            zeroCount++;
            row.classList.add('highlight-zero');
        } else {
            row.classList.remove('highlight-zero');
            margeSum += (maxP - raw) / maxP * 100;
            margeCount++;
        }
    });

    document.getElementById('stat-total').textContent = total;
    document.getElementById('stat-zero').textContent  = zeroCount;
    document.getElementById('stat-avg-marge').textContent = margeCount > 0
        ? Math.round(margeSum / margeCount) + '%' : '—';

    const alertBox = document.getElementById('zero-price-alert');
    const alertMsg = document.getElementById('zero-price-msg');
    if (zeroCount > 0) {
        alertMsg.textContent = `{{ translations.number_of_products_without_fixed_maximum_price|default('Produkty bez ustalonej ceny maks.') }}: ${zeroCount}`;
        alertBox.classList.remove('d-none');
    } else {
        alertBox.classList.add('d-none');
    }
    updateCategoryWarnings();
}

function updateCategoryWarnings() {
    document.querySelectorAll('[data-cat-id]').forEach(headerRow => {
        const catId = headerRow.dataset.catId;
        const zeroInCat = Array.from(document.querySelectorAll(`.cat-${catId}`))
            .filter(r => (parseFloat(r.dataset.maxPrice) || 0) === 0).length;
        const warn = document.getElementById(`cat-warn-${catId}`);
        if (warn) warn.classList.toggle('d-none', zeroInCat === 0);
    });
}

function applyFilters() {
    const search       = document.getElementById('pricing-search').value.toLowerCase().trim();
    const margeFilter  = document.getElementById('marge-filter').value;
    let visibleCount   = 0;
    const catVisible   = {};

    document.querySelectorAll('.product-row').forEach(row => {
        const name      = row.dataset.name || '';
        const maxP      = parseFloat(row.dataset.maxPrice) || 0;
        const raw       = parseFloat(row.dataset.raw) || 0;
        const expMargin = parseFloat(row.dataset.expMargin) || 0;
        const catId     = row.dataset.categoryId;
        let show = true;

        if (search && !name.includes(search)) show = false;

        if (show && margeFilter !== 'all') {
            if (margeFilter === 'zero' && maxP !== 0) show = false;
            if (margeFilter === 'below_target') {
                if (maxP === 0 || expMargin === 0) show = false;
                else {
                    const m = (maxP - raw) / maxP * 100;
                    if (m >= expMargin) show = false;
                }
            }
            if (margeFilter === 'low') {
                if (maxP === 0) show = false;
                else if ((maxP - raw) / maxP * 100 >= 20) show = false;
            }
            if (margeFilter === 'ok') {
                if (maxP === 0) show = false;
                else if ((maxP - raw) / maxP * 100 < 20) show = false;
            }
        }

        row.classList.toggle('d-none', !show);
        if (show) { visibleCount++; catVisible[catId] = true; }
    });

    document.querySelectorAll('.category-header-row').forEach(hr => {
        hr.classList.toggle('d-none', !catVisible[hr.dataset.catId]);
    });
    document.getElementById('empty-filter-state').classList.toggle('d-none', visibleCount > 0);
}

function resetFilters() {
    document.getElementById('pricing-search').value = '';
    document.getElementById('marge-filter').value   = 'all';
    applyFilters();
}
</script>
{% endblock %}
```

---

## Tłumaczenia

Wszystkie klucze w formacie `translations.KEY` muszą być w plikach JSON:
- `public/resources/lang/page/pl/product.json`
- `public/resources/lang/page/en/product.json`
- `public/resources/lang/page/fr/product.json`
- `public/resources/lang/page/nl/product.json`
- `public/resources/lang/page/it/product.json`

**Lista kluczy (skopiuj z `pricing.twig` i dodaj):**

```json
{
  "max_price_management": "Zarządzanie maksymalnymi cenami produktów",
  "max_price_subtitle": "Ustaw maksymalną cenę sprzedaży dla każdego produktu w systemie",
  "products": "Produkty",
  "without_max_price": "Bez ceny maks.",
  "avg_margin_on_max_price": "Śred. marża (cena maks.)",
  "product_list": "Lista produktów",
  "pricing_subtitle_hint": "Ustaw maksymalną cenę sprzedaży dla każdego produktu",
  "search": "Szukaj produktu...",
  "clear": "Wyczyść",
  "all_products": "Wszystkie",
  "below_target_margin": "Marża poniżej celu",
  "low_margin": "Niska marża (< 20%)",
  "ok_margin": "Dobra marża (≥ 20%)",
  "cost_section": "Koszt",
  "expected_margin_section": "Oczek. marża",
  "max_price_section": "Maksymalna cena",
  "product": "Produkt",
  "production_cost": "Koszt prod.",
  "vat": "VAT",
  "expected_margin_short": "Oczek.",
  "max_sale_price": "Maks. cena",
  "margin_on_max": "Marża",
  "vegetarian": "Wegetariański",
  "expected_margin_tooltip": "Oczekiwana marża dla tego produktu",
  "edit": "Edytuj",
  "save": "Zapisz",
  "cancel": "Anuluj",
  "for_expected_margin": "Dla",
  "no_results": "Brak wyników dla podanych filtrów",
  "reset_filters": "Resetuj filtry",
  "no_products": "Brak produktów",
  "legend": "Legenda:",
  "legend_good": "Wysoka marża",
  "legend_ok": "Dobra marża",
  "legend_bad": "Niska marża",
  "legend_target": "Oczekiwana marża",
  "legend_delta": "odchylenie od oczekiwanej marży",
  "save_was_successful": "Zapis powiódł się",
  "something_went_wrong": "Coś poszło nie tak",
  "number_of_products_without_fixed_maximum_price": "Produkty bez ustalonej ceny maks."
}
```

Dla innych języków (EN, FR, NL, IT) — przetłumacz odpowiednio.

---

## Dane z API

### Endpoint: `GET /products`
Zwraca wszystkie produkty dostępne w systemie (globalne, bez filtra po sklepie).

**Response:**
```json
{
  "success": true,
  "data": [
    {
      "id": 123,
      "name": "Tarta cytrynowa",
      "id_category": 11500,
      "category_name": "Tartes",
      "recipe_cost_gross": 8.50,
      "max_portion_price": 12.00,
      "expected_margin": 30.0,
      "tax_val_perc": 10,
      "is_vegetarian": false
    },
    ...
  ]
}
```

### Endpoint: `PATCH /products/{id}/max-price`
Aktualizuje maksymalną cenę sprzedaży produktu.

**Body:**
```json
{
  "max_portion_price": 12.50
}
```

**Response:**
```json
{
  "success": true,
  "message": "Maksymalna cena została zaktualizowana",
  "data": {
    "id": 123,
    "max_portion_price": 12.50
  },
  "code": 200
}
```

---

## Checklist implementacji

### Backend
- [ ] Utworzyć kontroler `controllers/Product/PriceController.php` (lub rozszerzyć istniejący)
- [ ] Dodać metodę `maxPriceList()` — pobiera produkty, grupuje po kategoriach, renderuje widok
- [ ] Dodać metodę `ajaxUpdateMaxPrice(int $id)` — AJAX endpoint do zapisu ceny
- [ ] Utworzyć/rozszerzyć serwis `services/Product/ProductService.php`
- [ ] Dodać metodę `getAll()` i `updateMaxPrice()`
- [ ] Utworzyć/rozszerzyć repozytorium `repositories/Product/ProductRepository.php`
- [ ] Dodać metodę `getAll()` → wywołuje `GET /products`
- [ ] Dodać metodę `updateMaxPrice()` → wywołuje `PATCH /products/{id}/max-price`
- [ ] Rozszerzyć `models/ProductModel.php`:
  - Pola: `recipe_cost_gross`, `max_portion_price`, `expected_margin`, `tax_val_perc`, `is_vegetarian`
  - Metoda `calcMargeOnMaxPrice()` → oblicza marżę
  - Metoda `getTargetPriceForExpectedMargin()` → oblicza sugerowaną cenę
- [ ] Zarejestrować routing:
  - `GET /products/max-price-list`
  - `POST /ajax/products/{id}/max-price`

### Frontend
- [ ] Utworzyć widok `views/product/price/max_price_list.twig`
- [ ] **Skopiować strukturę HTML z `pricing.twig`**:
  - Summary bar (bez nagłówka sklepu)
  - Karta z filtrami (search + select)
  - Tabela z sticky header
  - Wiersze kategorii (collapsible)
  - Wiersze produktów
  - Legenda
- [ ] **Usunąć kolumny**: "Obecna cena sprzedaży" i "Marża sp."
- [ ] **Zmienić grupowanie kolumn**: Koszt (2) + Oczek. marża (1) + Maks. cena (2)
- [ ] **Skopiować CSS** z `pricing.twig` (sticky header, category rows, badges, deltas, itp.)
- [ ] **Skopiować JavaScript** z `pricing.twig`:
  - Toggle kategorii
  - Inline edit (enable/cancel/disable)
  - AJAX save (**zmienić endpoint** na `/ajax/products/{id}/max-price`, **usuńć `id_shop`**)
  - Update row po zapisie (recalculate marge + delta)
  - Update stats (total, zero, avg marge)
  - Filtry (search + marge filter)
  - Reset filters
  - Tooltips Bootstrap

### Tłumaczenia
- [ ] Dodać klucze do `public/resources/lang/page/pl/product.json`
- [ ] Dodać klucze do `public/resources/lang/page/en/product.json`
- [ ] Dodać klucze do `public/resources/lang/page/fr/product.json`
- [ ] Dodać klucze do `public/resources/lang/page/nl/product.json`
- [ ] Dodać klucze do `public/resources/lang/page/it/product.json`

### API (backend zewnętrzny)
- [ ] Upewnić się, że endpoint `GET /products` zwraca wszystkie produkty z polami:
  - `id`, `name`, `id_category`, `category_name`
  - `recipe_cost_gross`, `max_portion_price`, `expected_margin`
  - `tax_val_perc`, `is_vegetarian`
- [ ] Upewnić się, że endpoint `PATCH /products/{id}/max-price` akceptuje:
  - `{ "max_portion_price": float }`
- [ ] Zwraca standardową odpowiedź JSON z `success`, `message`, `data`, `code`

---

## Podsumowanie

Masz teraz kompletny prompt do zaimplementowania modułu **Max Price Management** w panelu admina.

**Kluczowe punkty:**
1. **Odtwórz IDENTYCZNĄ strukturę UI/UX** z załączonego `pricing.twig`
2. **Usuń** kolumny związane z ceną sprzedaży sklepu (portion_price, marge on portion)
3. **Zachowaj** wszystkie style CSS, JavaScript, filtry, statystyki
4. **Zmień** tylko:
   - Routing: `/products/max-price-list`
   - Endpoint AJAX: `/ajax/products/{id}/max-price` (bez `id_shop`)
   - Usuń nagłówek ze szczegółami sklepu
   - Zmień tytuły/etykiety (admin context)

Moduł będzie **identycznie wyglądał** jak obecny moduł pricing dla sklepów, ale dostosowany do kontekstu admina (globalne produkty, maksymalna cena zamiast ceny sklepu).

Powodzenia w implementacji! 🚀

