Files
Jacek Pyziak 972c69b136 feat(v0.1): historia cen + jawnosc cen — milestone Initial Release
Historia cen:
- Tabela wp_price_history z WP Cronem dziennym (snapshot cen)
- AJAX endpoint apartamenty_get_price_history (zabezpieczony nonce)
- Popup "Historia cen" w widgecie — vanilla JS, modal zgodny z projektem

Jawnosc cen:
- Endpointy /ceny-mieszkan.xml + /dane-gov-pl.xml (XSD-compliant)
- Pliki MD5 dla obu XML
- Strona admina: Narzedzia -> Jawnosc Cen z URL-ami do Ministerstwa
- Transient cache 1h z inwalidacja przez cron

Dokumentacja: docs/readme.md + docs/jawnosc-cen.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 15:40:29 +01:00

20 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous
phase plan type wave depends_on files_modified autonomous
01-historia-cen 02 execute 1
01-01
wp-content/plugins/elementor-addon/widgets/apartaments.php
wp-content/plugins/elementor-addon/assets/css/main.scss
wp-content/plugins/elementor-addon/assets/css/main.css
wp-content/plugins/elementor-addon/assets/js/main.js
false
## Goal Zbudować popup „Historia cen" — klikalny przycisk na karcie apartamentu otwiera modal z aktualną ceną i tabelą historii zmian, zasilany przez AJAX endpoint z planu 01-01.

Purpose

Użytkownik może zobaczyć historię zmian cen apartamentu bez opuszczania strony. Popup pokazuje nazwę apartamentu, aktualną cenę brutto i za m², oraz tabelę dat ze zmianami.

