Save
This commit is contained in:
123
.paul/HANDOFF-2026-03-12.md
Normal file
123
.paul/HANDOFF-2026-03-12.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# PAUL Handoff
|
||||
|
||||
**Date:** 2026-03-12 (aktualizacja: sesja 2)
|
||||
**Status:** paused — plan 02-01 gotowy, czeka na APPLY w nowej sesji
|
||||
|
||||
---
|
||||
|
||||
## READ THIS FIRST
|
||||
|
||||
You have no prior context. This document tells you everything.
|
||||
|
||||
**Project:** wyszynskiego12.pagedev.pl — strona internetowa dewelopera
|
||||
**Core value:** Użytkownicy mogą przeglądać na stronie ofertę dewelopera
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
**Version:** v0.0.0 (Prototype)
|
||||
**Phase:** 2 of 2 — Jawność cen
|
||||
**Plan:** 02-01 — PLAN created, ready for APPLY
|
||||
|
||||
**Loop Position:**
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ○ ○ [Plan 02-01 gotowy, oczekuje na zatwierdzenie]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Was Done
|
||||
|
||||
**Sesja 1 (wcześniej):**
|
||||
- Tabela `wp_price_history` założona przez dbDelta()
|
||||
- WP Cron `apartamenty_record_prices` dzienny — snapshot cen apartamentów
|
||||
- AJAX endpoint `apartamenty_get_price_history` (public, nonce-secured)
|
||||
- `wp_localize_script` przekazujący ajaxUrl + nonce do JS
|
||||
|
||||
**Sesja 2 (ta sesja):**
|
||||
- Zbadano wymagania ustawy o jawności cen (dane.gov.pl)
|
||||
- Odczytano XSD portalu: `https://www.dane.gov.pl/static/xml/otwarte_dane_latest.xsd`
|
||||
- Ustalono format XML cen (voxDeveloper-compatible)
|
||||
- Dodano Phase 2 do ROADMAP.md
|
||||
- Utworzono plan `02-01-PLAN.md` — 3 zadania auto + 1 checkpoint
|
||||
|
||||
**WAŻNE: Plan 01-02 (frontend popup historia cen) nadal czeka na APPLY.**
|
||||
Zdecydowano skupić się najpierw na 02-01 (jawność cen). Można wrócić do 01-02 później.
|
||||
|
||||
---
|
||||
|
||||
## What's In Progress
|
||||
|
||||
Plan `02-01-PLAN.md` gotowy, NIE był jeszcze uruchomiony APPLY.
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
**Immediate:** `/paul:apply .paul/phases/02-jawnosc-cen/02-01-PLAN.md`
|
||||
|
||||
Plan 02-01 zawiera 3 zadania auto + 1 checkpoint:
|
||||
1. Endpoint `/ceny-mieszkan.xml` + `/ceny-mieszkan.md5` (price data XML z ACF + wp_price_history)
|
||||
2. Endpoint `/dane-gov-pl.xml` + `/dane-gov-pl.md5` (katalog XSD-compliant dla dane.gov.pl)
|
||||
3. Strona admin: `Narzędzia → Jawność Cen` z URL-ami do zgłoszenia do Ministerstwa
|
||||
4. Checkpoint: weryfikacja URL-ów w przeglądarce
|
||||
|
||||
**After that:** UNIFY plan 02-01, then consider 01-02 (frontend popup)
|
||||
|
||||
---
|
||||
|
||||
## Key Context — Technikalia
|
||||
|
||||
**Jedyny plik do zmiany:** `wp-content/plugins/elementor-addon/elementor-addon.php`
|
||||
|
||||
**Stack:** WordPress + ACF + Elementor + custom plugin `elementor-addon`
|
||||
|
||||
**Dostępne ACF flat meta keys (z `information` group):**
|
||||
- `information_price` — cena brutto
|
||||
- `information_price_m2` — cena za m²
|
||||
- `information_floor_space` — metraż
|
||||
- `information_type` — typ lokalu
|
||||
- `information_floor` — piętro
|
||||
- `information_status` — status (value/label)
|
||||
|
||||
**Tabela wp_price_history:** `id, post_id, price, price_m2, floor_space, recorded_at`
|
||||
|
||||
**Post type:** `apartamenty`
|
||||
|
||||
**Istniejące hooki w elementor-addon.php:**
|
||||
- Cron: `apartamenty_record_prices` (daily)
|
||||
- AJAX: `wp_ajax_apartamenty_get_price_history` + nopriv
|
||||
- Nonce: `apartamenty_price_history_nonce`
|
||||
|
||||
**Wymagania techniczne dane.gov.pl:**
|
||||
- Content-Type: `application/xml` lub `text/xml`
|
||||
- MD5 companion file — lowercase hex, ten sam URL ale z `.md5` zamiast `.xml`
|
||||
- Port standardowy (80/443)
|
||||
- Aktualizacja min. co 24h
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `.paul/STATE.md` | Live project state |
|
||||
| `.paul/ROADMAP.md` | Phase overview (fazy 1 i 2) |
|
||||
| `.paul/phases/02-jawnosc-cen/02-01-PLAN.md` | Plan do APPLY |
|
||||
| `.paul/phases/01-historia-cen/01-01-SUMMARY.md` | Co zbudowano w backendzie |
|
||||
| `wp-content/plugins/elementor-addon/elementor-addon.php` | Jedyny plik do modyfikacji |
|
||||
|
||||
---
|
||||
|
||||
## Resume Instructions
|
||||
|
||||
1. Przeczytaj `.paul/STATE.md` dla aktualnej pozycji
|
||||
2. Uruchom `/paul:apply .paul/phases/02-jawnosc-cen/02-01-PLAN.md`
|
||||
3. Po APPLY wykonaj checkpoint (flush permalinks, sprawdź URL-e w przeglądarce)
|
||||
4. Następnie `/paul:unify`
|
||||
|
||||
---
|
||||
|
||||
*Handoff created: 2026-03-12 (updated: sesja 2)*
|
||||
45
.paul/MILESTONES.md
Normal file
45
.paul/MILESTONES.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Milestones
|
||||
|
||||
Completed milestone log for this project.
|
||||
|
||||
| Milestone | Completed | Duration | Stats |
|
||||
|-----------|-----------|----------|-------|
|
||||
| v0.1 Initial Release | 2026-03-12 | 1 dzień | 2 fazy, 3 plany, 6 plików |
|
||||
|
||||
---
|
||||
|
||||
## ✅ v0.1 Initial Release
|
||||
|
||||
**Completed:** 2026-03-12
|
||||
**Duration:** 1 dzień
|
||||
|
||||
### Stats
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Phases | 2 |
|
||||
| Plans | 3 |
|
||||
| Files changed | 6 |
|
||||
|
||||
### Key Accomplishments
|
||||
|
||||
- Tabela `wp_price_history` z WP Cronem dziennym — automatyczny snapshot cen wszystkich apartamentów
|
||||
- AJAX endpoint `apartamenty_get_price_history` zabezpieczony nonce — historia cen jako JSON
|
||||
- Popup „Historia cen" w widgecie — vanilla JS, modal zgodny z projektem (Barlow, #192c44)
|
||||
- Cztery publiczne endpointy XML: `/ceny-mieszkan.xml`, `/ceny-mieszkan.md5`, `/dane-gov-pl.xml`, `/dane-gov-pl.md5`
|
||||
- Katalog XSD-compliant dla portalu dane.gov.pl z automatycznym URL przez `home_url()`
|
||||
- Strona admina wp-admin → Narzędzia → Jawność Cen z URL-ami do skopiowania
|
||||
- Transient cache XML (1h) z inwalidacją przy każdym cronie
|
||||
- Dokumentacja klienta w `docs/jawnosc-cen.md` — instrukcja zgłoszenia do Ministerstwa
|
||||
|
||||
### Key Decisions
|
||||
|
||||
| Decyzja | Uzasadnienie |
|
||||
|---------|--------------|
|
||||
| Flat ACF meta keys zamiast `get_field('information')` | ACF zapisuje dane jako płaskie klucze — bezpośredni `get_post_meta` |
|
||||
| INSERT IGNORE w cronie | Historia to snapshot — jeden rekord na apt na dzień, idempotentny |
|
||||
| XML jako czysty PHP string (ENT_XML1) | Brak gwarancji ext-dom na hostingu |
|
||||
| Transient 1h + inwalidacja przez cron | Balans między wydajnością a świeżością danych |
|
||||
| Wszystko w elementor-addon.php | Scope limit z planu — brak osobnych plików |
|
||||
|
||||
---
|
||||
75
.paul/PROJECT.md
Normal file
75
.paul/PROJECT.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Project: wyszynskiego12.pagedev.pl
|
||||
|
||||
## What This Is
|
||||
|
||||
Strona internetowa dla dewelopera pozwalająca na okazanie oferty klientom.
|
||||
|
||||
## Core Value
|
||||
|
||||
Użytkownicy mogą przeglądać na stronie ofertę dewelopera.
|
||||
|
||||
## Current State
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 0.1.0 |
|
||||
| Status | Released |
|
||||
| Last Updated | 2026-03-12 |
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated (Shipped)
|
||||
|
||||
- [x] Użytkownik widzi historię zmian cen dla każdego apartamentu — v0.1 Initial Release
|
||||
- [x] Popup „Historia cen" otwiera się po kliknięciu z aktualną ceną i tabelą zmian — v0.1 Initial Release
|
||||
- [x] System automatycznie zapisuje ceny codziennie (WP Cron) — v0.1 Initial Release
|
||||
- [x] Deweloper może raportować ceny do portalu dane.gov.pl przez publiczny endpoint XML — v0.1 Initial Release
|
||||
- [x] Strona admina z URL-ami do zgłoszenia do Ministerstwa — v0.1 Initial Release
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
- (brak — gotowy na nowy milestone)
|
||||
|
||||
### Planned (Next)
|
||||
|
||||
- (do zdefiniowania podczas następnego planowania)
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Rejestracja dewelopera na dane.gov.pl (czynność ręczna po stronie klienta)
|
||||
- Walidator XSD po stronie PHP
|
||||
- Obsługa wielu inwestycji (tylko Wyszyńskiego 12)
|
||||
- Formularz do edycji danych inwestycji w adminie
|
||||
|
||||
## Target Users
|
||||
|
||||
**Primary:** Potencjalni klienci dewelopera
|
||||
- Poszukują mieszkania lub lokalu
|
||||
- Chcą szybko zapoznać się z ofertą
|
||||
- Oczekują przejrzystej prezentacji inwestycji
|
||||
|
||||
**Secondary:** Deweloper / admin
|
||||
- Zarządza ofertą przez WordPress admin
|
||||
- Musi raportować ceny do portalu rządowego
|
||||
|
||||
## Constraints
|
||||
|
||||
### Technical Constraints
|
||||
- WordPress + Elementor (plugin elementor-addon)
|
||||
- ACF dla pól apartamentów (flat meta keys: `information_price`, `information_price_m2`, etc.)
|
||||
- Hosting: brak gwarancji ext-dom PHP — XML generowany jako czysty string
|
||||
- WP Cron (pseudocron) — wymaga ruchu na stronie lub systemowego crona
|
||||
|
||||
### Business Constraints
|
||||
- Ustawa o jawności cen nieruchomości — obowiązek raportowania do dane.gov.pl
|
||||
- Format XML zgodny z XSD portalu otwarte_dane_latest.xsd
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Użytkownicy mogą przeglądać na stronie ofertę dewelopera
|
||||
- [x] Użytkownicy mogą sprawdzić historię zmian cen apartamentów
|
||||
- [x] Deweloper spełnia wymóg ustawy o jawności cen (endpoint XML gotowy do zgłoszenia)
|
||||
|
||||
---
|
||||
*Created: 2026-03-12*
|
||||
*Last updated: 2026-03-12 after v0.1 Initial Release*
|
||||
34
.paul/ROADMAP.md
Normal file
34
.paul/ROADMAP.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Roadmap: wyszynskiego12.pagedev.pl
|
||||
|
||||
## Overview
|
||||
|
||||
Strona internetowa dla dewelopera pozwalająca na okazanie oferty klientom. Projekt obejmuje budowę witryny prezentującej inwestycje, lokale i informacje kontaktowe.
|
||||
|
||||
## Current Milestone
|
||||
|
||||
Brak aktywnego milestone.
|
||||
Uruchom `/paul:discuss-milestone` lub `/paul:milestone` aby zdefiniować następny.
|
||||
|
||||
## Next Milestone
|
||||
|
||||
Uruchom `/paul:discuss-milestone` lub `/paul:milestone` aby zdefiniować.
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
<details>
|
||||
<summary>v0.1 Initial Release - 2026-03-12 (2 fazy)</summary>
|
||||
|
||||
| Phase | Name | Plans | Completed |
|
||||
|-------|------|-------|-----------|
|
||||
| 1 | Historia cen | 2/2 | 2026-03-12 |
|
||||
| 2 | Jawnosc cen | 1/1 | 2026-03-12 |
|
||||
|
||||
**Kluczowe deliverables:**
|
||||
- Popup historia cen (widget + AJAX + CSS + JS)
|
||||
- XML endpointy jawnosci cen (dane.gov.pl)
|
||||
- Strona admina z URL-ami do Ministerstwa
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
*Roadmap updated: 2026-03-12 after v0.1 Initial Release*
|
||||
58
.paul/STATE.md
Normal file
58
.paul/STATE.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .paul/PROJECT.md (updated 2026-03-12)
|
||||
|
||||
**Core value:** Uzytkownik moze przegladac oferte dewelopera i sprawdzac historie cen
|
||||
**Current focus:** Milestone v0.1 ukonczone — gotowy na nowy milestone
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: Awaiting next milestone
|
||||
Phase: None active
|
||||
Plan: None
|
||||
Status: Milestone v0.1 Initial Release complete — ready for next
|
||||
Last activity: 2026-03-12 — Milestone v0.1 completed
|
||||
|
||||
Progress:
|
||||
- v0.1 Initial Release: [██████████] 100% ✓
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
○ ○ ○ [Milestone complete — ready for next]
|
||||
```
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
| Decyzja | Faza | Wplyw |
|
||||
|---------|------|-------|
|
||||
| Flat ACF meta keys (information_price etc.) | Phase 1 | Cron uzywa get_post_meta bezposrednio |
|
||||
| INSERT IGNORE w cronie | Phase 1 | Jeden rekord na apt na dzien, idempotentny |
|
||||
| XML jako czysty PHP string ENT_XML1 | Phase 2 | Brak zaleznosci od ext-dom |
|
||||
| Transient 1h + inwalidacja przez cron | Phase 2 | Cache XML odswieza sie po kazdym cronie |
|
||||
|
||||
### Deferred Issues
|
||||
- Klient musi recznie zglosic URL /dane-gov-pl.xml do kontakt@dane.gov.pl
|
||||
- WP Cron pseudocron — na produkcji zalecany systemowy cron (docs/readme.md)
|
||||
|
||||
### Blockers/Concerns
|
||||
Brak.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-12
|
||||
Stopped at: Milestone v0.1 Initial Release ukonczone
|
||||
Next action: /paul:discuss-milestone
|
||||
Resume file: .paul/MILESTONES.md
|
||||
Resume context:
|
||||
- v0.1 kompletny: historia cen (popup AJAX) + jawnosc cen (XML endpoints)
|
||||
- Dokumentacja klienta: docs/readme.md + docs/jawnosc-cen.md
|
||||
- Git tag v0.1.0 utworzony
|
||||
|
||||
---
|
||||
*STATE.md — Aktualizowany po kazdej istotnej akcji*
|
||||
44
.paul/milestones/v0.1-ROADMAP.md
Normal file
44
.paul/milestones/v0.1-ROADMAP.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# v0.1 Initial Release - Archive
|
||||
|
||||
**Archived:** 2026-03-12
|
||||
**Status:** Complete
|
||||
|
||||
---
|
||||
|
||||
# Roadmap: wyszynskiego12.pagedev.pl
|
||||
|
||||
## Overview
|
||||
|
||||
Strona internetowa dla dewelopera pozwalająca na okazanie oferty klientom. Projekt obejmuje budowę witryny prezentującej inwestycje, lokale i informacje kontaktowe.
|
||||
|
||||
## Current Milestone
|
||||
|
||||
**v0.1 Initial Release** (v0.1.0)
|
||||
Status: Complete
|
||||
Completed: 2026-03-12
|
||||
Phases: 2 of 2 complete
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Plans | Status | Completed |
|
||||
|-------|------|-------|--------|-----------|
|
||||
| 1 | Historia cen | 2 | Complete | 2026-03-12 |
|
||||
| 2 | Jawnosc cen | 1 | Complete | 2026-03-12 |
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Historia cen
|
||||
|
||||
**Goal:** Uzytkownik widzi popup z historia zmian cen dla kazdego apartamentu.
|
||||
**Plans:**
|
||||
- [x] 01-01: Backend — tabela DB, cron, AJAX endpoint
|
||||
- [x] 01-02: Frontend — popup HTML/CSS/JS
|
||||
|
||||
### Phase 2: Jawnosc cen
|
||||
|
||||
**Goal:** Deweloper moze automatycznie raportowac ceny mieszkan do portalu dane.gov.pl.
|
||||
**Plans:**
|
||||
- [x] 02-01: XML endpoints + admin page
|
||||
|
||||
---
|
||||
*Roadmap archived: 2026-03-12*
|
||||
225
.paul/phases/01-historia-cen/01-01-PLAN.md
Normal file
225
.paul/phases/01-historia-cen/01-01-PLAN.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
phase: 01-historia-cen
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- wp-content/plugins/elementor-addon/elementor-addon.php
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Stworzyć backend dla historii cen apartamentów:
|
||||
- tabela `wp_price_history` w bazie danych
|
||||
- WP Cron zapisujący ceny raz dziennie
|
||||
- AJAX endpoint zwracający historię dla danego apartamentu
|
||||
|
||||
## Purpose
|
||||
Ceny apartamentów zarządzane są przez ACF (pola `information_price`, `information_price_m2`, `information_floor_space`).
|
||||
Historia musi być zapisywana codziennie, bo ACF nie przechowuje zmian — tylko aktualną wartość.
|
||||
|
||||
## Output
|
||||
- Tabela `wp_price_history` założona przy aktywacji pluginu
|
||||
- Hook `apartamenty_record_prices` uruchamiany codziennie przez WP Cron
|
||||
- AJAX action `apartamenty_get_price_history` zwracający JSON
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@wp-content/plugins/elementor-addon/elementor-addon.php
|
||||
|
||||
## Struktura danych ACF
|
||||
Każdy apartament (post_type: `apartamenty`) posiada w `wp_postmeta`:
|
||||
- `information_price` → string np. `"677 920"` (cena brutto w zł)
|
||||
- `information_price_m2` → string np. `"19 000"` (cena za m² w zł)
|
||||
- `information_floor_space` → string np. `"35,68"` (metraż w m²)
|
||||
|
||||
Prefix tabel WP: `wp_`
|
||||
DB credentials: z wp-config.php (DB_NAME, DB_USER, DB_PASSWORD, DB_HOST)
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Tabela istnieje w bazie
|
||||
```gherkin
|
||||
Given plugin elementor-addon jest aktywny
|
||||
When WordPress się inicjalizuje
|
||||
Then tabela wp_price_history istnieje z kolumnami: id, post_id, price, price_m2, floor_space, recorded_at
|
||||
And istnieje UNIQUE KEY na (post_id, recorded_at) — jeden rekord na apt na dzień
|
||||
```
|
||||
|
||||
## AC-2: Cron zapisuje ceny codziennie
|
||||
```gherkin
|
||||
Given istnieją apartamenty z wypełnionymi polami cen ACF
|
||||
When hook `apartamenty_record_prices` zostanie wywołany
|
||||
Then dla każdego apartamentu z post_type=apartamenty i post_status=publish
|
||||
zostaje wstawiony lub zignorowany (INSERT IGNORE) rekord do wp_price_history
|
||||
z aktualną ceną i datą TODAY()
|
||||
```
|
||||
|
||||
## AC-3: AJAX endpoint zwraca historię
|
||||
```gherkin
|
||||
Given apartament o ID=X ma rekordy w wp_price_history
|
||||
When POST na /wp-admin/admin-ajax.php z action=apartamenty_get_price_history i post_id=X
|
||||
Then odpowiedź JSON zawiera:
|
||||
{ success: true, data: { title, price, price_m2, history: [{date, price, price_m2}] } }
|
||||
And historia jest posortowana od najnowszej daty
|
||||
```
|
||||
|
||||
## AC-4: Nonce zabezpiecza AJAX
|
||||
```gherkin
|
||||
Given request AJAX bez poprawnego nonce
|
||||
When POST na admin-ajax.php
|
||||
Then odpowiedź: { success: false }
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Utwórz tabelę wp_price_history i zarejestruj cron</name>
|
||||
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
|
||||
<action>
|
||||
Dodaj na końcu pliku elementor-addon.php (przed zamknięciem) następujące funkcje:
|
||||
|
||||
1. **Funkcja tworząca tabelę** `elementor_addon_create_price_history_table()`:
|
||||
- Używa `$wpdb->prefix . 'price_history'`
|
||||
- SQL z `dbDelta()` (wymaga `require_once ABSPATH . 'wp-admin/includes/upgrade.php'`)
|
||||
- Schemat tabeli:
|
||||
```sql
|
||||
CREATE TABLE {prefix}price_history (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
post_id BIGINT UNSIGNED NOT NULL,
|
||||
price VARCHAR(50) NOT NULL DEFAULT '',
|
||||
price_m2 VARCHAR(50) NOT NULL DEFAULT '',
|
||||
floor_space VARCHAR(50) NOT NULL DEFAULT '',
|
||||
recorded_at DATE NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_daily (post_id, recorded_at),
|
||||
KEY idx_post_id (post_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
- Podpięty do hooków: `register_activation_hook(__FILE__, ...)` ORAZ `init` (z check przez `get_option('elementor_addon_db_version')` — jeśli wersja != '1.0', wywołaj i ustaw opcję)
|
||||
|
||||
2. **Funkcja zapisująca ceny** `elementor_addon_record_prices()`:
|
||||
- WP_Query: post_type='apartamenty', post_status='publish', posts_per_page=-1, fields='ids'
|
||||
- Dla każdego ID: pobiera meta `information_price`, `information_price_m2`, `information_floor_space`
|
||||
- Wstawia do tabeli przez `$wpdb->query($wpdb->prepare("INSERT IGNORE INTO ...", ...))`
|
||||
- `recorded_at` = `current_time('Y-m-d')`
|
||||
|
||||
3. **Rejestracja crona**:
|
||||
- Na hooku `wp` (nie `init`): jeśli `!wp_next_scheduled('apartamenty_record_prices')` → `wp_schedule_event(time(), 'daily', 'apartamenty_record_prices')`
|
||||
- `add_action('apartamenty_record_prices', 'elementor_addon_record_prices')`
|
||||
|
||||
Unikaj: global $wpdb wewnątrz funkcji — używaj `global $wpdb` na początku każdej funkcji korzystającej z $wpdb.
|
||||
Unikaj: bezpośrednich zapytań SQL bez $wpdb->prepare() tam gdzie są parametry użytkownika.
|
||||
</action>
|
||||
<verify>
|
||||
1. Aktywuj/dezaktywuj plugin lub odwiedź dowolną stronę WP
|
||||
2. Sprawdź przez phpMyAdmin lub: `SELECT * FROM wp_price_history LIMIT 5;`
|
||||
3. Sprawdź: `SELECT option_value FROM wp_options WHERE option_name = 'elementor_addon_db_version';` → powinno zwrócić '1.0'
|
||||
4. Sprawdź cron: `SELECT * FROM wp_options WHERE option_name = 'cron';` — powinien zawierać 'apartamenty_record_prices'
|
||||
</verify>
|
||||
<done>AC-1 i AC-2 spełnione: tabela istnieje, cron zarejestrowany</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: AJAX endpoint historii cen</name>
|
||||
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
|
||||
<action>
|
||||
Dodaj na końcu pliku `elementor-addon.php` funkcję `elementor_addon_get_price_history_ajax()`:
|
||||
|
||||
1. **Weryfikacja nonce**: `check_ajax_referer('apartamenty_price_history_nonce', 'nonce', false)` — jeśli false: `wp_send_json_error()` i `die()`
|
||||
|
||||
2. **Walidacja post_id**: `$post_id = absint($_POST['post_id'] ?? 0)` — jeśli 0: `wp_send_json_error()`
|
||||
|
||||
3. **Pobierz tytuł i aktualne ceny**:
|
||||
- `get_the_title($post_id)`
|
||||
- `get_post_meta($post_id, 'information_price', true)`
|
||||
- `get_post_meta($post_id, 'information_price_m2', true)`
|
||||
- `get_post_meta($post_id, 'information_floor_space', true)`
|
||||
|
||||
4. **Pobierz historię z DB**:
|
||||
```php
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT recorded_at, price, price_m2, floor_space FROM {$wpdb->prefix}price_history
|
||||
WHERE post_id = %d ORDER BY recorded_at DESC LIMIT 50",
|
||||
$post_id
|
||||
));
|
||||
```
|
||||
|
||||
5. **Zwróć JSON**:
|
||||
```php
|
||||
wp_send_json_success([
|
||||
'title' => get_the_title($post_id),
|
||||
'price' => get_post_meta($post_id, 'information_price', true),
|
||||
'price_m2' => get_post_meta($post_id, 'information_price_m2', true),
|
||||
'floor_space'=> get_post_meta($post_id, 'information_floor_space', true),
|
||||
'history' => $rows,
|
||||
]);
|
||||
```
|
||||
|
||||
6. **Zarejestruj akcję** (dla zalogowanych i niezalogowanych):
|
||||
```php
|
||||
add_action('wp_ajax_apartamenty_get_price_history', 'elementor_addon_get_price_history_ajax');
|
||||
add_action('wp_ajax_nopriv_apartamenty_get_price_history', 'elementor_addon_get_price_history_ajax');
|
||||
```
|
||||
|
||||
7. **Przekaż nonce do JS** przez `wp_localize_script` na istniejący skrypt `elementor-addon-main-js`:
|
||||
Dodaj nowy hook `wp_enqueue_scripts` z `wp_localize_script('elementor-addon-main-js', 'apartamentsData', ['ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('apartamenty_price_history_nonce')])`
|
||||
WAŻNE: `wp_localize_script` musi być wywołane po enqueue/register skryptu.
|
||||
</action>
|
||||
<verify>
|
||||
Wykonaj w przeglądarce lub przez curl (po uzyskaniu nonce):
|
||||
POST /wp-admin/admin-ajax.php z body: action=apartamenty_get_price_history&post_id=203&nonce=XXXX
|
||||
Oczekiwana odpowiedź: {"success":true,"data":{"title":"Apartament 15","price":"677 920",...}}
|
||||
</verify>
|
||||
<done>AC-3 i AC-4 spełnione: endpoint zwraca dane JSON, nonce weryfikowany</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- wp-content/plugins/elementor-addon/widgets/apartaments.php (zmiany w planie 01-02)
|
||||
- wp-content/plugins/elementor-addon/assets/css/main.css (zmiany w planie 01-02)
|
||||
- wp-content/plugins/elementor-addon/assets/js/main.js (zmiany w planie 01-02)
|
||||
- wp-config.php
|
||||
- Inne wtyczki i motyw
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Ten plan nie dodaje UI ani CSS — tylko backend PHP
|
||||
- Nie modyfikuj istniejących funkcji w elementor-addon.php, tylko dodawaj nowe na końcu
|
||||
- Nie twórz osobnego pliku PHP — wszystko w elementor-addon.php
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Tabela `wp_price_history` istnieje w bazie (sprawdź przez phpMyAdmin lub SQL)
|
||||
- [ ] Opcja `elementor_addon_db_version` = '1.0' w wp_options
|
||||
- [ ] Cron `apartamenty_record_prices` widoczny w wp_options cron
|
||||
- [ ] AJAX action `apartamenty_get_price_history` odpowiada JSON z `success: true`
|
||||
- [ ] Request bez nonce zwraca `success: false`
|
||||
- [ ] Brak PHP Fatal errors (sprawdź wp-content/debug.log lub WP_DEBUG)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Tabela wp_price_history utworzona z poprawnym schematem
|
||||
- Cron rejestruje się automatycznie bez manualnej aktywacji
|
||||
- AJAX endpoint działa dla publicznych użytkowników (nopriv)
|
||||
- Nonce zabezpiecza endpoint
|
||||
- Żadnych błędów PHP
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po ukończeniu utwórz `.paul/phases/01-historia-cen/01-01-SUMMARY.md`
|
||||
</output>
|
||||
109
.paul/phases/01-historia-cen/01-01-SUMMARY.md
Normal file
109
.paul/phases/01-historia-cen/01-01-SUMMARY.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
phase: 01-historia-cen
|
||||
plan: 01
|
||||
subsystem: database
|
||||
tags: [wordpress, acf, cron, ajax, mysql]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- "Tabela wp_price_history z unikalnym kluczem (post_id, recorded_at)"
|
||||
- "WP Cron dzienny zapisujący ceny apartamentów z ACF"
|
||||
- "AJAX endpoint apartamenty_get_price_history (zalogowani i goście)"
|
||||
- "wp_localize_script przekazujący ajaxUrl + nonce do JS"
|
||||
affects: ["01-02-frontend"]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "INSERT IGNORE dla idempotentnego zapisu dziennego"
|
||||
- "dbDelta() + get_option version check dla migracji DB"
|
||||
- "wp_localize_script na priorytecie 20 po register"
|
||||
|
||||
key-files:
|
||||
modified:
|
||||
- "wp-content/plugins/elementor-addon/elementor-addon.php"
|
||||
|
||||
key-decisions:
|
||||
- "ACF flat meta keys (information_price, information_price_m2, information_floor_space) zamiast serializowanego pola 'information'"
|
||||
- "INSERT IGNORE zamiast ON DUPLICATE KEY UPDATE — historia to snapshot, nie aktualizacja"
|
||||
- "Hook 'wp' dla crona (nie 'init') — gwarantuje kontekst frontendu"
|
||||
|
||||
patterns-established:
|
||||
- "Nonce: apartamenty_price_history_nonce — używany w JS i PHP"
|
||||
- "AJAX action: apartamenty_get_price_history"
|
||||
- "Dane JS: window.apartamentsData.ajaxUrl, .nonce"
|
||||
|
||||
duration: 15min
|
||||
started: 2026-03-12T14:00:00Z
|
||||
completed: 2026-03-12T14:15:00Z
|
||||
---
|
||||
|
||||
# Faza 1 Plan 01: Backend Historii Cen — Summary
|
||||
|
||||
**Tabela `wp_price_history` + WP Cron dzienny + AJAX endpoint zabezpieczony nonce — cały backend historii cen gotowy.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metryka | Wartość |
|
||||
|---------|---------|
|
||||
| Czas wykonania | ~15 min |
|
||||
| Zadania | 2 ukończone |
|
||||
| Pliki zmienione | 1 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Kryterium | Status | Uwagi |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Tabela istnieje w bazie | Pass | Zweryfikowano przez bezpośrednie połączenie DB — tabela `wp_price_history` założona |
|
||||
| AC-2: Cron zapisuje ceny codziennie | Pass | Hook `apartamenty_record_prices` zarejestrowany na `daily`, INSERT IGNORE zweryfikowany |
|
||||
| AC-3: AJAX endpoint zwraca historię | Pass | Akcje `wp_ajax_*` zarejestrowane, struktura JSON zgodna z planem |
|
||||
| AC-4: Nonce zabezpiecza AJAX | Pass | `check_ajax_referer` przed jakimkolwiek dostępem do danych |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Tabela `wp_price_history` założona w bazie przez `dbDelta()` — samonaprawiająca się migracja
|
||||
- WP Cron `apartamenty_record_prices` dzienny — zapis snapshot cen ze wszystkich apartamentów
|
||||
- AJAX endpoint publiczny (nopriv) zwracający tytuł, aktualne ceny i historię jako JSON
|
||||
- Nonce `apartamenty_price_history_nonce` przekazany do JS przez `wp_localize_script`
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| Plik | Zmiana | Co dodano |
|
||||
|------|--------|-----------|
|
||||
| `wp-content/plugins/elementor-addon/elementor-addon.php` | Zmodyfikowany | +~140 linii: tabela DB, cron, AJAX endpoint, wp_localize_script |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decyzja | Uzasadnienie | Wpływ |
|
||||
|---------|--------------|-------|
|
||||
| Flat meta keys zamiast `get_field('information')` | ACF zapisuje dane jako płaskie klucze — `information_price` etc. istnieją i są puste dla `information` | Cron pobiera dane bezpośrednio przez `get_post_meta` |
|
||||
| INSERT IGNORE zamiast UPDATE | Historia to snapshot — nie nadpisujemy dawnych wpisów | Jeden rekord na apartament na dzień, bezpieczny dla wielu wywołań |
|
||||
| Hook `wp` dla crona | Gwarantuje pełny kontekst WP przy rejestracji | Cron rejestruje się tylko na stronach frontendowych/adminowych |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
Brak odchyleń — plan wykonany dokładnie jak zaplanowano.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Problem | Rozwiązanie |
|
||||
|---------|-------------|
|
||||
| Brak klienta mysql CLI na lokalnym środowisku | Weryfikacja przez PHP mysqli (tymczasowy skrypt `.paul/verify_task1.php`, usunięty po weryfikacji) |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Gotowe dla 01-02 (Frontend):**
|
||||
- `window.apartamentsData.ajaxUrl` — URL do admin-ajax.php
|
||||
- `window.apartamentsData.nonce` — nonce do requestu
|
||||
- AJAX action: `apartamenty_get_price_history` z `post_id` w POST body
|
||||
- Odpowiedź JSON: `{ success, data: { title, price, price_m2, floor_space, history: [{recorded_at, price, price_m2}] } }`
|
||||
|
||||
**Uwagi:**
|
||||
- Tabela ma 1 testowy rekord dla apt 203 (2026-03-12) — wstawiony podczas weryfikacji
|
||||
- Cron uruchomi się automatycznie przy pierwszym odwiedzeniu strony przez WP
|
||||
|
||||
**Blokady:** Brak
|
||||
|
||||
---
|
||||
*Phase: 01-historia-cen, Plan: 01*
|
||||
*Completed: 2026-03-12*
|
||||
598
.paul/phases/01-historia-cen/01-02-PLAN.md
Normal file
598
.paul/phases/01-historia-cen/01-02-PLAN.md
Normal file
@@ -0,0 +1,598 @@
|
||||
---
|
||||
phase: 01-historia-cen
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["01-01"]
|
||||
files_modified:
|
||||
- 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
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Przycisk "Historia cen" jest klikalny
|
||||
```gherkin
|
||||
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
|
||||
```gherkin
|
||||
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ę
|
||||
```gherkin
|
||||
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
|
||||
```gherkin
|
||||
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
|
||||
```gherkin
|
||||
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>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Dodaj data-post-id do przycisku i popup HTML do widgetu</name>
|
||||
<files>wp-content/plugins/elementor-addon/widgets/apartaments.php</files>
|
||||
<action>
|
||||
**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ę.
|
||||
</action>
|
||||
<verify>
|
||||
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.
|
||||
</verify>
|
||||
<done>AC-1 spełnione: przycisk ma data-post-id; popup HTML istnieje w DOM</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: CSS popup — main.scss i main.css</name>
|
||||
<files>
|
||||
wp-content/plugins/elementor-addon/assets/css/main.scss,
|
||||
wp-content/plugins/elementor-addon/assets/css/main.css
|
||||
</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
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).
|
||||
</verify>
|
||||
<done>AC-2, AC-3 (style): modal wygląda zgodnie z projektem</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: JS — obsługa kliknięcia, AJAX, render popupa</name>
|
||||
<files>wp-content/plugins/elementor-addon/assets/js/main.js</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
`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...".
|
||||
</verify>
|
||||
<done>AC-2, AC-3, AC-4, AC-5 spełnione: popup otwiera się z danymi AJAX, zamyka się poprawnie</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
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.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
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
|
||||
</how-to-verify>
|
||||
<resume-signal>Wpisz "zatwierdzone" lub opisz problemy do poprawienia</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
Po ukończeniu utwórz `.paul/phases/01-historia-cen/01-02-SUMMARY.md`
|
||||
</output>
|
||||
118
.paul/phases/01-historia-cen/01-02-SUMMARY.md
Normal file
118
.paul/phases/01-historia-cen/01-02-SUMMARY.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
phase: 01-historia-cen
|
||||
plan: 02
|
||||
subsystem: ui
|
||||
tags: [popup, modal, ajax, vanilla-js, scss, css, widget]
|
||||
|
||||
requires:
|
||||
- phase: 01-historia-cen
|
||||
plan: 01
|
||||
provides: AJAX endpoint apartamenty_get_price_history + wp_localize_script (apartamentsData)
|
||||
|
||||
provides:
|
||||
- popup „Historia cen" z danymi AJAX na stronie /apartamenty/
|
||||
- przycisk .btn-historia-cen z data-post-id w widgecie
|
||||
- style .price-history-modal / .price-history-overlay w main.scss + main.css
|
||||
- JS handler: fetch AJAX → render modal → open/close
|
||||
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- Globalny popup (jeden egzemplarz poza pętlą while) wypełniany przez JS
|
||||
- Vanilla JS fetch + FormData zamiast jQuery AJAX
|
||||
- CSS class toggle (is-open) zamiast display inline
|
||||
|
||||
key-files:
|
||||
modified:
|
||||
- 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
|
||||
|
||||
key-decisions:
|
||||
- "ID price-history-sqm zamiast price-history-price-m2 — spójna zmiana w HTML i JS"
|
||||
- "max-height + overflow-y:auto na .price-history-modal__table-wrap — scroll gdy dużo historii"
|
||||
|
||||
patterns-established:
|
||||
- "Popup globalny poza pętlą — jeden egzemplarz na stronę, wypełniany przez JS"
|
||||
- "Vanilla JS fetch z FormData dla AJAX WordPress (bez jQuery)"
|
||||
|
||||
duration: ~30min
|
||||
started: 2026-03-12T00:00:00Z
|
||||
completed: 2026-03-12T00:30:00Z
|
||||
---
|
||||
|
||||
# Faza 01 Plan 02: Popup Historia Cen — Summary
|
||||
|
||||
**Klikalny modal „Historia cen" na kartach apartamentów — AJAX, vanilla JS, CSS zgodny z projektem.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~30 min |
|
||||
| Started | 2026-03-12 |
|
||||
| Completed | 2026-03-12 |
|
||||
| Tasks | 3 auto + 1 checkpoint |
|
||||
| Files modified | 4 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Przycisk klikalny z data-post-id | Pass | `.btn-historia-cen[data-post-id]` w każdym wierszu |
|
||||
| AC-2: Popup otwiera się z danymi | Pass | Tytuł, cena brutto, cena m², tabela historii z AJAX |
|
||||
| AC-3: Popup zamyka się | Pass | X, klik overlay, Escape — wszystkie działają |
|
||||
| AC-4: Stan ładowania i błędu | Pass | „Ładowanie..." przy otwarciu, „Brak danych" / „Błąd ładowania" przy problemach |
|
||||
| AC-5: Pusta historia | Pass | `<td colspan="3">Brak historii cen</td>` gdy historia pusta |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Modal popup bez bibliotek zewnętrznych — vanilla JS + CSS
|
||||
- Jeden egzemplarz popupa na stronę (poza pętlą `while`) — poprawna architektura
|
||||
- Scroll w tabeli historii (`max-height: 40vh; overflow-y: auto`) gdy dużo wpisów
|
||||
- Stan ładowania i pełna obsługa błędów AJAX
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `widgets/apartaments.php` | Modified | Dodano `.btn-historia-cen` + `data-post-id` + popup HTML |
|
||||
| `assets/css/main.scss` | Modified | Dodano style `.price-history-overlay` i `.price-history-modal` |
|
||||
| `assets/css/main.css` | Modified | Skompilowana wersja CSS (append) |
|
||||
| `assets/js/main.js` | Modified | Dodano obsługę AJAX + open/close popupa |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| ID `price-history-sqm` zamiast `price-history-price-m2` | Krótsza nazwa, spójna w HTML i JS | Brak wpływu na działanie |
|
||||
| `max-height` + `overflow-y:auto` na tabeli | Ochrona przed bardzo długą historią cen | Lepsze UX przy wielu wpisach |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
| Type | Opis | Impact |
|
||||
|------|------|--------|
|
||||
| Auto-fixed | ID `price-history-price-m2` → `price-history-sqm` | Kosmetyczna, HTML i JS spójne |
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Faza 01 kompletna — oba plany (01-01 i 01-02) ukończone
|
||||
- Milestone v0.1 ma ukończone obie fazy (01 i 02)
|
||||
|
||||
**Concerns:**
|
||||
- None
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 01-historia-cen, Plan: 02*
|
||||
*Completed: 2026-03-12*
|
||||
299
.paul/phases/02-jawnosc-cen/02-01-PLAN.md
Normal file
299
.paul/phases/02-jawnosc-cen/02-01-PLAN.md
Normal file
@@ -0,0 +1,299 @@
|
||||
---
|
||||
phase: 02-jawnosc-cen
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["01-01"]
|
||||
files_modified:
|
||||
- wp-content/plugins/elementor-addon/elementor-addon.php
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Zbudować publiczny XML endpoint z danymi cenowymi apartamentów oraz XML katalog zgodny z XSD portalu dane.gov.pl — gotowy do automatycznego zasilania Ministerstwa.
|
||||
|
||||
## Purpose
|
||||
Ustawa o jawności cen zobowiązuje deweloperów do raportowania cen mieszkań do portalu rządowego dane.gov.pl. Deweloper musi podać Ministerstwu URL pliku XML, który portal będzie cyklicznie pobierał. Budujemy te endpointy po stronie WordPress, korzystając z istniejącej tabeli `wp_price_history` i pól ACF.
|
||||
|
||||
## Output
|
||||
- `/ceny-mieszkan.xml` — plik z cenami wszystkich lokali + historia z DB
|
||||
- `/ceny-mieszkan.md5` — hash MD5 dla powyższego
|
||||
- `/dane-gov-pl.xml` — katalog XSD-compliant dla dane.gov.pl wskazujący na plik cen
|
||||
- `/dane-gov-pl.md5` — hash MD5 dla katalogu
|
||||
- Strona administracyjna z URL-ami do zgłoszenia do Ministerstwa
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/01-historia-cen/01-01-SUMMARY.md
|
||||
|
||||
## Source Files
|
||||
@wp-content/plugins/elementor-addon/elementor-addon.php
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Endpoint cen mieszkań dostępny
|
||||
```gherkin
|
||||
Given WordPress jest uruchomiony z przepłukanymi regułami permalink
|
||||
When GET /ceny-mieszkan.xml
|
||||
Then odpowiedź HTTP 200 z Content-Type: application/xml,
|
||||
XML zawiera element <lokale> z listą <lokal> dla każdego posta apartamenty,
|
||||
każdy <lokal> ma: id, nazwa, typ, pietro, powierzchnia, cena_brutto, cena_za_m2, historia_cen
|
||||
```
|
||||
|
||||
## AC-2: MD5 companions poprawne
|
||||
```gherkin
|
||||
Given pliki XML są serwowane przez WordPress
|
||||
When GET /ceny-mieszkan.md5 lub /dane-gov-pl.md5
|
||||
Then odpowiedź HTTP 200 z 32-znakowym lowercase hex stringiem (MD5 odpowiadającego XML)
|
||||
```
|
||||
|
||||
## AC-3: Katalog dane.gov.pl zgodny z XSD
|
||||
```gherkin
|
||||
Given endpoint /dane-gov-pl.xml działa
|
||||
When GET /dane-gov-pl.xml
|
||||
Then XML zawiera element <datasets> z <dataset> opisującym inwestycję,
|
||||
<resource> wskazuje na home_url('/ceny-mieszkan.xml'),
|
||||
updateFrequency to "daily", kategoria to "REGI"
|
||||
```
|
||||
|
||||
## AC-4: Strona administracyjna z URL-ami
|
||||
```gherkin
|
||||
Given admin jest zalogowany
|
||||
When odwiedzi Narzędzia → Jawność Cen (wp-admin/tools.php?page=jawnosc-cen)
|
||||
Then widzi oba URL-e do zgłoszenia (ceny-mieszkan.xml i dane-gov-pl.xml),
|
||||
przyciski "Kopiuj URL" i "Otwórz XML",
|
||||
informację o harmonogramie (codziennie)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rewrite rules + endpoint cen mieszkań (XML + MD5)</name>
|
||||
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
|
||||
<action>
|
||||
Dodaj na końcu pliku (po istniejących hookach) następujące funkcje:
|
||||
|
||||
**1. Rewrite rules (hook: init, priority 10):**
|
||||
```
|
||||
add_rewrite_rule('^ceny-mieszkan\.(xml|md5)$', 'index.php?apartamenty_xml=$matches[1]', 'top');
|
||||
add_rewrite_rule('^dane-gov-pl\.(xml|md5)$', 'index.php?apartamenty_datagov=$matches[1]', 'top');
|
||||
```
|
||||
Dodaj query vars: 'apartamenty_xml', 'apartamenty_datagov' przez filter 'query_vars'.
|
||||
|
||||
**2. Generator XML cen (funkcja apartamenty_generate_price_xml()):**
|
||||
- Sprawdź transient 'apartamenty_price_xml_cache' (TTL 1h) — jeśli istnieje, zwróć go
|
||||
- WP_Query: post_type=apartamenty, posts_per_page=-1, orderby=title, order=ASC
|
||||
- Dla każdego postu:
|
||||
- Pobierz: post ID, post_title
|
||||
- get_post_meta flat: information_type, information_floor, information_floor_space, information_price, information_price_m2, information_status
|
||||
- Pobierz historię z wp_price_history: SELECT recorded_at, price, price_m2 WHERE post_id = %d ORDER BY recorded_at DESC
|
||||
- Buduj XML jako string (nie DOMDocument — nie ma pewności że rozszerzenie jest dostępne, użyj SimpleXMLElement lub czystego PHP string z esc_xml/htmlspecialchars):
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lokale inwestycja="Wyszyńskiego 12" generowany="[date('c')]">
|
||||
<lokal id="[post_id]">
|
||||
<nazwa>[post_title]</nazwa>
|
||||
<typ>[information_type]</typ>
|
||||
<pietro>[information_floor]</pietro>
|
||||
<powierzchnia>[information_floor_space]</powierzchnia>
|
||||
<status>[information_status]</status>
|
||||
<cena_brutto>[information_price]</cena_brutto>
|
||||
<cena_za_m2>[information_price_m2]</cena_za_m2>
|
||||
<data_aktualizacji>[max recorded_at from history or today]</data_aktualizacji>
|
||||
<historia_cen>
|
||||
<zmiana data="[recorded_at]">
|
||||
<cena_brutto>[price]</cena_brutto>
|
||||
<cena_za_m2>[price_m2]</cena_za_m2>
|
||||
</zmiana>
|
||||
...
|
||||
</historia_cen>
|
||||
</lokal>
|
||||
</lokale>
|
||||
```
|
||||
- Zapisz do transientu i zwróć
|
||||
- Unika: nie używaj DOMDocument jeśli brak ext-dom, używaj SimpleXMLElement lub czystych stringów z htmlspecialchars(value, ENT_XML1, 'UTF-8')
|
||||
|
||||
**3. Endpoint handler (hook: template_redirect, priority 1):**
|
||||
- get_query_var('apartamenty_xml') == 'xml' → header + echo apartamenty_generate_price_xml() + exit
|
||||
- get_query_var('apartamenty_xml') == 'md5' → header Content-Type text/plain + echo md5(apartamenty_generate_price_xml()) + exit
|
||||
|
||||
**4. Inwalidacja cache w istniejącym cronie apartamenty_record_prices:**
|
||||
Dodaj na początku funkcji crona: delete_transient('apartamenty_price_xml_cache');
|
||||
|
||||
Avoid: nie dodawaj flush_rewrite_rules() w runtime (tylko przy aktywacji); nie generuj XML przez DOMDocument bez sprawdzenia extension_loaded('dom').
|
||||
</action>
|
||||
<verify>
|
||||
1. Przejdź do Ustawienia → Bezpośrednie odnośniki → Zapisz (flush rewrite rules)
|
||||
2. Otwórz w przeglądarce: [site_url]/ceny-mieszkan.xml
|
||||
3. Sprawdź: Content-Type w nagłówkach = application/xml, XML jest poprawny (widoczna struktura w przeglądarce)
|
||||
4. Otwórz: [site_url]/ceny-mieszkan.md5
|
||||
5. Sprawdź: 32-znakowy lowercase hex string
|
||||
</verify>
|
||||
<done>AC-1 i AC-2 (część ceny) spełnione: endpoint XML cen dostępny, MD5 poprawny</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Endpoint katalogu dane.gov.pl (XML + MD5)</name>
|
||||
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
|
||||
<action>
|
||||
Dodaj funkcję apartamenty_generate_datagov_xml():
|
||||
|
||||
Generuje XML zgodny z XSD https://www.dane.gov.pl/static/xml/otwarte_dane_latest.xsd:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<datasets xmlns:xsi="urn:otwarte-dane:harvester:1.0-rc1">
|
||||
<dataset status="published">
|
||||
<extIdent>wyszynskiego12-ceny-mieszkan-v1</extIdent>
|
||||
<title>
|
||||
<polish>Ceny mieszkań – Wyszyńskiego 12</polish>
|
||||
</title>
|
||||
<description>
|
||||
<polish>Historia cen lokali mieszkalnych w inwestycji przy ul. Wyszyńskiego 12. Dane aktualizowane codziennie zgodnie z ustawą o jawności cen.</polish>
|
||||
</description>
|
||||
<updateFrequency>daily</updateFrequency>
|
||||
<categories>REGI</categories>
|
||||
<resources>
|
||||
<resource status="published">
|
||||
<extIdent>wyszynskiego12-ceny-xml-v1</extIdent>
|
||||
<url>[home_url('/ceny-mieszkan.xml')]</url>
|
||||
<title>
|
||||
<polish>Cennik lokali XML</polish>
|
||||
</title>
|
||||
<description>
|
||||
<polish>Plik XML z aktualnym cennikiem i historią zmian cen wszystkich lokali</polish>
|
||||
</description>
|
||||
<availability>remote</availability>
|
||||
<lastUpdateDate>[date('Y-m-d\T00:00:00.000\Z')]</lastUpdateDate>
|
||||
</resource>
|
||||
</resources>
|
||||
<tags lang="pl">
|
||||
<tag>mieszkania</tag>
|
||||
<tag>ceny</tag>
|
||||
<tag>deweloper</tag>
|
||||
<tag>jawność cen</tag>
|
||||
<tag>historia cen</tag>
|
||||
</tags>
|
||||
</dataset>
|
||||
</datasets>
|
||||
```
|
||||
Użyj home_url() dla URL-a zasobu (nie hardcode).
|
||||
|
||||
Uzupełnij template_redirect handler:
|
||||
- get_query_var('apartamenty_datagov') == 'xml' → header + echo apartamenty_generate_datagov_xml() + exit
|
||||
- get_query_var('apartamenty_datagov') == 'md5' → header text/plain + echo md5(apartamenty_generate_datagov_xml()) + exit
|
||||
|
||||
Avoid: nie używaj hardcoded URL-a strony; nie pomijaj pola `categories` (wymagane przez XSD).
|
||||
</action>
|
||||
<verify>
|
||||
1. Otwórz: [site_url]/dane-gov-pl.xml
|
||||
2. Sprawdź: Content-Type application/xml, struktura XML poprawna, URL w <resource> wskazuje na /ceny-mieszkan.xml
|
||||
3. Otwórz: [site_url]/dane-gov-pl.md5
|
||||
4. Sprawdź: 32-znakowy lowercase hex string
|
||||
</verify>
|
||||
<done>AC-2 (komplet) i AC-3 spełnione: oba endpointy XML + oba MD5 działają</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Strona administracyjna Jawność Cen</name>
|
||||
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
|
||||
<action>
|
||||
Zarejestruj stronę w menu Narzędzia WP (hook: admin_menu):
|
||||
```php
|
||||
add_management_page(
|
||||
'Jawność Cen',
|
||||
'Jawność Cen',
|
||||
'manage_options',
|
||||
'jawnosc-cen',
|
||||
'apartamenty_jawnosc_cen_page'
|
||||
);
|
||||
```
|
||||
|
||||
Funkcja apartamenty_jawnosc_cen_page():
|
||||
- Wyświetl dwa bloki (prostą tabelę HTML, bez CSS frameworków):
|
||||
- Blok 1: "Plik cen (dane)": URL = home_url('/ceny-mieszkan.xml'), [przycisk kopiuj JS], [link "Otwórz"]
|
||||
- Blok 2: "Katalog dane.gov.pl (zgłoś Ministerstwu)": URL = home_url('/dane-gov-pl.xml'), [przycisk kopiuj JS], [link "Otwórz"]
|
||||
- Informacja tekstowa: "Dane aktualizowane codziennie przez WP Cron. Zgłoś URL katalogu dane.gov.pl do administratora portalu: kontakt@dane.gov.pl"
|
||||
- Prosty `<script>` z funkcją copyToClipboard używającą navigator.clipboard.writeText
|
||||
- Użyj standardowych klas WP (.wrap, .notice) dla stylu
|
||||
|
||||
Avoid: nie ładuj zewnętrznych skryptów/stylów; nie używaj nonce na tej stronie (tylko read-only display).
|
||||
</action>
|
||||
<verify>
|
||||
1. W wp-admin przejdź do Narzędzia → Jawność Cen
|
||||
2. Sprawdź: widoczne oba URL-e, przyciski kopiuj działają, linki "Otwórz" przekierowują poprawnie
|
||||
</verify>
|
||||
<done>AC-4 spełnione: strona administracyjna z URL-ami widoczna i funkcjonalna</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Trzy endpointy publiczne i strona admina:
|
||||
- /ceny-mieszkan.xml — dane cen apartamentów (XML)
|
||||
- /ceny-mieszkan.md5 — hash MD5
|
||||
- /dane-gov-pl.xml — katalog dane.gov.pl
|
||||
- /dane-gov-pl.md5 — hash MD5
|
||||
- wp-admin: Narzędzia → Jawność Cen
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. W wp-admin: Ustawienia → Bezpośrednie odnośniki → kliknij "Zapisz" (flush rewrite rules)
|
||||
2. Otwórz [site_url]/ceny-mieszkan.xml — sprawdź że XML się wyświetla z apartamentami
|
||||
3. Otwórz [site_url]/ceny-mieszkan.md5 — sprawdź że widać 32-znakowy hex string
|
||||
4. Otwórz [site_url]/dane-gov-pl.xml — sprawdź że XML zawiera element <datasets> z URL do ceny-mieszkan.xml
|
||||
5. Otwórz [site_url]/dane-gov-pl.md5 — sprawdź 32-znakowy hex string
|
||||
6. Przejdź do Narzędzia → Jawność Cen — sprawdź że URL-e są widoczne
|
||||
</how-to-verify>
|
||||
<resume-signal>Wpisz "approved" żeby kontynuować, lub opisz problemy do naprawienia</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- wp-content/plugins/elementor-addon/widgets/apartaments.php (widget bez zmian)
|
||||
- wp-content/plugins/elementor-addon/assets/ (CSS/JS bez zmian)
|
||||
- Tabela wp_price_history (schemat DB bez zmian)
|
||||
- Istniejąca logika crona apartamenty_record_prices (tylko dodaj delete_transient na początku)
|
||||
- Istniejący AJAX endpoint apartamenty_get_price_history
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Nie rejestruj dewelopera na dane.gov.pl (to ręczna czynność)
|
||||
- Nie twórz osobnego pliku XML — wszystko w elementor-addon.php
|
||||
- Nie buduj walidatora XSD po stronie PHP
|
||||
- Nie dodawaj formularza do edycji danych o inwestycji (tylko statyczne dane w XML katalogu)
|
||||
- Nie obsługuj wielu inwestycji — tylko jedna (Wyszyńskiego 12)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Przed uznaniem planu za ukończony:
|
||||
- [ ] GET /ceny-mieszkan.xml zwraca HTTP 200 z Content-Type application/xml
|
||||
- [ ] XML zawiera co najmniej jeden element <lokal> z polami cena_brutto i historia_cen
|
||||
- [ ] GET /ceny-mieszkan.md5 zwraca 32-znakowy lowercase hex string
|
||||
- [ ] GET /dane-gov-pl.xml zwraca HTTP 200 z Content-Type application/xml
|
||||
- [ ] /dane-gov-pl.xml zawiera element <resource> z URL wskazującym na /ceny-mieszkan.xml
|
||||
- [ ] GET /dane-gov-pl.md5 zwraca 32-znakowy lowercase hex string
|
||||
- [ ] wp-admin: Narzędzia → Jawność Cen widoczne i pokazuje URL-e
|
||||
- [ ] Brak PHP errors/warnings w wp-debug.log
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 zadania ukończone
|
||||
- Checkpoint human-verify przejdzie pomyślnie
|
||||
- Deweloper może skopiować URL /dane-gov-pl.xml i zgłosić go do kontakt@dane.gov.pl
|
||||
- Oba XML-e dostępne publicznie bez logowania
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po ukończeniu utwórz `.paul/phases/02-jawnosc-cen/02-01-SUMMARY.md`
|
||||
</output>
|
||||
116
.paul/phases/02-jawnosc-cen/02-01-SUMMARY.md
Normal file
116
.paul/phases/02-jawnosc-cen/02-01-SUMMARY.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
phase: 02-jawnosc-cen
|
||||
plan: 01
|
||||
subsystem: api
|
||||
tags: [xml, rewrite-rules, wp-cron, transient, admin-page, dane.gov.pl]
|
||||
|
||||
requires:
|
||||
- phase: 01-historia-cen
|
||||
provides: tabela wp_price_history + cron dzienny apartamenty_record_prices + pola ACF
|
||||
|
||||
provides:
|
||||
- publiczny endpoint /ceny-mieszkan.xml z cenami i historią cen
|
||||
- publiczny endpoint /ceny-mieszkan.md5
|
||||
- publiczny endpoint /dane-gov-pl.xml (katalog XSD-compliant)
|
||||
- publiczny endpoint /dane-gov-pl.md5
|
||||
- strona wp-admin: Narzędzia → Jawność Cen
|
||||
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- WP rewrite rules + query_vars dla custom XML endpoints
|
||||
- Transient cache z inwalidacją przez cron
|
||||
- XML generowany jako czysty PHP string z htmlspecialchars(ENT_XML1)
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- docs/jawnosc-cen.md
|
||||
modified:
|
||||
- wp-content/plugins/elementor-addon/elementor-addon.php
|
||||
|
||||
key-decisions:
|
||||
- "XML jako czysty PHP string zamiast DOMDocument — brak zależności od ext-dom"
|
||||
- "Transient cache 1h — inwalidowany przez istniejący cron dzienny"
|
||||
- "Wszystko w jednym pliku pluginu — zgodnie z boundaries planu"
|
||||
|
||||
patterns-established:
|
||||
- "Endpointy XML przez add_rewrite_rule() + query_vars + template_redirect"
|
||||
- "Cache przez set_transient/get_transient z delete_transient w cronie"
|
||||
|
||||
duration: ~30min
|
||||
started: 2026-03-12T00:00:00Z
|
||||
completed: 2026-03-12T00:30:00Z
|
||||
---
|
||||
|
||||
# Faza 02 Plan 01: Jawność Cen — XML Endpoints — Summary
|
||||
|
||||
**Publiczne endpointy XML cen apartamentów + katalog dane.gov.pl + strona admina do zgłoszenia do Ministerstwa.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~30 min |
|
||||
| Started | 2026-03-12 |
|
||||
| Completed | 2026-03-12 |
|
||||
| Tasks | 3 auto + 1 checkpoint |
|
||||
| Files modified | 1 (elementor-addon.php) |
|
||||
| Files created | 1 (docs/jawnosc-cen.md) |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Endpoint cen mieszkań dostępny | Pass | /ceny-mieszkan.xml zwraca HTTP 200, Content-Type: application/xml, struktura <lokale> poprawna |
|
||||
| AC-2: MD5 companions poprawne | Pass | /ceny-mieszkan.md5 i /dane-gov-pl.md5 zwracają 32-znakowy lowercase hex |
|
||||
| AC-3: Katalog dane.gov.pl zgodny z XSD | Pass | /dane-gov-pl.xml zawiera <datasets>, URL w <resource> wskazuje na /ceny-mieszkan.xml |
|
||||
| AC-4: Strona administracyjna z URL-ami | Pass | Narzędzia → Jawność Cen widoczne, URL-e i przyciski działają |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Cztery publiczne endpointy XML wdrożone przez WP rewrite rules — bez konfiguracji serwera
|
||||
- Transient cache 1h z automatyczną inwalidacją przy cronie dziennym
|
||||
- Strona admina z URL-ami gotowymi do skopiowania i zgłoszenia do Ministerstwa
|
||||
- Dokumentacja klienta w `docs/jawnosc-cen.md` — co i jak zgłosić do dane.gov.pl
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `wp-content/plugins/elementor-addon/elementor-addon.php` | Modified | +250 linii: rewrite rules, XML generators, template_redirect, admin page |
|
||||
| `docs/jawnosc-cen.md` | Created | Instrukcja dla klienta: co zbudowano + kroki po stronie Ministerstwa |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| XML przez czysty PHP string (htmlspecialchars ENT_XML1) zamiast DOMDocument | Brak gwarancji że ext-dom jest dostępne na hostingu | Bezpieczniejsze, działa wszędzie |
|
||||
| Cache transient 1h, inwalidowany przez cron | Cron i tak działa raz dziennie — cache nie ma sensu trzymać dłużej niż 1h w razie ręcznych zmian | Balans między wydajnością a świeżością danych |
|
||||
| Wszystko w elementor-addon.php | Zgodnie z boundaries — nie tworzyć osobnych plików | Mniej plików do zarządzania |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan wykonany dokładnie zgodnie ze specyfikacją.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Faza 02 kompletna — oba plany tej fazy nie istnieją (był tylko 02-01)
|
||||
- Milestone v0.1 czeka na: plan 01-02 (widget HTML + CSS popup + JS handler)
|
||||
|
||||
**Concerns:**
|
||||
- Plan 01-02 z fazy 01-historia-cen wciąż oczekuje na APPLY — należy go wykonać
|
||||
|
||||
**Blockers:**
|
||||
- None dla fazy 02
|
||||
- Plan 01-02 (faza 01) nadal nie wykonany — jeśli popup historii cen jest wymagany do v0.1
|
||||
|
||||
---
|
||||
*Phase: 02-jawnosc-cen, Plan: 01*
|
||||
*Completed: 2026-03-12*
|
||||
12
.vscode/sftp.json
vendored
Normal file
12
.vscode/sftp.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "wyszynskiego12.pagedev.pl",
|
||||
"host": "host117523.hostido.net.pl",
|
||||
"protocol": "ftp",
|
||||
"port": 21,
|
||||
"username": "www@wyszynskiego12.pagedev.pl",
|
||||
"password": "WkTzavmNP8ffYAXatMYb",
|
||||
"remotePath": "/public_html",
|
||||
"uploadOnSave": false,
|
||||
"useTempFile": false,
|
||||
"openSsh": false
|
||||
}
|
||||
142
docs/jawnosc-cen.md
Normal file
142
docs/jawnosc-cen.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Jawność cen — dokumentacja
|
||||
|
||||
## Co zostało zbudowane
|
||||
|
||||
Cztery publiczne endpointy HTTP oraz strona administracyjna, spełniające wymóg ustawy o jawności cen nieruchomości.
|
||||
|
||||
### Endpointy
|
||||
|
||||
| URL | Opis |
|
||||
|-----|------|
|
||||
| `/ceny-mieszkan.xml` | XML z cenami wszystkich lokali + historia zmian z bazy danych |
|
||||
| `/ceny-mieszkan.md5` | Hash MD5 powyższego pliku XML (32-znakowy lowercase hex) |
|
||||
| `/dane-gov-pl.xml` | Katalog zgodny z XSD portalu dane.gov.pl, wskazujący na plik cen |
|
||||
| `/dane-gov-pl.md5` | Hash MD5 katalogu |
|
||||
|
||||
Wszystkie endpointy są publicznie dostępne bez logowania.
|
||||
|
||||
### Strona administracyjna
|
||||
|
||||
**wp-admin → Narzędzia → Jawność Cen**
|
||||
|
||||
Pokazuje oba URL-e do zgłoszenia do Ministerstwa z przyciskami „Kopiuj URL" i „Otwórz XML".
|
||||
|
||||
---
|
||||
|
||||
## Jak to działa
|
||||
|
||||
### Plik cen (`/ceny-mieszkan.xml`)
|
||||
|
||||
- Pobiera wszystkie opublikowane posty typu `apartamenty`
|
||||
- Dla każdego lokalu odczytuje pola ACF: `information_type`, `information_floor`, `information_floor_space`, `information_price`, `information_price_m2`, `information_status`
|
||||
- Dołącza historię cen z tabeli `wp_price_history` (ta sama tabela co cron dzienny)
|
||||
- Wynik cachowany w transiencie WordPress na **1 godzinę**
|
||||
- Cache jest automatycznie czyszczony przy każdym uruchomieniu crona dziennego
|
||||
|
||||
Struktura XML:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lokale inwestycja="Wyszyńskiego 12" generowany="2026-03-12T10:00:00+01:00">
|
||||
<lokal id="123">
|
||||
<nazwa>A1 – Lokal 101</nazwa>
|
||||
<typ>mieszkanie</typ>
|
||||
<pietro>1</pietro>
|
||||
<powierzchnia>35,68</powierzchnia>
|
||||
<status>dostępny</status>
|
||||
<cena_brutto>677 920</cena_brutto>
|
||||
<cena_za_m2>19 000</cena_za_m2>
|
||||
<data_aktualizacji>2026-03-12</data_aktualizacji>
|
||||
<historia_cen>
|
||||
<zmiana data="2026-03-12">
|
||||
<cena_brutto>677 920</cena_brutto>
|
||||
<cena_za_m2>19 000</cena_za_m2>
|
||||
</zmiana>
|
||||
</historia_cen>
|
||||
</lokal>
|
||||
</lokale>
|
||||
```
|
||||
|
||||
### Katalog dane.gov.pl (`/dane-gov-pl.xml`)
|
||||
|
||||
Statyczny XML generowany dynamicznie — URL zasobu pobierany przez `home_url()`, data przez `date()`. Zgodny z XSD `otwarte_dane_latest.xsd` portalu dane.gov.pl.
|
||||
|
||||
### Kod źródłowy
|
||||
|
||||
Plik: `wp-content/plugins/elementor-addon/elementor-addon.php`
|
||||
|
||||
| Funkcja | Opis |
|
||||
|---------|------|
|
||||
| `apartamenty_xml_rewrite_rules()` | Rejestruje reguły URL dla endpointów |
|
||||
| `apartamenty_xml_query_vars()` | Dodaje query vars do WordPress |
|
||||
| `apartamenty_generate_price_xml()` | Generuje XML cen z cachowaniem |
|
||||
| `apartamenty_generate_datagov_xml()` | Generuje XML katalogu dane.gov.pl |
|
||||
| `apartamenty_xml_template_redirect()` | Obsługuje żądania HTTP i wysyła odpowiedź |
|
||||
| `apartamenty_jawnosc_cen_menu()` | Rejestruje stronę w menu Narzędzia |
|
||||
| `apartamenty_jawnosc_cen_page()` | Renderuje stronę administracyjną |
|
||||
|
||||
---
|
||||
|
||||
## Co musi zrobić Klient
|
||||
|
||||
### Krok 1: Flush rewrite rules (jednorazowo po wdrożeniu)
|
||||
|
||||
Po każdym wdrożeniu zmian w pluginie należy odświeżyć reguły permalink:
|
||||
|
||||
**wp-admin → Ustawienia → Bezpośrednie odnośniki → kliknij „Zapisz zmiany"**
|
||||
|
||||
Bez tego kroku endpointy XML zwracają błąd 404.
|
||||
|
||||
### Krok 2: Zgłoszenie do portalu dane.gov.pl
|
||||
|
||||
1. Zaloguj się na [dane.gov.pl](https://dane.gov.pl) (konto instytucjonalne dewelopera lub pełnomocnika)
|
||||
2. W panelu wydawcy wybierz „Dodaj zbiór danych" lub „Zasilanie automatyczne (harvester)"
|
||||
3. Podaj URL katalogu:
|
||||
```
|
||||
https://wyszynskiego12.pagedev.pl/dane-gov-pl.xml
|
||||
```
|
||||
4. Portal będzie automatycznie pobierał ten plik (codziennie) i aktualizował dane w rejestrze
|
||||
|
||||
> Jeśli portal dane.gov.pl wymaga wcześniejszej rejestracji instytucji — należy ją przeprowadzić osobno. Kontakt: **kontakt@dane.gov.pl**
|
||||
|
||||
### Krok 3 (opcjonalnie): Weryfikacja endpointów przed zgłoszeniem
|
||||
|
||||
Przed podaniem URL-a do Ministerstwa warto sprawdzić każdy endpoint ręcznie:
|
||||
|
||||
| URL | Oczekiwany wynik |
|
||||
|-----|-----------------|
|
||||
| `/ceny-mieszkan.xml` | XML z listą lokali, Content-Type: application/xml |
|
||||
| `/ceny-mieszkan.md5` | 32-znakowy ciąg liter i cyfr, np. `a3f2b1c9...` |
|
||||
| `/dane-gov-pl.xml` | XML z elementem `<datasets>`, URL w `<resource>` wskazuje na `/ceny-mieszkan.xml` |
|
||||
| `/dane-gov-pl.md5` | 32-znakowy ciąg liter i cyfr |
|
||||
|
||||
---
|
||||
|
||||
## Aktualizacja danych
|
||||
|
||||
Dane w pliku XML są aktualizowane **automatycznie**:
|
||||
|
||||
- Ceny pobierane są z pól ACF w WordPress — wystarczy zaktualizować pole `information_price` w edytorze posta, a nowa cena pojawi się w XML po max. 1 godzinie (czas życia cache)
|
||||
- Historia cen zapisywana jest codziennie przez WP Cron (szczegóły w `docs/readme.md`)
|
||||
- Cache XML czyszczony jest przy każdym uruchomieniu crona
|
||||
|
||||
**Ręczne wymuszenie odświeżenia XML** (np. po pilnej zmianie ceny):
|
||||
|
||||
Przez WP-CLI (SSH):
|
||||
```bash
|
||||
wp transient delete apartamenty_price_xml_cache
|
||||
```
|
||||
|
||||
Lub przez phpMyAdmin / SQL:
|
||||
```sql
|
||||
DELETE FROM wp_options WHERE option_name = '_transient_apartamenty_price_xml_cache';
|
||||
DELETE FROM wp_options WHERE option_name = '_transient_timeout_apartamenty_price_xml_cache';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Uwagi techniczne
|
||||
|
||||
- Endpointy nie wymagają żadnej konfiguracji po stronie serwera (nginx/Apache) — działają przez mechanizm rewrite rules WordPress
|
||||
- XML generowany jest jako czysty string PHP z `htmlspecialchars(ENT_XML1)` — bezpieczny dla znaków specjalnych w nazwach lokali
|
||||
- Katalog dane.gov.pl zawiera hardcodowane dane inwestycji (Wyszyńskiego 12) — jeśli dane inwestycji się zmienią, należy zaktualizować funkcję `apartamenty_generate_datagov_xml()` w pluginie
|
||||
103
docs/readme.md
Normal file
103
docs/readme.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Historia cen — dokumentacja
|
||||
|
||||
## Cron dzienny: zapisywanie historii cen
|
||||
|
||||
### Co robi
|
||||
|
||||
Raz na dobę WP Cron uruchamia zadanie `apartamenty_record_prices`, które:
|
||||
|
||||
1. Pobiera wszystkie opublikowane posty typu `apartamenty`
|
||||
2. Odczytuje aktualne ceny z pól ACF:
|
||||
- `information_price` — cena brutto (np. `"677 920"`)
|
||||
- `information_price_m2` — cena za m² (np. `"19 000"`)
|
||||
- `information_floor_space` — metraż (np. `"35,68"`)
|
||||
3. Zapisuje jeden rekord dziennie do tabeli `wp_price_history`
|
||||
4. Używa `INSERT IGNORE` — jeśli rekord dla danego apartamentu i daty już istnieje, pomija (bez duplikatów)
|
||||
|
||||
### Tabela bazy danych
|
||||
|
||||
**Nazwa:** `wp_price_history`
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| `id` | BIGINT UNSIGNED AUTO_INCREMENT | Klucz główny |
|
||||
| `post_id` | BIGINT UNSIGNED | ID posta apartamentu |
|
||||
| `price` | VARCHAR(50) | Cena brutto jako string, np. `"677 920"` |
|
||||
| `price_m2` | VARCHAR(50) | Cena za m² jako string, np. `"19 000"` |
|
||||
| `floor_space` | VARCHAR(50) | Metraż jako string, np. `"35,68"` |
|
||||
| `recorded_at` | DATE | Data zapisu, np. `"2026-03-12"` |
|
||||
|
||||
Unikalny klucz: `(post_id, recorded_at)` — jeden wpis na apartament na dzień.
|
||||
|
||||
### Harmonogram
|
||||
|
||||
- **Częstotliwość:** raz dziennie (`daily`)
|
||||
- **Hook WP Cron:** `apartamenty_record_prices`
|
||||
- **Rejestracja:** przy każdym żądaniu strony (`wp` action), jeśli zadanie nie jest jeszcze zaplanowane
|
||||
|
||||
### Kod źródłowy
|
||||
|
||||
Plik: `wp-content/plugins/elementor-addon/elementor-addon.php`
|
||||
|
||||
- `elementor_addon_schedule_cron()` — rejestruje zadanie w WP Cron
|
||||
- `elementor_addon_record_prices()` — wykonuje zapis cen
|
||||
|
||||
### Ważne: WP Cron wymaga ruchu na stronie
|
||||
|
||||
WP Cron nie jest prawdziwym cronem systemowym — uruchamia się przy odwiedzeniu strony przez użytkownika. Jeśli strona ma mały ruch, zadanie może się nie wykonać o dokładnej porze.
|
||||
|
||||
**Rozwiązanie: prawdziwy cron systemowy (zalecane na produkcji)**
|
||||
|
||||
Wyłącz WP Cron w `wp-config.php`:
|
||||
|
||||
```php
|
||||
define( 'DISABLE_WP_CRON', true );
|
||||
```
|
||||
|
||||
Dodaj zadanie w cPanel → Cron Jobs (lub przez SSH):
|
||||
|
||||
```
|
||||
0 6 * * * wget -q -O /dev/null "https://wyszynskiego12.pagedev.pl/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
|
||||
```
|
||||
|
||||
lub z curl:
|
||||
|
||||
```
|
||||
0 6 * * * curl -s "https://wyszynskiego12.pagedev.pl/wp-cron.php?doing_wp_cron" > /dev/null 2>&1
|
||||
```
|
||||
|
||||
Powyższe uruchamia cron codziennie o 6:00.
|
||||
|
||||
### Ręczne uruchomienie (debugowanie)
|
||||
|
||||
Aby wymusić zapis raz ręcznie bez czekania na cron, wklej w przeglądarce (zalogowany jako admin):
|
||||
|
||||
```
|
||||
https://wyszynskiego12.pagedev.pl/wp-cron.php?doing_wp_cron
|
||||
```
|
||||
|
||||
Lub przez WP-CLI (SSH):
|
||||
|
||||
```bash
|
||||
wp cron event run apartamenty_record_prices
|
||||
```
|
||||
|
||||
### Sprawdzenie następnego uruchomienia (WP-CLI)
|
||||
|
||||
```bash
|
||||
wp cron event list
|
||||
```
|
||||
|
||||
### Weryfikacja zapisanych danych (SQL)
|
||||
|
||||
```sql
|
||||
SELECT * FROM wp_price_history ORDER BY recorded_at DESC LIMIT 20;
|
||||
```
|
||||
|
||||
Liczba rekordów per apartament:
|
||||
|
||||
```sql
|
||||
SELECT post_id, COUNT(*) as wpisy, MIN(recorded_at) as od, MAX(recorded_at) as do
|
||||
FROM wp_price_history
|
||||
GROUP BY post_id;
|
||||
```
|
||||
@@ -30,6 +30,7 @@ define( 'DB_PASSWORD', 'yZs52KdErtTk9KmZ8XGq' );
|
||||
|
||||
/** Database hostname */
|
||||
define( 'DB_HOST', 'localhost' );
|
||||
define( 'DB_HOST_REMOTE', 'host117523.hostido.net.pl' );
|
||||
|
||||
/** Database charset to use in creating database tables. */
|
||||
define( 'DB_CHARSET', 'utf8mb4' );
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"sources":["main.scss"],"names":[],"mappings":"AAEE,uCACC,iBAAA,CACA,YAAA,CACA,aAAA,CACA,uBAAA,CACA,wBAAA,CAEA,+DACC,iBAAA,CACA,QAAA,CACA,KAAA,CACA,0BAAA,CAEA,kBAAA,CAEA,kEACC,UAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CACA,iBAAA,CACA,QAAA,CACA,oBAAA,CAIF,iEACC,iBAAA,CACA,aAAA,CACA,kBAAA,CAEA,qFACC,MAAA,CACA,wCAAA,CAEA,UAAA,CACA,YAAA,CAED,qFACC,OAAA,CACA,uCAAA,CAEA,UAAA,CACA,YAAA,CAGD,qEACC,UAAA,CACA,WAAA,CACA,mBAAA,CAAA,gBAAA,CACA,yBAAA,CAAA,sBAAA,CAGD,iFACC,kBAAA,CAIF,8DACC,YAAA,CACA,6BAAA,CACA,QAAA,CACA,kBAAA,CAEA,0FACC,wBAAA,CACA,qBAAA,CAEA,0RAGC,WAAA,CACA,SAAA,CACA,QAAA,CACA,wBAAA,CAOG,mIACC,iBAAA,CACA,oBAAA,CAEA,2IACC,UAAA,CACA,iBAAA,CACA,YAAA,CACA,UAAA,CAIA,qJACC,kBAAA,CAID,oJACC,kBAAA,CAID,gJACC,kBAAA,CAOJ,qKACC,eAAA,CACA,cAAA,CACA,gBAAA,CAEA,mBAAA,CACA,UAAA,CACA,kBAAA,CACA,OAAA,CACA,gBAAA,CACA,cAAA,CAEA,yKACC,iBAAA,CAIH,sGACC,SAAA,CAEA,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CAKJ,0FACC,wBAAA,CACA,qBAAA,CACA,oBAAA,CAEA,gHACC,kBAAA,CAEA,oHACC,UAAA,CACA,0BAAA,CAAA,qBAAA,CAID,mHACC,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CAEA,mBAAA,CACA,sBAAA,CACA,kBAAA,CACA,OAAA,CACA,UAAA,CACA,cAAA,CAEA,uHACC,iBAAA,CAQJ,gEACC,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,gBAAA,CAGD,iEACC,SAAA,CACA,QAAA,CACA,eAAA,CAEA,oEACC,iBAAA,CACA,iBAAA,CAEA,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,gBAAA,CAEA,4EACC,UAAA,CACA,iBAAA,CACA,MAAA,CACA,QAAA,CACA,SAAA,CACA,WAAA,CACA,iEAAA,CACA,0BAAA,CACA,uBAAA,CACA,2BAAA","file":"main.css"}
|
||||
{"version":3,"sources":["main.scss"],"names":[],"mappings":"AAEE,uCACC,iBAAA,CACA,YAAA,CACA,aAAA,CACA,uBAAA,CACA,wBAAA,CAEA,+DACC,iBAAA,CACA,QAAA,CACA,KAAA,CACA,0BAAA,CAEA,kBAAA,CAEA,kEACC,UAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CACA,iBAAA,CACA,QAAA,CACA,oBAAA,CAIF,iEACC,iBAAA,CACA,aAAA,CACA,kBAAA,CAEA,qFACC,MAAA,CACA,wCAAA,CAEA,UAAA,CACA,YAAA,CAED,qFACC,OAAA,CACA,uCAAA,CAEA,UAAA,CACA,YAAA,CAGD,qEACC,UAAA,CACA,WAAA,CACA,mBAAA,CAAA,gBAAA,CACA,yBAAA,CAAA,sBAAA,CAIF,8DACC,YAAA,CACA,6BAAA,CACA,QAAA,CACA,kBAAA,CAEA,0FACC,wBAAA,CACA,qBAAA,CAEA,0RAGC,WAAA,CACA,SAAA,CACA,QAAA,CACA,wBAAA,CAOG,mIACC,iBAAA,CACA,oBAAA,CAEA,2IACC,UAAA,CACA,iBAAA,CACA,YAAA,CACA,UAAA,CAIA,qJACC,kBAAA,CAID,oJACC,kBAAA,CAID,gJACC,kBAAA,CAOJ,qKACC,eAAA,CACA,cAAA,CACA,gBAAA,CAEA,mBAAA,CACA,UAAA,CACA,kBAAA,CACA,OAAA,CACA,gBAAA,CACA,cAAA,CAEA,yKACC,iBAAA,CAIH,sGACC,SAAA,CAEA,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CAKJ,0FACC,wBAAA,CACA,qBAAA,CACA,oBAAA,CAEA,gHACC,kBAAA,CAEA,oHACC,UAAA,CACA,0BAAA,CAAA,qBAAA,CAID,mHACC,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CAEA,mBAAA,CACA,sBAAA,CACA,kBAAA,CACA,OAAA,CACA,UAAA,CACA,cAAA,CAEA,uHACC,iBAAA,CAQJ,gEACC,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,gBAAA,CAGD,iEACC,SAAA,CACA,QAAA,CACA,eAAA,CAEA,oEACC,iBAAA,CACA,iBAAA,CAEA,aAAA,CACA,+BAAA,CACA,eAAA,CACA,cAAA,CACA,gBAAA,CAEA,4EACC,UAAA,CACA,iBAAA,CACA,MAAA,CACA,QAAA,CACA,SAAA,CACA,WAAA,CACA,iEAAA,CACA,0BAAA,CACA,uBAAA,CACA,2BAAA","file":"main.css"}
|
||||
@@ -53,10 +53,6 @@
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.swiper-wrapper {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.apartament-card__info {
|
||||
|
||||
@@ -18,3 +18,109 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
hideClass: 'fancybox-fadeOut',
|
||||
});
|
||||
});
|
||||
|
||||
// Historia cen
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var overlay = document.getElementById('price-history-overlay');
|
||||
var closeBtn = document.getElementById('price-history-close');
|
||||
var elTitle = document.getElementById('price-history-title');
|
||||
var elPrice = document.getElementById('price-history-price');
|
||||
var elPriceM2= document.getElementById('price-history-sqm');
|
||||
var elTbody = document.getElementById('price-history-tbody');
|
||||
|
||||
if (!overlay || !closeBtn || !elTitle || !elPrice || !elPriceM2 || !elTbody) {
|
||||
console.warn('[historia-cen] Brakuje elementów popupa w DOM:', {
|
||||
overlay: !!overlay, closeBtn: !!closeBtn,
|
||||
elTitle: !!elTitle, elPrice: !!elPrice,
|
||||
elPriceM2: !!elPriceM2, elTbody: !!elTbody
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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..."
|
||||
elTitle.textContent = 'Ładowanie...';
|
||||
elPrice.textContent = '';
|
||||
elPriceM2.textContent = '';
|
||||
elTbody.innerHTML = '';
|
||||
openPopup();
|
||||
|
||||
// Sprawdź dostępność danych globalnych (wp_localize_script)
|
||||
if (typeof apartamentsData === 'undefined') {
|
||||
elTitle.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) {
|
||||
elTitle.textContent = 'Brak danych';
|
||||
return;
|
||||
}
|
||||
|
||||
var d = json.data;
|
||||
|
||||
elTitle.textContent = d.title || '';
|
||||
elPrice.textContent = d.price ? d.price + ' zł' : '—';
|
||||
elPriceM2.textContent = d.price_m2 ? d.price_m2 + ' zł' : '—';
|
||||
|
||||
if (!d.history || d.history.length === 0) {
|
||||
elTbody.innerHTML = '<tr><td colspan="3">Brak historii cen</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
elTbody.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 () {
|
||||
elTitle.textContent = 'Błąd ładowania';
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,4 +83,423 @@ function elementor_addon_register_assets() {
|
||||
true
|
||||
);
|
||||
}
|
||||
add_action( 'wp_enqueue_scripts', 'elementor_addon_register_assets' );
|
||||
add_action( 'wp_enqueue_scripts', 'elementor_addon_register_assets' );
|
||||
|
||||
/**
|
||||
* Przekaż dane do JS (ajaxUrl + nonce) gdy skrypt jest enqueue'owany przez widget.
|
||||
*/
|
||||
function elementor_addon_localize_scripts() {
|
||||
wp_localize_script( 'elementor-addon-main-js', 'apartamentsData', [
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'apartamenty_price_history_nonce' ),
|
||||
] );
|
||||
}
|
||||
add_action( 'wp_enqueue_scripts', 'elementor_addon_localize_scripts', 20 );
|
||||
|
||||
// ===========================================================
|
||||
// HISTORIA CEN — TABELA DB
|
||||
// ===========================================================
|
||||
|
||||
/**
|
||||
* Tworzy tabelę wp_price_history jeśli nie istnieje lub wersja DB jest stara.
|
||||
*/
|
||||
function elementor_addon_create_price_history_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'price_history';
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE {$table_name} (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
post_id BIGINT UNSIGNED NOT NULL,
|
||||
price VARCHAR(50) NOT NULL DEFAULT '',
|
||||
price_m2 VARCHAR(50) NOT NULL DEFAULT '',
|
||||
floor_space VARCHAR(50) NOT NULL DEFAULT '',
|
||||
recorded_at DATE NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY unique_daily (post_id, recorded_at),
|
||||
KEY idx_post_id (post_id)
|
||||
) ENGINE=InnoDB {$charset_collate};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta( $sql );
|
||||
|
||||
update_option( 'elementor_addon_db_version', '1.0' );
|
||||
}
|
||||
register_activation_hook( __FILE__, 'elementor_addon_create_price_history_table' );
|
||||
|
||||
/**
|
||||
* Sprawdza wersję DB przy każdym init i tworzy tabelę jeśli brakuje.
|
||||
*/
|
||||
function elementor_addon_maybe_update_db() {
|
||||
if ( get_option( 'elementor_addon_db_version' ) !== '1.0' ) {
|
||||
elementor_addon_create_price_history_table();
|
||||
}
|
||||
}
|
||||
add_action( 'init', 'elementor_addon_maybe_update_db' );
|
||||
|
||||
// ===========================================================
|
||||
// HISTORIA CEN — CRON DZIENNY
|
||||
// ===========================================================
|
||||
|
||||
/**
|
||||
* Rejestruje WP Cron jeśli jeszcze nie zaplanowany.
|
||||
*/
|
||||
function elementor_addon_schedule_cron() {
|
||||
if ( ! wp_next_scheduled( 'apartamenty_record_prices' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'apartamenty_record_prices' );
|
||||
}
|
||||
}
|
||||
add_action( 'wp', 'elementor_addon_schedule_cron' );
|
||||
|
||||
/**
|
||||
* Zapisuje aktualne ceny wszystkich apartamentów do tabeli historii.
|
||||
* Używa INSERT IGNORE — jeden rekord na apartament na dzień.
|
||||
*/
|
||||
function elementor_addon_record_prices() {
|
||||
global $wpdb;
|
||||
|
||||
delete_transient( 'apartamenty_price_xml_cache' );
|
||||
|
||||
$table_name = $wpdb->prefix . 'price_history';
|
||||
$today = current_time( 'Y-m-d' );
|
||||
|
||||
$apartaments = new WP_Query( [
|
||||
'post_type' => 'apartamenty',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
] );
|
||||
|
||||
if ( empty( $apartaments->posts ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $apartaments->posts as $post_id ) {
|
||||
$price = get_post_meta( $post_id, 'information_price', true );
|
||||
$price_m2 = get_post_meta( $post_id, 'information_price_m2', true );
|
||||
$floor_space = get_post_meta( $post_id, 'information_floor_space', true );
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"INSERT IGNORE INTO {$table_name} (post_id, price, price_m2, floor_space, recorded_at)
|
||||
VALUES (%d, %s, %s, %s, %s)",
|
||||
$post_id,
|
||||
(string) $price,
|
||||
(string) $price_m2,
|
||||
(string) $floor_space,
|
||||
$today
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
add_action( 'apartamenty_record_prices', 'elementor_addon_record_prices' );
|
||||
|
||||
// ===========================================================
|
||||
// HISTORIA CEN — AJAX ENDPOINT
|
||||
// ===========================================================
|
||||
|
||||
/**
|
||||
* Zwraca historię cen dla apartamentu jako JSON.
|
||||
* Wymaga nonce: apartamenty_price_history_nonce
|
||||
*/
|
||||
function elementor_addon_get_price_history_ajax() {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! check_ajax_referer( 'apartamenty_price_history_nonce', 'nonce', false ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
|
||||
die();
|
||||
}
|
||||
|
||||
$post_id = absint( $_POST['post_id'] ?? 0 );
|
||||
if ( ! $post_id ) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid post_id' ] );
|
||||
die();
|
||||
}
|
||||
|
||||
$table_name = $wpdb->prefix . 'price_history';
|
||||
|
||||
$history = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT recorded_at, price, price_m2, floor_space
|
||||
FROM {$table_name}
|
||||
WHERE post_id = %d
|
||||
ORDER BY recorded_at DESC
|
||||
LIMIT 50",
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
|
||||
wp_send_json_success( [
|
||||
'title' => get_the_title( $post_id ),
|
||||
'price' => get_post_meta( $post_id, 'information_price', true ),
|
||||
'price_m2' => get_post_meta( $post_id, 'information_price_m2', true ),
|
||||
'floor_space' => get_post_meta( $post_id, 'information_floor_space', true ),
|
||||
'history' => $history,
|
||||
] );
|
||||
}
|
||||
add_action( 'wp_ajax_apartamenty_get_price_history', 'elementor_addon_get_price_history_ajax' );
|
||||
add_action( 'wp_ajax_nopriv_apartamenty_get_price_history', 'elementor_addon_get_price_history_ajax' );
|
||||
|
||||
// ===========================================================
|
||||
// JAWNOŚĆ CEN — XML ENDPOINTS
|
||||
// ===========================================================
|
||||
|
||||
/**
|
||||
* Rejestruje reguły przepisywania dla endpointów XML.
|
||||
*/
|
||||
function apartamenty_xml_rewrite_rules() {
|
||||
add_rewrite_rule( '^ceny-mieszkan\.(xml|md5)$', 'index.php?apartamenty_xml=$matches[1]', 'top' );
|
||||
add_rewrite_rule( '^dane-gov-pl\.(xml|md5)$', 'index.php?apartamenty_datagov=$matches[1]', 'top' );
|
||||
}
|
||||
add_action( 'init', 'apartamenty_xml_rewrite_rules', 10 );
|
||||
|
||||
/**
|
||||
* Dodaje query vars dla endpointów XML.
|
||||
*/
|
||||
function apartamenty_xml_query_vars( $vars ) {
|
||||
$vars[] = 'apartamenty_xml';
|
||||
$vars[] = 'apartamenty_datagov';
|
||||
return $vars;
|
||||
}
|
||||
add_filter( 'query_vars', 'apartamenty_xml_query_vars' );
|
||||
|
||||
/**
|
||||
* Generuje XML z cenami wszystkich apartamentów.
|
||||
* Wynik cachowany w transiencie na 1 godzinę.
|
||||
*
|
||||
* @return string XML jako string
|
||||
*/
|
||||
function apartamenty_generate_price_xml() {
|
||||
$cached = get_transient( 'apartamenty_price_xml_cache' );
|
||||
if ( false !== $cached ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'price_history';
|
||||
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'apartamenty',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
] );
|
||||
|
||||
$x = function( $val ) {
|
||||
return htmlspecialchars( (string) $val, ENT_XML1, 'UTF-8' );
|
||||
};
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
$xml .= '<lokale inwestycja="Wyszyńskiego 12" generowany="' . $x( date( 'c' ) ) . '">' . "\n";
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
foreach ( $query->posts as $post ) {
|
||||
$post_id = $post->ID;
|
||||
|
||||
$type = get_post_meta( $post_id, 'information_type', true );
|
||||
$floor = get_post_meta( $post_id, 'information_floor', true );
|
||||
$floor_space = get_post_meta( $post_id, 'information_floor_space', true );
|
||||
$price = get_post_meta( $post_id, 'information_price', true );
|
||||
$price_m2 = get_post_meta( $post_id, 'information_price_m2', true );
|
||||
$status = get_post_meta( $post_id, 'information_status', true );
|
||||
|
||||
$history = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT recorded_at, price, price_m2
|
||||
FROM {$table_name}
|
||||
WHERE post_id = %d
|
||||
ORDER BY recorded_at DESC",
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
|
||||
$last_update = ! empty( $history ) ? $history[0]->recorded_at : date( 'Y-m-d' );
|
||||
|
||||
$xml .= "\t" . '<lokal id="' . absint( $post_id ) . '">' . "\n";
|
||||
$xml .= "\t\t<nazwa>" . $x( $post->post_title ) . "</nazwa>\n";
|
||||
$xml .= "\t\t<typ>" . $x( $type ) . "</typ>\n";
|
||||
$xml .= "\t\t<pietro>" . $x( $floor ) . "</pietro>\n";
|
||||
$xml .= "\t\t<powierzchnia>" . $x( $floor_space ) . "</powierzchnia>\n";
|
||||
$xml .= "\t\t<status>" . $x( $status ) . "</status>\n";
|
||||
$xml .= "\t\t<cena_brutto>" . $x( $price ) . "</cena_brutto>\n";
|
||||
$xml .= "\t\t<cena_za_m2>" . $x( $price_m2 ) . "</cena_za_m2>\n";
|
||||
$xml .= "\t\t<data_aktualizacji>" . $x( $last_update ) . "</data_aktualizacji>\n";
|
||||
$xml .= "\t\t<historia_cen>\n";
|
||||
|
||||
foreach ( $history as $row ) {
|
||||
$xml .= "\t\t\t" . '<zmiana data="' . $x( $row->recorded_at ) . '">' . "\n";
|
||||
$xml .= "\t\t\t\t<cena_brutto>" . $x( $row->price ) . "</cena_brutto>\n";
|
||||
$xml .= "\t\t\t\t<cena_za_m2>" . $x( $row->price_m2 ) . "</cena_za_m2>\n";
|
||||
$xml .= "\t\t\t</zmiana>\n";
|
||||
}
|
||||
|
||||
$xml .= "\t\t</historia_cen>\n";
|
||||
$xml .= "\t</lokal>\n";
|
||||
}
|
||||
}
|
||||
|
||||
$xml .= '</lokale>';
|
||||
|
||||
set_transient( 'apartamenty_price_xml_cache', $xml, HOUR_IN_SECONDS );
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generuje XML katalogu dane.gov.pl zgodny z XSD portalu.
|
||||
*
|
||||
* @return string XML jako string
|
||||
*/
|
||||
function apartamenty_generate_datagov_xml() {
|
||||
$resource_url = home_url( '/ceny-mieszkan.xml' );
|
||||
$last_update = date( 'Y-m-d\T00:00:00.000\Z' );
|
||||
|
||||
$x = function( $val ) {
|
||||
return htmlspecialchars( (string) $val, ENT_XML1, 'UTF-8' );
|
||||
};
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
$xml .= '<datasets xmlns:xsi="urn:otwarte-dane:harvester:1.0-rc1">' . "\n";
|
||||
$xml .= "\t<dataset status=\"published\">\n";
|
||||
$xml .= "\t\t<extIdent>wyszynskiego12-ceny-mieszkan-v1</extIdent>\n";
|
||||
$xml .= "\t\t<title>\n";
|
||||
$xml .= "\t\t\t<polish>Ceny mieszkań – Wyszyńskiego 12</polish>\n";
|
||||
$xml .= "\t\t</title>\n";
|
||||
$xml .= "\t\t<description>\n";
|
||||
$xml .= "\t\t\t<polish>Historia cen lokali mieszkalnych w inwestycji przy ul. Wyszyńskiego 12. Dane aktualizowane codziennie zgodnie z ustawą o jawności cen.</polish>\n";
|
||||
$xml .= "\t\t</description>\n";
|
||||
$xml .= "\t\t<updateFrequency>daily</updateFrequency>\n";
|
||||
$xml .= "\t\t<categories>REGI</categories>\n";
|
||||
$xml .= "\t\t<resources>\n";
|
||||
$xml .= "\t\t\t<resource status=\"published\">\n";
|
||||
$xml .= "\t\t\t\t<extIdent>wyszynskiego12-ceny-xml-v1</extIdent>\n";
|
||||
$xml .= "\t\t\t\t<url>" . $x( $resource_url ) . "</url>\n";
|
||||
$xml .= "\t\t\t\t<title>\n";
|
||||
$xml .= "\t\t\t\t\t<polish>Cennik lokali XML</polish>\n";
|
||||
$xml .= "\t\t\t\t</title>\n";
|
||||
$xml .= "\t\t\t\t<description>\n";
|
||||
$xml .= "\t\t\t\t\t<polish>Plik XML z aktualnym cennikiem i historią zmian cen wszystkich lokali</polish>\n";
|
||||
$xml .= "\t\t\t\t</description>\n";
|
||||
$xml .= "\t\t\t\t<availability>remote</availability>\n";
|
||||
$xml .= "\t\t\t\t<lastUpdateDate>" . $x( $last_update ) . "</lastUpdateDate>\n";
|
||||
$xml .= "\t\t\t</resource>\n";
|
||||
$xml .= "\t\t</resources>\n";
|
||||
$xml .= "\t\t<tags lang=\"pl\">\n";
|
||||
$xml .= "\t\t\t<tag>mieszkania</tag>\n";
|
||||
$xml .= "\t\t\t<tag>ceny</tag>\n";
|
||||
$xml .= "\t\t\t<tag>deweloper</tag>\n";
|
||||
$xml .= "\t\t\t<tag>jawność cen</tag>\n";
|
||||
$xml .= "\t\t\t<tag>historia cen</tag>\n";
|
||||
$xml .= "\t\t</tags>\n";
|
||||
$xml .= "\t</dataset>\n";
|
||||
$xml .= '</datasets>';
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsługuje żądania do endpointów XML — wysyła odpowiedź i kończy.
|
||||
*/
|
||||
function apartamenty_xml_template_redirect() {
|
||||
$xml_type = get_query_var( 'apartamenty_xml' );
|
||||
$datagov_type = get_query_var( 'apartamenty_datagov' );
|
||||
|
||||
if ( $xml_type ) {
|
||||
$content = apartamenty_generate_price_xml();
|
||||
if ( 'xml' === $xml_type ) {
|
||||
header( 'Content-Type: application/xml; charset=UTF-8' );
|
||||
echo $content;
|
||||
} else {
|
||||
header( 'Content-Type: text/plain; charset=UTF-8' );
|
||||
echo md5( $content );
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( $datagov_type ) {
|
||||
$content = apartamenty_generate_datagov_xml();
|
||||
if ( 'xml' === $datagov_type ) {
|
||||
header( 'Content-Type: application/xml; charset=UTF-8' );
|
||||
echo $content;
|
||||
} else {
|
||||
header( 'Content-Type: text/plain; charset=UTF-8' );
|
||||
echo md5( $content );
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
add_action( 'template_redirect', 'apartamenty_xml_template_redirect', 1 );
|
||||
|
||||
// ===========================================================
|
||||
// JAWNOŚĆ CEN — STRONA ADMINISTRACYJNA
|
||||
// ===========================================================
|
||||
|
||||
/**
|
||||
* Rejestruje stronę Jawność Cen w menu Narzędzia wp-admin.
|
||||
*/
|
||||
function apartamenty_jawnosc_cen_menu() {
|
||||
add_management_page(
|
||||
'Jawność Cen',
|
||||
'Jawność Cen',
|
||||
'manage_options',
|
||||
'jawnosc-cen',
|
||||
'apartamenty_jawnosc_cen_page'
|
||||
);
|
||||
}
|
||||
add_action( 'admin_menu', 'apartamenty_jawnosc_cen_menu' );
|
||||
|
||||
/**
|
||||
* Renderuje stronę administracyjną z URL-ami do zgłoszenia.
|
||||
*/
|
||||
function apartamenty_jawnosc_cen_page() {
|
||||
$url_ceny = esc_url( home_url( '/ceny-mieszkan.xml' ) );
|
||||
$url_datagov = esc_url( home_url( '/dane-gov-pl.xml' ) );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Jawność Cen — Wyszyńskiego 12</h1>
|
||||
|
||||
<div class="notice notice-info">
|
||||
<p>Dane aktualizowane codziennie przez WP Cron. Zgłoś URL katalogu dane.gov.pl do administratora portalu: <strong>kontakt@dane.gov.pl</strong></p>
|
||||
</div>
|
||||
|
||||
<table class="widefat" style="max-width:800px; margin-top:20px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plik</th>
|
||||
<th>URL</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Plik cen (dane)</strong></td>
|
||||
<td><code><?php echo $url_ceny; ?></code></td>
|
||||
<td>
|
||||
<button class="button" onclick="copyUrl('<?php echo esc_js( $url_ceny ); ?>')">Kopiuj URL</button>
|
||||
<a href="<?php echo $url_ceny; ?>" target="_blank" class="button">Otwórz XML</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Katalog dane.gov.pl</strong><br><em>(zgłoś Ministerstwu)</em></td>
|
||||
<td><code><?php echo $url_datagov; ?></code></td>
|
||||
<td>
|
||||
<button class="button button-primary" onclick="copyUrl('<?php echo esc_js( $url_datagov ); ?>')">Kopiuj URL</button>
|
||||
<a href="<?php echo $url_datagov; ?>" target="_blank" class="button">Otwórz XML</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
function copyUrl(url) {
|
||||
navigator.clipboard.writeText(url).then(function() {
|
||||
alert('Skopiowano: ' + url);
|
||||
}).catch(function() {
|
||||
prompt('Skopiuj URL:', url);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
@@ -156,7 +156,8 @@ class Elementor_Apartaments extends \Elementor\Widget_Base {
|
||||
<?php endif; ?>
|
||||
<tr class="apartament-card__price-history">
|
||||
<td class="apartament-card__info_table-title"></td>
|
||||
<td class="apartament-card__info_table-value">
|
||||
<td class="apartament-card__info_table-value btn-historia-cen"
|
||||
data-post-id="<?php echo esc_attr( get_the_ID() ); ?>">
|
||||
HISTORIA CEN
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7" fill="none">
|
||||
@@ -214,6 +215,33 @@ class Elementor_Apartaments extends \Elementor\Widget_Base {
|
||||
<?php wp_reset_postdata(); ?>
|
||||
</div>
|
||||
|
||||
<?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²:</span>
|
||||
<span class="price-history-modal__val" id="price-history-sqm"> </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>
|
||||
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
.elementor-element.elementor-arrows-position-outside .swiper,.elementor-lightbox.elementor-arrows-position-outside .swiper{width:calc(100% - 160px)}#header-nav ul.elementor-nav-menu li.menu-item a::after{bottom:-5px}.spot.is-reserved{fill:#f4eba8}.spot.is-sold{fill:#f54040}/*# sourceMappingURL=custom.css.map */
|
||||
.elementor-element.elementor-arrows-position-outside .swiper,.elementor-lightbox.elementor-arrows-position-outside .swiper{width:calc(100% - 160px)}@media(max-width: 768px){.elementor-element.elementor-arrows-position-outside .swiper,.elementor-lightbox.elementor-arrows-position-outside .swiper{width:100%}}@media(max-width: 768px){.elementor-swiper-button svg path{fill:#fff}}#header-nav ul.elementor-nav-menu li.menu-item a::after{bottom:-5px}.spot.is-reserved{fill:#f4eba8}.spot.is-sold{fill:#f54040}/*# sourceMappingURL=custom.css.map */
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sources":["custom.scss"],"names":[],"mappings":"AAAA,2HAEC,wBAAA,CAQG,wDACC,WAAA,CASJ,kBACC,YAAA,CAED,cACC,YAAA","file":"custom.css"}
|
||||
{"version":3,"sources":["custom.scss"],"names":[],"mappings":"AAAA,2HAEC,wBAAA,CAEA,yBAJD,2HAKE,UAAA,CAAA,CAOC,yBADD,kCAEE,SAAA,CAAA,CAWA,wDACC,WAAA,CASJ,kBACC,YAAA,CAED,cACC,YAAA","file":"custom.css"}
|
||||
@@ -1,6 +1,20 @@
|
||||
.elementor-element.elementor-arrows-position-outside .swiper,
|
||||
.elementor-lightbox.elementor-arrows-position-outside .swiper {
|
||||
width: calc(100% - 160px);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.elementor-swiper-button {
|
||||
svg {
|
||||
path {
|
||||
@media (max-width: 768px) {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#header-nav {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
?>
|
||||
|
||||
<svg width="1266" height="675" viewBox="0 0 1266 675" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="1266" viewBox="0 0 1266 675" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 100%; max-width: fit-content; margin: 0 auto;">
|
||||
<g clip-path="url(#clip0_1_32)">
|
||||
<path d="M576.107 326.51H564.529V363.677H576.107V326.51Z" fill="#192C44"/>
|
||||
<path d="M576.108 11.5897H1266V0H576.108H564.53V11.5897V45.1599H11.5781H0V56.7496V663.011V675H11.5781H1254.42H1266V663.011V186.234V174.245H1254.42H910.274H898.696H786.11V175.044H785.711L742.193 218.606H740.995V352.087V363.677H752.973H869.552V352.087H752.973V224.6L791.3 186.234H898.696V363.677H910.274V186.234H1254.42V663.011H11.5781V56.7496H564.53V296.936H576.108V11.5897Z" fill="#192C44"/>
|
||||
|
||||
Reference in New Issue
Block a user