feat(05-admin-panel): Admin panel z historią rezerwacji

Phase 5 complete — CPT carei_reservation z automatycznym zapisem,
lista z kolumnami i filtrem statusu, meta box szczegółów,
system statusów nowe/przeczytane/zrealizowane, auto-mark-read.

Milestone v0.1 Formularz Rezerwacji MVP — all 5 phases complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 17:39:09 +01:00
parent a82ec90a51
commit 2af73782f2
11 changed files with 1261 additions and 23 deletions

View File

@@ -24,6 +24,30 @@ Plugin Elementor do rezerwacji samochodu na stronie carei.pagedev.pl, zintegrowa
- Formularz NIE jest natywnym formularzem Elementor Pro — to custom widget - Formularz NIE jest natywnym formularzem Elementor Pro — to custom widget
- Brak dodatkowych zależności npm/composer — czysty PHP + JS - Brak dodatkowych zależności npm/composer — czysty PHP + JS
## Validated Requirements (Milestone v0.1)
- ✓ Elementor Widget z modalem rezerwacji — Phase 1-2
- ✓ Integracja z Softra Rent API (auth, branch, carclass, pricelist, booking) — Phase 1, 3
- ✓ Multi-step form: formularz → podsumowanie → sukces — Phase 2-3
- ✓ Responsive modal (desktop overlay + mobile) — Phase 2
- ✓ Error handling: token retry, timeout, network errors — Phase 4
- ✓ Accessibility: ARIA dialog, focus trap, aria-live — Phase 4
- ✓ Admin panel: CPT carei_reservation, lista, szczegóły, statusy — Phase 5
## Key Decisions
| Decision | Phase | Rationale |
|----------|-------|-----------|
| CPT + post_meta (nie custom table) | 5 | WordPress-native, prostsze dla MVP |
| Fire-and-forget save | 5 | Nie blokuj response — rezerwacja już w Softra |
| Meta-based status (nie taxonomy) | 5 | Prosty 3-wartościowy enum |
| Token retry on 401/403 | 4 | Automatyczny re-auth bez interwencji usera |
| Inline display:none for steps | 4 | CSS class conflict resolution |
## Out of Scope (backlog)
- Ubezpieczenie (pakiet Soft/Premium) — czeka na API Softra
- Wyjazd zagraniczny (lista krajów + ceny) — czeka na API Softra
- Eksport CSV/PDF rezerwacji
- Email notyfikacje
## API Endpoints (kluczowe) ## API Endpoints (kluczowe)
| Endpoint | Metoda | Użycie | | Endpoint | Metoda | Użycie |
|----------|--------|--------| |----------|--------|--------|

View File

@@ -7,11 +7,18 @@
### Phase 1: Plugin Skeleton + API Proxy ✅ Complete ### Phase 1: Plugin Skeleton + API Proxy ✅ Complete
Utworzenie pluginu WordPress z proxy REST API do Softra Rent. Backend obsługujący autoryzację JWT, pobieranie oddziałów, klas pojazdów, cenników i dodatków. Rejestracja widgetu Elementor. Utworzenie pluginu WordPress z proxy REST API do Softra Rent. Backend obsługujący autoryzację JWT, pobieranie oddziałów, klas pojazdów, cenników i dodatków. Rejestracja widgetu Elementor.
### Phase 2: Form UI — Krok 1 (Formularz) ⬜ Not started ### Phase 2: Form UI — Krok 1 (Formularz) ✅ Complete
Frontend formularza rezerwacji zgodny z Figmą: modal/bottom-sheet, pola (segment, daty, lokalizacja, opcje dodatkowe, dane osobowe, wiadomość, zgoda RODO). Dynamiczne ładowanie danych z API (oddziały, klasy). Responsywny desktop + mobile. Frontend formularza rezerwacji w modalu Elementor. Dynamiczne segmenty (ze wszystkich lokalizacji), oddziały filtrowane po segmencie, opcje dodatkowe z cennika API, walidacja, responsive. Pominięto ubezpieczenie i wyjazd zagraniczny (brak w API — backlog).
### Phase 3: Form UI — Krok 2 (Overlay / Podsumowanie) ⬜ Not started ### Phase 3: Submit + Booking Flow ✅ Complete
Rozwinięcie formularza po wypełnieniu kroku 1: podsumowanie kosztów (pricing summary z API), tworzenie klienta, złożenie rezerwacji, potwierdzenie. Obsługa błędów i stanów ładowania. Pełny flow: formularz → customer/add → pricing summary overlay → makebooking → confirm → success z numerem. Pola adresowe + PESEL. Error handling. Fixy: boolean API params, timeout mapowania, drivers[] required.
### Phase 4: Polish & Integration Testing ⬜ Not started ### Phase 4: Polish & Integration Testing ✅ Complete
Testy end-to-end na carei.pagedev.pl, walidacja formularza, obsługa edge cases (brak dostępności, timeout tokenu, błędy API), animacje przejść między krokami, a11y. Edge cases (token retry, timeout, network errors), animacje przejść form↔summary↔success, accessibility (ARIA dialog, focus trap, focus management, aria-live), CSS fix.
### Phase 5: Admin Panel — Historia Formularzy ✅ Complete
CPT `carei_reservation` z automatycznym zapisem po booking, lista z kolumnami i filtrem statusu, meta box szczegółów, system statusów nowe/przeczytane/zrealizowane, auto-mark-read.
### Backlog (do realizacji gdy API będzie gotowe)
- **Wyjazd zagraniczny:** Checkbox "Planuję trasę poza granicę Polski" + wyszukiwarka krajów z cenami. Wymaga endpointu listy krajów i cennika per kraj w API Softra.
- **Ubezpieczenie:** Sekcja "Pakiet ochrony Soft/Premium" jak na Figmie. Wymaga dedykowanych pozycji ubezpieczeniowych w API pricelist.

View File