Output

  • Przycisk „HISTORIA CEN" z data-post-id w widgecie
  • Globalny popup HTML (overlay + modal) w widgecie
  • CSS modal pasujący do projektu (font Barlow, kolor #192c44, border 4px solid)
  • JS: fetch AJAX → wypełnienie popupa → pokaż/ukryj
## Project Context @.paul/PROJECT.md @.paul/STATE.md

Prior Work

@.paul/phases/01-historia-cen/01-01-SUMMARY.md

Source Files

@wp-content/plugins/elementor-addon/widgets/apartaments.php @wp-content/plugins/elementor-addon/assets/css/main.scss @wp-content/plugins/elementor-addon/assets/js/main.js

API Contract (z planu 01-01)

POST admin-ajax.php body: action=apartamenty_get_price_history&post_id=ID&nonce=NONCE response: { success: true, data: { title: "Apartament X", price: "677 920", // cena brutto price_m2: "19 000", // cena za m² floor_space: "35,68", // metraż history: [ { recorded_at: "2026-01-16", price: "677 920", price_m2: "19 000" } ] } }

JS globals dostępne na stronie

window.apartamentsData.ajaxUrl — URL do admin-ajax.php window.apartamentsData.nonce — nonce 'apartamenty_price_history_nonce'

Design (z mockupu Group 68.png)

  • Białe tło, border matching kart (#192c44)
  • Tytuł (bold, duży) + X w prawym górnym rogu
  • "Cena brutto:" (bold) → wartość bold po prawej z " zł"
  • "Cena m²:" (normal) → wartość normal po prawej z " zł"
  • Separator
  • Tabela: data | cena_m2 (format "X zł/m²") | cena brutto (format "X,00 zł")
  • Overlay ciemne tło półprzezroczyste

Istniejące style CSS

  • Font: 'Barlow', sans-serif
  • Kolor główny: #192c44
  • Border kart: 4px solid #192c44
  • Wiersz .apartament-card__price-history już ma cursor: pointer

<acceptance_criteria>

AC-1: Przycisk "Historia cen" jest klikalny

Given apartament ma dane cen w ACF
When użytkownik widzi kartę apartamentu
Then wiersz "HISTORIA CEN" jest klikalny i posiada atrybut data-post-id z ID apartamentu

AC-2: Popup otwiera się z danymi

Given użytkownik klika "HISTORIA CEN" przy apartamencie
When request AJAX zakończy się sukcesem
Then pojawia się modal z:
  - tytułem apartamentu
  - aktualną ceną brutto (bold)
  - aktualną ceną m² (normal)
  - tabelą historii (data | cena m² | cena brutto)

AC-3: Popup zamyka się

Given popup jest otwarty
When użytkownik klika przycisk X lub klika poza modalem (na overlay)
Then popup znika

AC-4: Stan ładowania i błędu

Given użytkownik kliknął "Historia cen"
When AJAX jest w trakcie
Then popup pokazuje "Ładowanie..."
And jeśli AJAX zwróci błąd, popup pokazuje "Brak danych"

AC-5: Historia jest pusta

Given apartament nie ma jeszcze rekordów w tabeli historii
When użytkownik otworzy popup
Then widoczna jest sekcja z aktualną ceną, a tabela historii jest pusta lub pokazuje komunikat "Brak historii cen"

</acceptance_criteria>

Task 1: Dodaj data-post-id do przycisku i popup HTML do widgetu wp-content/plugins/elementor-addon/widgets/apartaments.php **Zmiana 1: Wiersz "HISTORIA CEN"**
Znajdź wiersz z klasą `apartament-card__price-history` (linia ~157).
Zmień `<td class="apartament-card__info_table-value">` na:
```php
<td class="apartament-card__info_table-value btn-historia-cen"
    data-post-id="<?php echo esc_attr( get_the_ID() ); ?>">
```
Reszta wiersza (tekst "HISTORIA CEN" + SVG) pozostaje bez zmian.

**Zmiana 2: Globalny popup HTML**

Dodaj PRZED `<?php wp_reset_postdata(); ?>` (czyli po zamknięciu pętli while)
następujący HTML popupa (jeden egzemplarz dla całej strony):

```php
<?php // Popup historia cen — jeden globalny, wypełniany przez JS ?>
<div class="price-history-overlay" id="price-history-overlay" aria-hidden="true">
    <div class="price-history-modal" role="dialog" aria-modal="true">
        <button class="price-history-modal__close" id="price-history-close" aria-label="Zamknij">
            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M13 1L1 13M1 1L13 13" stroke="#192C44" stroke-width="2" stroke-linecap="round"/>
            </svg>
        </button>
        <h3 class="price-history-modal__title" id="price-history-title"></h3>
        <div class="price-history-modal__current">
            <div class="price-history-modal__row price-history-modal__row--bold">
                <span>Cena brutto:</span>
                <span class="price-history-modal__val" id="price-history-price"></span>
            </div>
            <div class="price-history-modal__row">
                <span>Cena m<sup>2</sup>:</span>
                <span class="price-history-modal__val" id="price-history-price-m2"></span>
            </div>
        </div>
        <div class="price-history-modal__table-wrap">
            <table class="price-history-modal__table">
                <tbody id="price-history-tbody"></tbody>
            </table>
        </div>
    </div>
</div>
```

Unikaj: dodawania popup wewnątrz pętli while — tylko jeden egzemplarz na stronę.
Sprawdź PHP syntax: `php -l wp-content/plugins/elementor-addon/widgets/apartaments.php` Sprawdź czy data-post-id jest w HTML widgetu przez DevTools lub view-source. AC-1 spełnione: przycisk ma data-post-id; popup HTML istnieje w DOM Task 2: CSS popup — main.scss i main.css wp-content/plugins/elementor-addon/assets/css/main.scss, wp-content/plugins/elementor-addon/assets/css/main.css Dodaj na końcu pliku `main.scss` (po zamknięciu `.apartaments { }`) poniższe style. Następnie dodaj **te same skompilowane style** (bez SCSS zagnieżdżeń, rozwinięte) na końcu pliku `main.css`.
**Style do dodania w main.scss:**
```scss
// Historia cen — popup overlay
.price-history-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 99999;
    background: rgba(25, 44, 68, 0.55);
    align-items: center;
    justify-content: center;
    padding: 20px;

    &.is-open {
        display: flex;
    }
}

.price-history-modal {
    position: relative;
    background: #fff;
    border: 4px solid #192c44;
    padding: 32px 36px 28px;
    max-width: 560px;
    width: 100%;
    max-height: 80vh;
    overflow-y: auto;
    font-family: 'Barlow', sans-serif;
    color: #192c44;

    @media (max-width: 600px) {
        padding: 24px 20px 20px;
    }

    &__close {
        position: absolute;
        top: 14px;
        right: 14px;
        background: none;
        border: 2px solid #192c44;
        width: 30px;
        height: 30px;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        padding: 0;
        line-height: 1;
    }

    &__title {
        font-size: 22px;
        font-weight: 700;
        margin: 0 0 18px;
        padding-right: 40px;
        color: #192c44;
    }

    &__current {
        margin-bottom: 16px;
    }

    &__row {
        display: flex;
        justify-content: space-between;
        font-size: 18px;
        line-height: 1.5;
        color: #192c44;

        &--bold {
            font-weight: 700;
        }
    }

    &__val {
        text-align: right;
    }

    &__table-wrap {
        border-top: 1px solid #192c44;
        padding-top: 12px;
        margin-top: 4px;
    }

    &__table {
        width: 100%;
        border-collapse: collapse;

        tr {
            border: none;
            background: transparent;

            td {
                padding: 4px 0;
                font-size: 15px;
                color: #192c44;
                font-family: 'Barlow', sans-serif;
                font-weight: 400;
                border: none;
                background: transparent;

                &:last-child {
                    text-align: right;
                }

                &:nth-child(2) {
                    text-align: center;
                }
            }
        }
    }
}
```

**Skompilowana wersja CSS do dodania na końcu main.css** (rozwinięte selektory, bez &):

```css
/* Historia cen — popup */
.price-history-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 99999;
    background: rgba(25, 44, 68, 0.55);
    align-items: center;
    justify-content: center;
    padding: 20px;
}
.price-history-overlay.is-open {
    display: flex;
}
.price-history-modal {
    position: relative;
    background: #fff;
    border: 4px solid #192c44;
    padding: 32px 36px 28px;
    max-width: 560px;
    width: 100%;
    max-height: 80vh;
    overflow-y: auto;
    font-family: 'Barlow', sans-serif;
    color: #192c44;
}
@media (max-width: 600px) {
    .price-history-modal {
        padding: 24px 20px 20px;
    }
}
.price-history-modal__close {
    position: absolute;
    top: 14px;
    right: 14px;
    background: none;
    border: 2px solid #192c44;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    line-height: 1;
}
.price-history-modal__title {
    font-size: 22px;
    font-weight: 700;
    margin: 0 0 18px;
    padding-right: 40px;
    color: #192c44;
}
.price-history-modal__current {
    margin-bottom: 16px;
}
.price-history-modal__row {
    display: flex;
    justify-content: space-between;
    font-size: 18px;
    line-height: 1.5;
    color: #192c44;
}
.price-history-modal__row--bold {
    font-weight: 700;
}
.price-history-modal__val {
    text-align: right;
}
.price-history-modal__table-wrap {
    border-top: 1px solid #192c44;
    padding-top: 12px;
    margin-top: 4px;
}
.price-history-modal__table {
    width: 100%;
    border-collapse: collapse;
}
.price-history-modal__table tr {
    border: none;
    background: transparent;
}
.price-history-modal__table td {
    padding: 4px 0;
    font-size: 15px;
    color: #192c44;
    font-family: 'Barlow', sans-serif;
    font-weight: 400;
    border: none;
    background: transparent;
}
.price-history-modal__table td:last-child {
    text-align: right;
}
.price-history-modal__table td:nth-child(2) {
    text-align: center;
}
```

Unikaj: modyfikowania istniejących reguł CSS — tylko append na końcu.
Unikaj: zmian w mapie .css.map — pozostaw jak jest.
Sprawdź że klasy `.price-history-overlay` i `.price-history-modal` są obecne w main.css. Sprawdź że nie ma błędów składni SCSS (ręczna inspekcja nawiasów). AC-2, AC-3 (style): modal wygląda zgodnie z projektem Task 3: JS — obsługa kliknięcia, AJAX, render popupa wp-content/plugins/elementor-addon/assets/js/main.js Dodaj na końcu pliku `main.js` (po zamknięciu istniejącego DOMContentLoaded) nowy blok obsługi historii cen.
**Kod do dodania:**
```javascript
// Historia cen
document.addEventListener('DOMContentLoaded', function () {
    var overlay  = document.getElementById('price-history-overlay');
    var closeBtn = document.getElementById('price-history-close');

    if (!overlay) return; // popup nie istnieje na tej stronie

    function openPopup() {
        overlay.setAttribute('aria-hidden', 'false');
        overlay.classList.add('is-open');
        document.body.style.overflow = 'hidden';
    }

    function closePopup() {
        overlay.setAttribute('aria-hidden', 'true');
        overlay.classList.remove('is-open');
        document.body.style.overflow = '';
    }

    // Zamknij przyciskiem X
    closeBtn.addEventListener('click', closePopup);

    // Zamknij klikając na overlay (poza modalem)
    overlay.addEventListener('click', function (e) {
        if (e.target === overlay) closePopup();
    });

    // Zamknij klawiszem Escape
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && overlay.classList.contains('is-open')) closePopup();
    });

    // Kliknięcie w przycisk Historia cen
    document.querySelectorAll('.btn-historia-cen').forEach(function (btn) {
        btn.addEventListener('click', function () {
            var postId = this.dataset.postId;
            if (!postId) return;

            // Reset i pokaż "Ładowanie..."
            document.getElementById('price-history-title').textContent   = 'Ładowanie...';
            document.getElementById('price-history-price').textContent   = '';
            document.getElementById('price-history-price-m2').textContent = '';
            document.getElementById('price-history-tbody').innerHTML     = '';
            openPopup();

            // Sprawdź dostępność danych globalnych (wp_localize_script)
            if (typeof apartamentsData === 'undefined') {
                document.getElementById('price-history-title').textContent = 'Błąd konfiguracji';
                return;
            }

            // Buduj FormData
            var formData = new FormData();
            formData.append('action',  'apartamenty_get_price_history');
            formData.append('post_id', postId);
            formData.append('nonce',   apartamentsData.nonce);

            fetch(apartamentsData.ajaxUrl, {
                method: 'POST',
                body: formData,
                credentials: 'same-origin',
            })
            .then(function (res) { return res.json(); })
            .then(function (json) {
                if (!json.success) {
                    document.getElementById('price-history-title').textContent = 'Brak danych';
                    return;
                }

                var d = json.data;

                document.getElementById('price-history-title').textContent =
                    d.title || '';
                document.getElementById('price-history-price').textContent =
                    d.price ? d.price + ' zł' : '—';
                document.getElementById('price-history-price-m2').textContent =
                    d.price_m2 ? d.price_m2 + ' zł' : '—';

                var tbody = document.getElementById('price-history-tbody');
                if (!d.history || d.history.length === 0) {
                    tbody.innerHTML = '<tr><td colspan="3">Brak historii cen</td></tr>';
                    return;
                }

                tbody.innerHTML = d.history.map(function (row) {
                    return '<tr>' +
                        '<td>' + (row.recorded_at || '') + '</td>' +
                        '<td>' + (row.price_m2 ? row.price_m2 + ' zł/m²' : '—') + '</td>' +
                        '<td>' + (row.price ? row.price + ' zł' : '—') + '</td>' +
                    '</tr>';
                }).join('');
            })
            .catch(function () {
                document.getElementById('price-history-title').textContent = 'Błąd ładowania';
            });
        });
    });
});
```

Unikaj: modyfikowania istniejącego bloku DOMContentLoaded (Swiper/Fancybox) — tylko append.
Unikaj: jQuery — używaj vanilla JS zgodnie z istniejącym stylem kodu.
`php -l` nie jest tu potrzebny — sprawdź składnię JS ręcznie (nawiasy, cudzysłowy). Po wgraniu: otwórz stronę /apartamenty/, otwórz DevTools Console, kliknij "HISTORIA CEN" → popup powinien się pojawić z danymi lub "Ładowanie...". AC-2, AC-3, AC-4, AC-5 spełnione: popup otwiera się z danymi AJAX, zamyka się poprawnie Popup „Historia cen" — kliknięcie przycisku otwiera modal z danymi z AJAX. Przycisk ma data-post-id, popup zamyka się przez X / klik overlay / Escape. 1. Odwiedź: https://wyszynskiego12.pagedev.pl/apartamenty/ 2. Odszukaj wiersz „HISTORIA CEN" przy dowolnym apartamencie i kliknij go 3. Sprawdź czy pojawia się modal z: - Tytułem apartamentu (np. „Apartament 15") - Ceną brutto (bold) - Ceną m² (normal) - Tabelą historii (lub „Brak historii cen" jeśli tabela pusta) 4. Kliknij X → popup zamknięty 5. Kliknij poza modalem → popup zamknięty 6. Naciśnij Escape → popup zamknięty 7. Otwórz DevTools → Console → brak błędów JavaScript Wpisz "zatwierdzone" lub opisz problemy do poprawienia

DO NOT CHANGE

  • wp-content/plugins/elementor-addon/elementor-addon.php (ukończony w planie 01-01)
  • wp-content/plugins/elementor-addon/plugins/ (biblioteki zewnętrzne)
  • wp-config.php

SCOPE LIMITS

  • Nie dodawaj animacji CSS poza prostym display:flex/none
  • Nie dodawaj nowych zależności JS (bez jQuery, bez bibliotek)
  • Popup ma jeden egzemplarz na stronę — nie w pętli
  • Nie modyfikuj istniejących reguł CSS — tylko append
Before declaring plan complete: - [ ] PHP syntax OK: `php -l widgets/apartaments.php` - [ ] Klasa `.price-history-overlay` istnieje w main.css - [ ] Klasa `.btn-historia-cen` jest na wierszu HISTORIA CEN w HTML - [ ] `data-post-id` jest ustawiony poprawnie dla każdego apartamentu - [ ] JS nie wyrzuca błędów w konsoli - [ ] Checkpoint human-verify zatwierdzony

<success_criteria>

  • Kliknięcie "HISTORIA CEN" otwiera popup z danymi apartamentu
  • Popup zamyka się przez X, klik overlay i Escape
  • Stan ładowania i błędu jest obsługiwany
  • Brak błędów JS w konsoli
  • Wygląd zgodny z projektem (font Barlow, kolor #192c44, border 4px) </success_criteria>
Po ukończeniu utwórz `.paul/phases/01-historia-cen/01-02-SUMMARY.md`