--- phase: 01-historia-cen plan: 01 type: execute wave: 1 depends_on: [] files_modified: - wp-content/plugins/elementor-addon/elementor-addon.php autonomous: true --- ## 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 ## 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) ## 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 } ``` Task 1: Utwórz tabelę wp_price_history i zarejestruj cron wp-content/plugins/elementor-addon/elementor-addon.php 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. 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' AC-1 i AC-2 spełnione: tabela istnieje, cron zarejestrowany Task 2: AJAX endpoint historii cen wp-content/plugins/elementor-addon/elementor-addon.php 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. 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",...}} AC-3 i AC-4 spełnione: endpoint zwraca dane JSON, nonce weryfikowany ## 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 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) - 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 Po ukończeniu utwórz `.paul/phases/01-historia-cen/01-01-SUMMARY.md`