@@ -2,33 +2,35 @@
## Current Position ## Current Position
Milestone: v0.1 Formularz Rezerwacji MVP Milestone: v0.1 Formularz Rezerwacji MVP — COMPLETE
Phase: 2 of 4 (Form UI — Krok 1) — APPLY in progress Phase: 5 of 5 (Admin Panel — Historia Formularzy) — Complete
Plan: 02-01 tasks 1+2 done, task 3 (checkpoint:human-verify) pending Plan: 05-01 — Complete
Status: Awaiting deploy + visual test on carei.pagedev.pl Status: Milestone v0.1 complete — all 5 phases finished
Last activity: 2026-03-25 — Fixed API (native cURL), added car-classes-all endpoint, built full form UI Last activity: 2026-03-25 — Phase 5 unified, milestone complete
Progress: Progress:
- Milestone: [██░░░░░░░░] 25% - Milestone: [██████████] 100% ✅
- Phase 1: [██████████] 100% ✅ - Phase 1: [██████████] 100% ✅
- Phase 2: [██████░░░░] 66% (2/3 tasks, checkpoint pending) - Phase 2: [██████████] 100% ✅
- Phase 3: [██████████] 100% ✅
- Phase 4: [██████████] 100% ✅
- Phase 5: [██████████] 100% ✅
## Loop Position ## Loop Position
Current loop state: Current loop state:
``` ```
PLAN ──▶ APPLY ──▶ UNIFY PLAN ──▶ APPLY ──▶ UNIFY
[Apply in progress — checkpoint waiting] [Loop complete — milestone v0.1 finished]
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-03-25 Last session: 2026-03-25
Stopped at: Phase 2 Plan 02-01 — Tasks 1+2 complete, Task 3 checkpoint:human-verify pending Stopped at: Milestone v0.1 complete — all phases unified
Next action: Deploy files to server, test form visually, then "approved" or describe issues Next action: /paul:complete-milestone or start next milestone
Resume file: .paul/HANDOFF-2026-03-25.md Resume file: .paul/ROADMAP.md
Resume context: Resume context:
- 5 plugin files need deploying (API, proxy, widget, CSS, JS) - All 5 phases complete: skeleton, form UI, booking flow, polish, admin panel
- Key fix: native cURL instead of wp_remote_post - Plugin fully functional: formularz → API Softra → admin panel
- Key fix: car-classes-all endpoint loads segments without requiring branch - Backlog: ubezpieczenie + wyjazd zagraniczny (czeka na API)
- After approved → /paul:unify → /paul:plan Phase 3

View File

@@ -0,0 +1,108 @@
# PAUL Handoff
**Date:** 2026-03-25
**Status:** paused — Phase 3 complete, Phase 4 not started
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** Carei — Formularz rezerwacji samochodu jako plugin Elementor, zintegrowany z Softra Rent API
**Core value:** Klient klika "Złóż zapytanie o rezerwację" → modal z formularzem → dane z API Softra → rezerwacja
---
## Current State
**Milestone:** v0.1 Formularz Rezerwacji MVP
**Phase:** 3 of 5 — Complete ✅
**Plan:** 03-01 — Complete
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Loop complete — ready for next PLAN]
```
---
## What Was Done
**This session:**
- Refactored form layout: segment+dates in one row, pickup+same-return checkbox in one row
- New endpoint `/segments-branches-map` (simplified: 2 API calls, all branches for all segments)
- Segment→branch filtering in pickup select (all branches shown, API verifies at booking)
- Extras hidden until segment AND pickup selected
- Investigated API for insurance (none) and foreign travel (none) → deferred to backlog
- Added Phase 5 to roadmap: Admin Panel historia formularzy
- **Phase 3 complete:** full booking flow end-to-end
- Added address fields (city, zipCode, street) + PESEL
- Pricing summary overlay with cost breakdown table
- Success view with reservation number
- customer/add → princingSummary → makebooking → confirm
- Error handling at each step
**API discoveries (critical for future work):**
- Boolean params: must send `true`/`false`, NOT `'T'`/`'N'` (Java deserialization)
- `drivers[]` is required in makebooking (not documented as required)
- Penalty items in pricelist (BRAK/BRUD/KARA codes) filtered from display
- No insurance or foreign travel endpoints in current API version
---
## What's Next
**Immediate:** `/paul:plan` for Phase 4 — Polish & Integration Testing
- Edge cases (brak dostępności, timeout tokenu, błędy API)
- Animacje przejść form↔summary↔success
- Accessibility
- End-to-end testing on carei.pagedev.pl
**After that:**
- Phase 5: Admin Panel — zapisywanie formularzy w WP, przeglądanie w wp-admin
**Backlog (czeka na nową wersję API):**
- Ubezpieczenie (Pakiet Soft/Premium)
- Wyjazd zagraniczny (checkbox + wyszukiwarka krajów)
---
## Key Decisions Made
| Decision | Rationale |
|----------|-----------|
| Boolean API params (true/false) | Java backend rejects 'T'/'N' strings |
| Simplified branch mapping (no per-branch filtering) | 39 branches × API call = timeout; all branches shown, API verifies at booking |
| drivers[] = najemca | API requires NotNull, form collects same data |
| skipAccountCreate: true | MVP — no customer accounts needed |
| Penalty items filtered by code prefix | BRAK/BRUD/KARA not customer-facing options |
| Ubezpieczenie + wyjazd zagraniczny → backlog | API has no dedicated endpoints |
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview (1-3 ✅, 4-5 ⬜, backlog) |
| `.paul/phases/03-form-submit-booking/03-01-SUMMARY.md` | Phase 3 summary |
| `wp-content/plugins/carei-reservation/` | Plugin directory (all code) |
| `docs/rent-api-02-klienci-i-konta.md` | Customer API docs |
| `docs/rent-api-03-rezerwacje-i-platnosci.md` | Booking API docs |
| `softra-test.php` | API test reference |
| `.env` | API credentials |
---
## Resume Instructions
1. Read `.paul/STATE.md` for latest position
2. Phase 3 complete — ready for Phase 4 planning
3. Run `/paul:resume` or `/paul:plan`
---
*Handoff created: 2026-03-25*

View File

@@ -0,0 +1,122 @@
# PAUL Handoff
**Date:** 2026-03-25
**Status:** paused — Phase 5 APPLY in progress, checkpoint pending
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** Carei — Formularz rezerwacji samochodu jako plugin Elementor, zintegrowany z Softra Rent API
**Core value:** Klient klika "Złóż zapytanie o rezerwację" → modal z formularzem → dane z API Softra → rezerwacja
---
## Current State
**Milestone:** v0.1 Formularz Rezerwacji MVP
**Phase:** 5 of 5 — Admin Panel — Historia Formularzy
**Plan:** 05-01 — APPLY in progress (2/3 tasks done, checkpoint pending)
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ◐ ○ [APPLY in progress — checkpoint pending]
```
---
## What Was Done
**This session (Phase 4 + Phase 5):**
### Phase 4: Polish & Integration Testing ✅ COMPLETE
- Token retry on 401/403 (auto-retry 1x)
- AbortController timeout 15s + Polish error messages
- Network error detection (TypeError → retry → user message)
- Extended reject reason translations (6 codes)
- Animated step transitions: form ↔ summary ↔ success (fade+translate 250ms)
- Modal open/close animations (fade + scale)
- ARIA dialog with focus trap and focus management
- aria-live announcements for screen readers
- CSS bug fix (orphaned media query styles)
- Fix: inline display:none vs CSS class conflict (auto-fixed during checkpoint)
### Phase 5: Admin Panel (in progress)
- Created `includes/class-admin-panel.php` — full admin panel:
- CPT `carei_reservation` registered (menu "Rezerwacje", dashicons-car)
- Admin columns: Nr rezerwacji, Klient, Segment, Daty, Oddział, Status, Data
- Status filter dropdown above list
- Meta box "Szczegóły rezerwacji" with full data table
- Status dropdown (nowe/przeczytane/zrealizowane) with colored badges
- Auto-mark-read on post edit open
- Inline admin CSS for badges and meta table
- Static `save_reservation()` method for fire-and-forget save
- Modified `class-rest-proxy.php``make_booking()` calls `save_reservation()` on success
- Modified `carei-reservation.php` — includes + initializes `Carei_Admin_Panel`
- All PHP syntax verified clean
---
## What's In Progress
- **Phase 5 Plan 05-01 checkpoint:** Human verification in wp-admin pending
- All code written, PHP syntax OK
- Need user to test: CPT in menu, submit reservation, check admin list, meta box, status changes
---
## What's Next
**Immediate:** Resume Phase 5 checkpoint verification
1. User tests admin panel on carei.pagedev.pl
2. If approved → `/paul:unify .paul/phases/05-admin-panel/05-01-PLAN.md`
3. Unify closes Phase 5 → Milestone v0.1 complete!
**After milestone:**
- Backlog: ubezpieczenie + wyjazd zagraniczny (czeka na nową wersję API Softra)
---
## Key Decisions Made
| Decision | Rationale |
|----------|-----------|
| CPT + post_meta (not custom table) | WordPress-native, simpler for MVP, full admin UI for free |
| Fire-and-forget save | Never block user response — rezerwacja already in Softra |
| Meta-based status (not taxonomy) | Simple 3-value enum, no UI overhead |
| Auto-mark-read on edit | Reduce manual clicks, natural workflow |
| Hidden title input | Title auto-generated, not user-editable |
| style.display for step transitions | Inline display:none overrides CSS classes |
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview (1-4 ✅, 5 in progress) |
| `.paul/phases/05-admin-panel/05-01-PLAN.md` | Phase 5 plan (checkpoint pending) |
| `.paul/phases/04-polish-testing/04-01-SUMMARY.md` | Phase 4 summary |
| `wp-content/plugins/carei-reservation/` | Plugin directory (all code) |
| `includes/class-admin-panel.php` | **NEW** — Admin panel CPT + columns + meta box |
| `includes/class-rest-proxy.php` | Modified — save_reservation() call |
| `includes/class-elementor-widget.php` | ARIA attrs (Phase 4) |
| `assets/js/carei-reservation.js` | Edge cases + animations + a11y (Phase 4) |
| `assets/css/carei-reservation.css` | Transitions + CSS fix (Phase 4) |
---
## Resume Instructions
1. Read `.paul/STATE.md` for latest position
2. Phase 5 APPLY in progress — checkpoint pending
3. Run `/paul:resume` — will route to checkpoint verification
4. After approved → `/paul:unify` → milestone complete
---
*Handoff created: 2026-03-25*

View File

@@ -0,0 +1,112 @@
# PAUL Handoff
**Date:** 2026-03-25
**Status:** paused — checkpoint human-verify pending (awaiting deploy + test)
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** Carei — Formularz rezerwacji samochodu jako plugin Elementor, zintegrowany z Softra Rent API
**Core value:** Klient klika "Złóż zapytanie o rezerwację" → modal z formularzem → dane z API Softra → rezerwacja
---
## Current State
**Milestone:** v0.1 Formularz Rezerwacji MVP
**Phase:** 2 of 4 — Form UI Krok 1 (Formularz)
**Plan:** 02-01 — APPLY in progress (Tasks 1+2 done, Task 3 checkpoint pending)
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ◐ ○ [Apply in progress — checkpoint:human-verify waiting]
```
---
## What Was Done
**Phase 1 (complete):**
- Plugin `carei-reservation` z natywnym cURL proxy do Softra Rent API
- 10 WP REST endpoints w namespace `carei/v1`
- Widget Elementor z przyciskiem CTA + modal overlay
- Fix: widget require_once przeniesiony do hooka elementor/widgets/register
- Fix: zamiana wp_remote_post na natywny cURL (matching softra-test.php)
**Phase 2 (in progress — Tasks 1+2 done):**
- HTML formularza z wszystkimi sekcjami z Figmy (widget render)
- CSS 541 linii (responsive, custom checkboxy, karty opcji, walidacja)
- JS 580+ linii (API integration, walidacja, interakcje)
- Nowy endpoint GET /car-classes-all (listAll) — segmenty ładują się od razu bez wymagania oddziału
- Opcje dodatkowe ładowane dynamicznie z cennika API
---
## What's In Progress
- **Task 3 checkpoint:human-verify** — użytkownik musi wgrać pliki na serwer i przetestować wizualnie
- Pliki do wgrania:
- `includes/class-softra-api.php` (natywny cURL + get_all_car_classes)
- `includes/class-rest-proxy.php` (nowy endpoint car-classes-all)
- `includes/class-elementor-widget.php` (pełny HTML formularza)
- `assets/css/carei-reservation.css` (kompletne style)
- `assets/js/carei-reservation.js` (logika + API)
---
## What's Next
**Immediate:** Wgrać pliki na serwer → przetestować formularz → "approved" lub opisać problemy
**After that:**
- Jeśli approved → /paul:unify → Phase 3 (Overlay/podsumowanie + submit rezerwacji do API)
- Jeśli issues → fix → re-verify
**Remaining phases:**
- Phase 3: Form UI Krok 2 (Overlay z podsumowaniem, tworzenie klienta, booking)
- Phase 4: Polish & integration testing
---
## Key Decisions Made
| Decision | Rationale |
|----------|-----------|
| Natywny cURL zamiast wp_remote_post | softra-test.php działa z cURL, WP HTTP API timeout |
| GET /car/class/listAll dla segmentów | Segment jest pierwszym polem w Figmie, nie powinien wymagać oddziału |
| Osobny plugin carei-reservation | Czystsza separacja od elementor-addon |
| sslverify nie ustawiane (domyślne cURL) | Tak jak w softra-test.php który działa na produkcji |
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview (Phase 1 ✅, Phase 2-4 ⬜) |
| `.paul/phases/02-form-ui-step1/02-01-PLAN.md` | Current plan |
| `.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md` | Phase 1 summary |
| `wp-content/plugins/carei-reservation/` | Plugin directory (all code) |
| `docs/figma-formularz/README.md` | Figma design spec |
| `docs/figma-formularz/screenshot-desktop.png` | Desktop reference |
| `docs/figma-formularz/screenshot-mobile.png` | Mobile reference |
| `softra-test.php` | Working Softra API test (reference for cURL config) |
| `.env` | API credentials (url, username, password) |
---
## Resume Instructions
1. Read `.paul/STATE.md` for latest position
2. User needs to deploy files to server and test
3. After test: "approved" → run `/paul:unify` then `/paul:plan` for Phase 3
4. Or run `/paul:resume` or `/paul:progress`
---
*Handoff created: 2026-03-25*

View File

@@ -0,0 +1,274 @@
---
phase: 05-admin-panel
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/carei-reservation/includes/class-admin-panel.php
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
- wp-content/plugins/carei-reservation/carei-reservation.php
autonomous: false
---
<objective>
## Goal
Admin panel w wp-admin do przeglądania rezerwacji: Custom Post Type `carei_reservation`, automatyczny zapis przy udanej rezerwacji, lista z kolumnami i filtrami, podgląd szczegółów, statusy (nowe/przeczytane/zrealizowane).
## Purpose
Właściciel firmy musi mieć przegląd wszystkich rezerwacji złożonych przez formularz — bez logowania do panelu Softra. Dane zapisane w WordPress = szybki podgląd, filtrowanie, zmiana statusu.
## Output
- Nowy plik: `includes/class-admin-panel.php` (CPT, kolumny, meta box, statusy)
- Zmodyfikowany: `includes/class-rest-proxy.php` (zapis rezerwacji po sukcesie)
- Zmodyfikowany: `carei-reservation.php` (include nowej klasy)
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/03-form-submit-booking/03-01-SUMMARY.md
- Booking flow: customer/add → pricingSummary → makebooking → confirm
- Dane zbierane: segment, daty, oddział, extras, imię, nazwisko, adres, email, telefon, PESEL, wiadomość
- Po sukcesie: reservationId + reservationNo z API
## Source Files
@wp-content/plugins/carei-reservation/carei-reservation.php
@wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
</context>
<acceptance_criteria>
## AC-1: CPT carei_reservation zarejestrowany
```gherkin
Given plugin jest aktywny
When otworzę wp-admin
Then w menu bocznym widzę "Rezerwacje" z ikoną dashicons-car
And CPT nie jest publiczny (brak na frontendzie)
And supports: title (auto-generowany, nie edytowalny)
```
## AC-2: Rezerwacja zapisuje się automatycznie po sukcesie
```gherkin
Given użytkownik wypełnił formularz i potwierdził rezerwację
When API makebooking + confirm zwraca sukces
Then w wp-admin pojawia się nowy wpis z danymi rezerwacji
And tytuł: "Rezerwacja #[reservationNo] [Imię Nazwisko]"
And wszystkie dane formularza zapisane w post_meta
And status: "nowe"
```
## AC-3: Lista rezerwacji z kolumnami
```gherkin
Given jestem w wp-admin Rezerwacje
When widzę listę wpisów
Then kolumny: Nr rezerwacji | Klient | Segment | Daty | Oddział | Status | Data
And kolumna Status pokazuje kolorowy badge (nowe=niebieski, przeczytane=żółty, zrealizowane=zielony)
And mogę sortować po dacie
And mogę filtrować po statusie (dropdown nad listą)
```
## AC-4: Podgląd szczegółów rezerwacji
```gherkin
Given kliknę na rezerwację w liście
When otwiera się edycja wpisu
Then widzę meta box "Szczegóły rezerwacji" z tabelą:
- Nr rezerwacji, ID klienta Softra
- Segment, daty od-do, oddział odbioru, oddział zwrotu
- Imię, nazwisko, email, telefon, PESEL, adres
- Wybrane opcje dodatkowe
- Wiadomość klienta
And widzę dropdown do zmiany statusu (nowe przeczytane zrealizowane)
And status zmienia się po kliknięciu "Zaktualizuj"
```
## AC-5: Przy otwarciu rezerwacji status zmienia się na "przeczytane"
```gherkin
Given rezerwacja ma status "nowe"
When otwieram ją w wp-admin
Then status automatycznie zmienia się na "przeczytane"
And badge w liście aktualizuje się
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: CPT registration + admin panel class</name>
<files>wp-content/plugins/carei-reservation/includes/class-admin-panel.php, wp-content/plugins/carei-reservation/carei-reservation.php</files>
<action>
1. **Utwórz `includes/class-admin-panel.php`:**
Klasa `Carei_Admin_Panel` z metodami:
a) `register_post_type()` — hook na `init`:
- CPT: `carei_reservation`
- label: "Rezerwacje" / "Rezerwacja"
- menu_icon: `dashicons-car`
- public: false, show_ui: true, show_in_menu: true
- supports: `['title']` (title auto-generowany)
- capability_type: 'post'
- menu_position: 26 (pod Komentarze)
b) `register_statuses()` — custom taxonomy NIE jest potrzebna, użyj post_meta `_carei_status` z wartościami: `nowe`, `przeczytane`, `zrealizowane`
c) `add_admin_columns($columns)` — hook `manage_carei_reservation_posts_columns`:
- Kolumny: cb, reservation_no, client, segment, dates, branch, status, date
- Usuń domyślny title
d) `render_admin_column($column, $post_id)` — hook `manage_carei_reservation_posts_custom_column`:
- reservation_no: get_post_meta `_carei_reservation_no`
- client: `_carei_first_name` + `_carei_last_name`
- segment: `_carei_segment`
- dates: `_carei_date_from` + `_carei_date_to` (format: d.m.Y H:i)
- branch: `_carei_pickup_branch`
- status: badge z kolorem (nowe=#2F2482, przeczytane=#f59e0b, zrealizowane=#22c55e)
e) `add_status_filter()` — hook `restrict_manage_posts`:
- Dropdown filtra statusu nad listą
- Opcje: Wszystkie, Nowe, Przeczytane, Zrealizowane
f) `filter_by_status($query)` — hook `pre_get_posts`:
- Filtruj po `_carei_status` meta_query
g) `add_meta_box()` — hook `add_meta_boxes`:
- Meta box "Szczegóły rezerwacji" w edycji wpisu
- Tabela z wszystkimi danymi
- Dropdown statusu z save
h) `save_meta_box($post_id)` — hook `save_post_carei_reservation`:
- Zapisz wybrany status
i) `auto_mark_read($post_id)` — hook `edit_form_after_title` lub `admin_head`:
- Jeśli status == 'nowe' i jesteśmy na ekranie edycji → zmień na 'przeczytane'
j) **Inline CSS** dla badge'ów statusu i meta box — dodaj przez `admin_head` hook, scoped do CPT screen
2. **Zmodyfikuj `carei-reservation.php`:**
- Dodaj `require_once` dla nowej klasy
- Zainicjalizuj `new Carei_Admin_Panel()` w `plugins_loaded`
Avoid: Nie twórz oddzielnych template files. Wszystko w jednej klasie. Nie dodawaj custom taxonomy — meta jest wystarczające.
</action>
<verify>
- Otwórz wp-admin → "Rezerwacje" widoczne w menu
- Lista pusta, kolumny widoczne
- Filtr statusu nad listą
</verify>
<done>AC-1, AC-3, AC-4, AC-5 satisfied: CPT zarejestrowany, kolumny, meta box, auto-read</done>
</task>
<task type="auto">
<name>Task 2: Zapis rezerwacji po sukcesie booking</name>
<files>wp-content/plugins/carei-reservation/includes/class-rest-proxy.php</files>
<action>
1. **W `class-rest-proxy.php`**, w callback dla `/booking` endpoint:
- Po udanym `make_booking()` i `confirm_booking()` (jeśli confirm jest w tym samym request) LUB:
- Lepiej: dodaj nowy statyczny helper `Carei_Admin_Panel::save_reservation($data, $result)`
2. **W callback `/booking`:**
- Po `$result = $api->make_booking($body)` z sukcesem:
- Wywołaj `Carei_Admin_Panel::save_reservation($body, $result)`
3. **Metoda `save_reservation($booking_data, $api_result)`:**
- `wp_insert_post()` z:
- post_type: `carei_reservation`
- post_title: `Rezerwacja #{reservationNo} — {firstName} {lastName}`
- post_status: `publish`
- `update_post_meta()` dla:
- `_carei_reservation_no` ← reservationNo
- `_carei_reservation_id` ← reservationId
- `_carei_customer_id` ← customerId
- `_carei_segment` ← carParameters.categoryName
- `_carei_date_from` ← dateFrom
- `_carei_date_to` ← dateTo
- `_carei_pickup_branch` ← pickUpLocation.branchName
- `_carei_return_branch` ← returnLocation.branchName
- `_carei_first_name` ← drivers[0].firstName (lub z booking_data)
- `_carei_last_name` ← drivers[0].lastName
- `_carei_email` ← drivers[0].email
- `_carei_phone` ← drivers[0].phone
- `_carei_pesel` ← drivers[0].pesel
- `_carei_address` ← JSON encode drivers[0].address
- `_carei_extras` ← JSON encode priceItems
- `_carei_comments` ← comments
- `_carei_status` ← 'nowe'
- `_carei_raw_response` ← JSON encode api_result (backup)
4. **Error handling:** Jeśli wp_insert_post() fail, loguj do error_log ale NIE blokuj response do usera (rezerwacja już w Softra jest OK).
Avoid: Nie modyfikuj flow rezerwacji. Zapis jest fire-and-forget — nigdy nie blokuje odpowiedzi do klienta.
</action>
<verify>
- Złóż testową rezerwację przez formularz
- Sprawdź wp-admin → Rezerwacje → nowy wpis z danymi
- Kliknij wpis → meta box ze szczegółami
</verify>
<done>AC-2 satisfied: rezerwacja zapisuje się automatycznie po sukcesie</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Admin panel rezerwacji w wp-admin: CPT, kolumny, filtr statusu, meta box szczegółów, auto-zapis po rezerwacji, auto-mark-read</what-built>
<how-to-verify>
1. Otwórz wp-admin na carei.pagedev.pl
2. W menu bocznym: "Rezerwacje" z ikoną samochodu
3. Złóż rezerwację przez formularz na frontendzie
4. Wróć do wp-admin → Rezerwacje → nowy wpis ze statusem "nowe" (niebieski badge)
5. Kolumny: Nr rezerwacji, Klient, Segment, Daty, Oddział, Status, Data
6. Filtr statusu nad listą (dropdown)
7. Kliknij wpis → meta box "Szczegóły rezerwacji" z pełnymi danymi
8. Status zmienił się na "przeczytane" (żółty)
9. Zmień status na "zrealizowane" → Zaktualizuj → zielony badge
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js (Phase 4 stable)
- wp-content/plugins/carei-reservation/assets/css/carei-reservation.css (Phase 4 stable)
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php (Phase 4 stable)
- wp-content/plugins/carei-reservation/includes/class-softra-api.php (API proxy stable)
- .env (credentials)
## SCOPE LIMITS
- Nie dodawaj eksportu CSV/PDF (przyszły feature)
- Nie dodawaj email notyfikacji (zależy od Softra)
- Nie modyfikuj frontend formularza
- Nie dodawaj REST API endpoints dla admin panelu (nie potrzebne — natywny WP admin)
- Nie twórz custom database table — CPT + post_meta wystarczy dla MVP
</boundaries>
<verification>
Before declaring plan complete:
- [ ] CPT `carei_reservation` zarejestrowany w wp-admin
- [ ] Menu "Rezerwacje" widoczne z ikoną
- [ ] Rezerwacja zapisuje się po sukcesie booking
- [ ] Lista: kolumny Nr, Klient, Segment, Daty, Oddział, Status, Data
- [ ] Filtr statusu działa
- [ ] Meta box ze wszystkimi danymi
- [ ] Auto-mark-read przy otwarciu
- [ ] Zmiana statusu przez dropdown + save
- [ ] Badge kolory: nowe=niebieski, przeczytane=żółty, zrealizowane=zielony
- [ ] Brak regresji w booking flow na frontendzie
</verification>
<success_criteria>
- All 5 acceptance criteria met
- All verification checks pass
- No regressions in frontend booking flow
- Human verification approved on carei.pagedev.pl
</success_criteria>
<output>
After completion, create `.paul/phases/05-admin-panel/05-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,129 @@
---
phase: 05-admin-panel
plan: 01
subsystem: admin
tags: [wordpress, cpt, post-meta, admin-panel]
requires:
- phase: 03-form-submit-booking
provides: booking flow z reservationId/reservationNo w API response
provides:
- Admin panel CPT carei_reservation w wp-admin
- Automatyczny zapis rezerwacji po sukcesie booking
- Lista z kolumnami, filtr statusu, meta box szczegółów
- System statusów nowe/przeczytane/zrealizowane
affects: []
tech-stack:
added: []
patterns: [CPT + post_meta for data storage, fire-and-forget save, static helper method pattern]
key-files:
created: [includes/class-admin-panel.php]
modified: [includes/class-rest-proxy.php, carei-reservation.php]
key-decisions:
- "CPT + post_meta zamiast custom table — WordPress-native, prostsze dla MVP"
- "Fire-and-forget save — nigdy nie blokuje response do usera"
- "Meta-based status zamiast taxonomy — prosty 3-wartościowy enum"
- "Auto-mark-read on edit — naturalny workflow bez dodatkowych kliknięć"
patterns-established:
- "Static save helper: Carei_Admin_Panel::save_reservation() wywoływany z REST proxy"
- "Inline admin CSS scoped do CPT screen"
duration: ~2h
started: 2026-03-25
completed: 2026-03-25
---
# Phase 5 Plan 01: Admin Panel — Historia Formularzy Summary
**CPT carei_reservation z automatycznym zapisem rezerwacji, listą z kolumnami/filtrami, meta boxem szczegółów i systemem statusów nowe/przeczytane/zrealizowane.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~2h |
| Started | 2026-03-25 |
| Completed | 2026-03-25 |
| Tasks | 3 completed (2 auto + 1 human-verify) |
| Files modified | 3 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: CPT carei_reservation zarejestrowany | Pass | Menu "Rezerwacje" z dashicons-car, public: false, show_ui: true |
| AC-2: Rezerwacja zapisuje się automatycznie | Pass | save_reservation() wywoływane po make_booking success |
| AC-3: Lista z kolumnami | Pass | Nr, Klient, Segment, Daty, Oddział, Status, Data + filtr statusu |
| AC-4: Podgląd szczegółów | Pass | Meta box z pełną tabelą danych + dropdown statusu |
| AC-5: Auto-mark-read | Pass | Status "nowe" → "przeczytane" przy otwarciu edycji |
## Accomplishments
- CPT `carei_reservation` z pełnym admin UI: kolumny, filtr, meta box, statusy z kolorowymi badge'ami
- Fire-and-forget zapis rezerwacji po sukcesie booking (nie blokuje response)
- Klikalny nr rezerwacji w liście (link do edycji)
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `includes/class-admin-panel.php` | Created | CPT registration, admin columns, meta box, status system, auto-mark-read, save helper |
| `includes/class-rest-proxy.php` | Modified | Wywołanie save_reservation() po udanym make_booking |
| `carei-reservation.php` | Modified | Include + inicjalizacja Carei_Admin_Panel |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| CPT + post_meta (nie custom table) | WordPress-native, pełne admin UI za darmo | Prostsze dla MVP, ewentualna migracja jeśli skala wzrośnie |
| Fire-and-forget save | Rezerwacja już w Softra — WP save nie może blokować usera | Bezpieczne — error_log w razie problemu |
| Meta-based status (nie taxonomy) | 3 wartości enum, zero UI overhead | Proste, wystarczające dla MVP |
| Auto-mark-read on edit | Naturalny workflow, mniej kliknięć | UX improvement |
| Klikalny nr rezerwacji | Brak kolumny title = brak linku do edycji | Naprawione podczas checkpoint — link w kolumnie reservation_no |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 1 | Kliknięcie w rezerwację — minor UX fix |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Minimalne — jeden fix UX podczas checkpoint
### Auto-fixed Issues
**1. Brak linku do edycji w liście rezerwacji**
- **Found during:** Checkpoint (Task 3)
- **Issue:** Bez kolumny `title` nie było klikalnego linku do szczegółów
- **Fix:** Owinięcie nr rezerwacji w `<a>` z `get_edit_post_link()`
- **Files:** `includes/class-admin-panel.php`
- **Verification:** User potwierdził "Teraz jest ok"
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Brak linku do edycji rezerwacji | Dodano link w kolumnie reservation_no |
## Next Phase Readiness
**Ready:**
- Milestone v0.1 kompletny — wszystkie 5 faz zakończone
- Plugin w pełni funkcjonalny: formularz → API → admin panel
**Concerns:**
- Brak — MVP complete
**Blockers:**
- None
---
*Phase: 05-admin-panel, Plan: 01*
*Completed: 2026-03-25*

View File

@@ -50,6 +50,7 @@ function carei_parse_env() {
*/ */
require_once CAREI_RESERVATION_PATH . 'includes/class-softra-api.php'; require_once CAREI_RESERVATION_PATH . 'includes/class-softra-api.php';
require_once CAREI_RESERVATION_PATH . 'includes/class-rest-proxy.php'; require_once CAREI_RESERVATION_PATH . 'includes/class-rest-proxy.php';
require_once CAREI_RESERVATION_PATH . 'includes/class-admin-panel.php';
/** /**
* Initialize plugin on plugins_loaded * Initialize plugin on plugins_loaded
@@ -73,6 +74,9 @@ add_action( 'plugins_loaded', function () {
// Initialize REST proxy // Initialize REST proxy
new Carei_REST_Proxy(); new Carei_REST_Proxy();
// Initialize admin panel
new Carei_Admin_Panel();
} ); } );
/** /**

View File

@@ -0,0 +1,412 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Carei_Admin_Panel {
const POST_TYPE = 'carei_reservation';
const META_PREFIX = '_carei_';
private static $statuses = array(
'nowe' => array( 'label' => 'Nowe', 'color' => '#2F2482' ),
'przeczytane' => array( 'label' => 'Przeczytane', 'color' => '#f59e0b' ),
'zrealizowane' => array( 'label' => 'Zrealizowane', 'color' => '#22c55e' ),
);
public function __construct() {
add_action( 'init', array( $this, 'register_post_type' ) );
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( $this, 'admin_columns' ) );
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( $this, 'render_column' ), 10, 2 );
add_action( 'restrict_manage_posts', array( $this, 'status_filter_dropdown' ) );
add_action( 'pre_get_posts', array( $this, 'filter_by_status' ) );
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
add_action( 'save_post_' . self::POST_TYPE, array( $this, 'save_meta_box' ), 10, 2 );
add_action( 'edit_form_after_title', array( $this, 'auto_mark_read' ) );
add_action( 'admin_head', array( $this, 'admin_styles' ) );
}
public function register_post_type() {
register_post_type( self::POST_TYPE, array(
'labels' => array(
'name' => 'Rezerwacje',
'singular_name' => 'Rezerwacja',
'menu_name' => 'Rezerwacje',
'all_items' => 'Wszystkie rezerwacje',
'view_item' => 'Zobacz rezerwację',
'edit_item' => 'Szczegóły rezerwacji',
'search_items' => 'Szukaj rezerwacji',
'not_found' => 'Nie znaleziono rezerwacji',
'not_found_in_trash' => 'Brak rezerwacji w koszu',
),
'public' => false,
'show_ui' => true,
'show_in_menu' => true,
'menu_icon' => 'dashicons-car',
'menu_position' => 26,
'supports' => array( 'title' ),
'capability_type' => 'post',
'has_archive' => false,
'hierarchical' => false,
'show_in_rest' => false,
) );
}
// ─── Admin Columns ──────────────────────────────────────────
public function admin_columns( $columns ) {
return array(
'cb' => '<input type="checkbox" />',
'reservation_no' => 'Nr rezerwacji',
'client' => 'Klient',
'segment' => 'Segment',
'dates' => 'Daty',
'branch' => 'Oddział',
'carei_status' => 'Status',
'date' => 'Data',
);
}
public function render_column( $column, $post_id ) {
switch ( $column ) {
case 'reservation_no':
$no = get_post_meta( $post_id, self::META_PREFIX . 'reservation_no', true ) ?: '—';
printf(
'<a class="row-title" href="%s"><strong>%s</strong></a>',
esc_url( get_edit_post_link( $post_id ) ),
esc_html( $no )
);
break;
case 'client':
$first = get_post_meta( $post_id, self::META_PREFIX . 'first_name', true );
$last = get_post_meta( $post_id, self::META_PREFIX . 'last_name', true );
echo esc_html( trim( $first . ' ' . $last ) ?: '—' );
break;
case 'segment':
echo esc_html( get_post_meta( $post_id, self::META_PREFIX . 'segment', true ) ?: '—' );
break;
case 'dates':
$from = get_post_meta( $post_id, self::META_PREFIX . 'date_from', true );
$to = get_post_meta( $post_id, self::META_PREFIX . 'date_to', true );
if ( $from && $to ) {
$from_fmt = date_i18n( 'd.m.Y H:i', strtotime( $from ) );
$to_fmt = date_i18n( 'd.m.Y H:i', strtotime( $to ) );
echo esc_html( $from_fmt . ' — ' . $to_fmt );
} else {
echo '—';
}
break;
case 'branch':
echo esc_html( get_post_meta( $post_id, self::META_PREFIX . 'pickup_branch', true ) ?: '—' );
break;
case 'carei_status':
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'nowe';
$info = isset( self::$statuses[ $status ] ) ? self::$statuses[ $status ] : self::$statuses['nowe'];
printf(
'<span class="carei-status-badge" style="background:%s;">%s</span>',
esc_attr( $info['color'] ),
esc_html( $info['label'] )
);
break;
}
}
// ─── Status Filter ──────────────────────────────────────────
public function status_filter_dropdown( $post_type ) {
if ( $post_type !== self::POST_TYPE ) {
return;
}
$current = isset( $_GET['carei_status'] ) ? sanitize_text_field( $_GET['carei_status'] ) : '';
echo '<select name="carei_status">';
echo '<option value="">Wszystkie statusy</option>';
foreach ( self::$statuses as $key => $info ) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr( $key ),
selected( $current, $key, false ),
esc_html( $info['label'] )
);
}
echo '</select>';
}
public function filter_by_status( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ( $query->get( 'post_type' ) !== self::POST_TYPE ) ) {
return;
}
if ( ! empty( $_GET['carei_status'] ) ) {
$status = sanitize_text_field( $_GET['carei_status'] );
if ( isset( self::$statuses[ $status ] ) ) {
$query->set( 'meta_query', array(
array(
'key' => self::META_PREFIX . 'status',
'value' => $status,
),
) );
}
}
// Default sort by date desc
if ( ! $query->get( 'orderby' ) ) {
$query->set( 'orderby', 'date' );
$query->set( 'order', 'DESC' );
}
}
// ─── Meta Box ───────────────────────────────────────────────
public function add_meta_boxes() {
add_meta_box(
'carei_reservation_details',
'Szczegóły rezerwacji',
array( $this, 'render_meta_box' ),
self::POST_TYPE,
'normal',
'high'
);
}
public function render_meta_box( $post ) {
wp_nonce_field( 'carei_save_reservation', 'carei_reservation_nonce' );
$meta = array(
'reservation_no' => get_post_meta( $post->ID, self::META_PREFIX . 'reservation_no', true ),
'reservation_id' => get_post_meta( $post->ID, self::META_PREFIX . 'reservation_id', true ),
'customer_id' => get_post_meta( $post->ID, self::META_PREFIX . 'customer_id', true ),
'segment' => get_post_meta( $post->ID, self::META_PREFIX . 'segment', true ),
'date_from' => get_post_meta( $post->ID, self::META_PREFIX . 'date_from', true ),
'date_to' => get_post_meta( $post->ID, self::META_PREFIX . 'date_to', true ),
'pickup_branch' => get_post_meta( $post->ID, self::META_PREFIX . 'pickup_branch', true ),
'return_branch' => get_post_meta( $post->ID, self::META_PREFIX . 'return_branch', true ),
'first_name' => get_post_meta( $post->ID, self::META_PREFIX . 'first_name', true ),
'last_name' => get_post_meta( $post->ID, self::META_PREFIX . 'last_name', true ),
'email' => get_post_meta( $post->ID, self::META_PREFIX . 'email', true ),
'phone' => get_post_meta( $post->ID, self::META_PREFIX . 'phone', true ),
'pesel' => get_post_meta( $post->ID, self::META_PREFIX . 'pesel', true ),
'address' => get_post_meta( $post->ID, self::META_PREFIX . 'address', true ),
'extras' => get_post_meta( $post->ID, self::META_PREFIX . 'extras', true ),
'comments' => get_post_meta( $post->ID, self::META_PREFIX . 'comments', true ),
'status' => get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'nowe',
);
$address = $meta['address'] ? json_decode( $meta['address'], true ) : null;
$address_str = '';
if ( $address ) {
$parts = array_filter( array(
isset( $address['street'] ) ? $address['street'] : '',
isset( $address['zipCode'] ) ? $address['zipCode'] : '',
isset( $address['city'] ) ? $address['city'] : '',
) );
$address_str = implode( ', ', $parts );
}
$extras = $meta['extras'] ? json_decode( $meta['extras'], true ) : array();
$extras_str = '';
if ( is_array( $extras ) && ! empty( $extras ) ) {
$names = array_map( function ( $e ) {
return isset( $e['name'] ) ? $e['name'] : '';
}, $extras );
$extras_str = implode( ', ', array_filter( $names ) );
}
$from_fmt = $meta['date_from'] ? date_i18n( 'd.m.Y H:i', strtotime( $meta['date_from'] ) ) : '—';
$to_fmt = $meta['date_to'] ? date_i18n( 'd.m.Y H:i', strtotime( $meta['date_to'] ) ) : '—';
?>
<table class="carei-meta-table">
<tr><th>Nr rezerwacji</th><td><?php echo esc_html( $meta['reservation_no'] ?: '—' ); ?></td></tr>
<tr><th>ID rezerwacji (Softra)</th><td><?php echo esc_html( $meta['reservation_id'] ?: '—' ); ?></td></tr>
<tr><th>ID klienta (Softra)</th><td><?php echo esc_html( $meta['customer_id'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Segment</th><td><?php echo esc_html( $meta['segment'] ?: '—' ); ?></td></tr>
<tr><th>Data od</th><td><?php echo esc_html( $from_fmt ); ?></td></tr>
<tr><th>Data do</th><td><?php echo esc_html( $to_fmt ); ?></td></tr>
<tr><th>Oddział odbioru</th><td><?php echo esc_html( $meta['pickup_branch'] ?: '—' ); ?></td></tr>
<tr><th>Oddział zwrotu</th><td><?php echo esc_html( $meta['return_branch'] ?: $meta['pickup_branch'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Imię</th><td><?php echo esc_html( $meta['first_name'] ?: '—' ); ?></td></tr>
<tr><th>Nazwisko</th><td><?php echo esc_html( $meta['last_name'] ?: '—' ); ?></td></tr>
<tr><th>Email</th><td><?php echo esc_html( $meta['email'] ?: '—' ); ?></td></tr>
<tr><th>Telefon</th><td><?php echo esc_html( $meta['phone'] ?: '—' ); ?></td></tr>
<tr><th>PESEL</th><td><?php echo esc_html( $meta['pesel'] ?: '—' ); ?></td></tr>
<tr><th>Adres</th><td><?php echo esc_html( $address_str ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Opcje dodatkowe</th><td><?php echo esc_html( $extras_str ?: 'Brak' ); ?></td></tr>
<tr><th>Wiadomość</th><td><?php echo esc_html( $meta['comments'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr>
<th>Status</th>
<td>
<select name="carei_status">
<?php foreach ( self::$statuses as $key => $info ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $meta['status'], $key ); ?>>
<?php echo esc_html( $info['label'] ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
</table>
<?php
}
public function save_meta_box( $post_id, $post ) {
if ( ! isset( $_POST['carei_reservation_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( $_POST['carei_reservation_nonce'], 'carei_save_reservation' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( isset( $_POST['carei_status'] ) ) {
$status = sanitize_text_field( $_POST['carei_status'] );
if ( isset( self::$statuses[ $status ] ) ) {
update_post_meta( $post_id, self::META_PREFIX . 'status', $status );
}
}
}
// ─── Auto Mark Read ─────────────────────────────────────────
public function auto_mark_read( $post ) {
if ( $post->post_type !== self::POST_TYPE ) {
return;
}
$status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true );
if ( $status === 'nowe' ) {
update_post_meta( $post->ID, self::META_PREFIX . 'status', 'przeczytane' );
}
}
// ─── Admin Styles ───────────────────────────────────────────
public function admin_styles() {
$screen = get_current_screen();
if ( ! $screen || $screen->post_type !== self::POST_TYPE ) {
return;
}
?>
<style>
.carei-status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
color: #fff;
font-size: 12px;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
}
.carei-meta-table {
width: 100%;
border-collapse: collapse;
}
.carei-meta-table th {
text-align: left;
padding: 8px 12px;
width: 180px;
color: #1d2327;
font-weight: 600;
vertical-align: top;
}
.carei-meta-table td {
padding: 8px 12px;
color: #50575e;
}
.carei-meta-table tr:nth-child(odd):not(.carei-meta-divider) {
background: #f9f9f9;
}
.carei-meta-divider td {
padding: 4px 0;
}
.carei-meta-divider hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 0;
}
.carei-meta-table select {
min-width: 180px;
}
/* Hide title input — auto-generated */
#post-body-content #titlediv { display: none; }
</style>
<?php
}
// ─── Save Reservation (called from REST proxy) ──────────────
public static function save_reservation( $booking_data, $api_result ) {
$reservation_no = isset( $api_result['reservationNo'] ) ? $api_result['reservationNo'] : '';
$reservation_id = isset( $api_result['reservationId'] ) ? $api_result['reservationId'] : '';
$driver = array();
if ( isset( $booking_data['drivers'] ) && is_array( $booking_data['drivers'] ) && ! empty( $booking_data['drivers'] ) ) {
$driver = $booking_data['drivers'][0];
}
$first_name = isset( $driver['firstName'] ) ? $driver['firstName'] : '';
$last_name = isset( $driver['lastName'] ) ? $driver['lastName'] : '';
$title = sprintf( 'Rezerwacja #%s — %s %s', $reservation_no ?: $reservation_id, $first_name, $last_name );
$post_id = wp_insert_post( array(
'post_type' => self::POST_TYPE,
'post_title' => trim( $title ),
'post_status' => 'publish',
), true );
if ( is_wp_error( $post_id ) ) {
error_log( 'Carei: Failed to save reservation — ' . $post_id->get_error_message() );
return false;
}
$pickup_branch = '';
if ( isset( $booking_data['pickUpLocation']['branchName'] ) ) {
$pickup_branch = $booking_data['pickUpLocation']['branchName'];
}
$return_branch = '';
if ( isset( $booking_data['returnLocation']['branchName'] ) ) {
$return_branch = $booking_data['returnLocation']['branchName'];
}
$segment = '';
if ( isset( $booking_data['carParameters']['categoryName'] ) ) {
$segment = $booking_data['carParameters']['categoryName'];
}
$meta = array(
'reservation_no' => $reservation_no,
'reservation_id' => $reservation_id,
'customer_id' => isset( $booking_data['customerId'] ) ? $booking_data['customerId'] : '',
'segment' => $segment,
'date_from' => isset( $booking_data['dateFrom'] ) ? $booking_data['dateFrom'] : '',
'date_to' => isset( $booking_data['dateTo'] ) ? $booking_data['dateTo'] : '',
'pickup_branch' => $pickup_branch,
'return_branch' => $return_branch,
'first_name' => $first_name,
'last_name' => $last_name,
'email' => isset( $driver['email'] ) ? $driver['email'] : '',
'phone' => isset( $driver['phone'] ) ? $driver['phone'] : '',
'pesel' => isset( $driver['pesel'] ) ? $driver['pesel'] : '',
'address' => isset( $driver['address'] ) ? wp_json_encode( $driver['address'] ) : '',
'extras' => isset( $booking_data['priceItems'] ) ? wp_json_encode( $booking_data['priceItems'] ) : '',
'comments' => isset( $booking_data['comments'] ) ? $booking_data['comments'] : '',
'status' => 'nowe',
'raw_response' => wp_json_encode( $api_result ),
);
foreach ( $meta as $key => $value ) {
update_post_meta( $post_id, self::META_PREFIX . $key, $value );
}
return $post_id;
}
}

View File

@@ -30,6 +30,13 @@ class Carei_REST_Proxy {
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
) ); ) );
// GET /segments-branches-map (cached segment→branches mapping)
register_rest_route( self::NAMESPACE, '/segments-branches-map', array(
'methods' => 'GET',
'callback' => array( $this, 'get_segments_branches_map' ),
'permission_callback' => '__return_true',
) );
// POST /car-classes // POST /car-classes
register_rest_route( self::NAMESPACE, '/car-classes', array( register_rest_route( self::NAMESPACE, '/car-classes', array(
'methods' => 'POST', 'methods' => 'POST',
@@ -99,6 +106,17 @@ class Carei_REST_Proxy {
), ),
) ); ) );
// POST /booking/cancel
register_rest_route( self::NAMESPACE, '/booking/cancel', array(
'methods' => 'POST',
'callback' => array( $this, 'cancel_booking' ),
'permission_callback' => array( $this, 'check_nonce' ),
'args' => array(
'reservationId' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'reason' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
),
) );
// GET /agreements // GET /agreements
register_rest_route( self::NAMESPACE, '/agreements', array( register_rest_route( self::NAMESPACE, '/agreements', array(
'methods' => 'GET', 'methods' => 'GET',
@@ -141,6 +159,14 @@ class Carei_REST_Proxy {
// ─── Callbacks ──────────────────────────────────────────────── // ─── Callbacks ────────────────────────────────────────────────
public function get_segments_branches_map( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_segments_branches_map() );
}
public function get_all_car_classes( WP_REST_Request $request ) { public function get_all_car_classes( WP_REST_Request $request ) {
$api = $this->api(); $api = $this->api();
if ( is_wp_error( $api ) ) { if ( is_wp_error( $api ) ) {
@@ -218,8 +244,15 @@ class Carei_REST_Proxy {
if ( is_wp_error( $api ) ) { if ( is_wp_error( $api ) ) {
return $api; return $api;
} }
$data = $request->get_json_params(); $data = $request->get_json_params();
return $this->respond( $api->make_booking( $data ) ); $result = $api->make_booking( $data );
// Save reservation to WP on success (fire-and-forget)
if ( ! is_wp_error( $result ) && isset( $result['success'] ) && $result['success'] ) {
Carei_Admin_Panel::save_reservation( $data, $result );
}
return $this->respond( $result );
} }
public function confirm_booking( WP_REST_Request $request ) { public function confirm_booking( WP_REST_Request $request ) {
@@ -232,6 +265,17 @@ class Carei_REST_Proxy {
) ); ) );
} }
public function cancel_booking( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->cancel_booking(
$request->get_param( 'reservationId' ),
$request->get_param( 'reason' )
) );
}
public function get_agreements( WP_REST_Request $request ) { public function get_agreements( WP_REST_Request $request ) {
$api = $this->api(); $api = $this->api();
if ( is_wp_error( $api ) ) { if ( is_wp_error( $api ) ) {