feat(13-protection-packages): Pakiety ochronne SOFT/PREMIUM z panelu WP

- Panel admina (wp-admin > Rezerwacje > Pakiety ochronne) do zarzadzania
  nazwami, cenami za dobe, aktywnoscia i opisami pakietow SOFT i PREMIUM
  (zapis w wp_options carei_protection_packages)
- REST endpoint GET /carei/v1/protection-packages zwracajacy aktywne pakiety
- Radio cards SOFT/PREMIUM w modalu rezerwacji nad pozycjami "Pakiety ochronne"
  z API (osobne zrodlo danych, separator wizualny)
- Radio z deselect (klik zaznaczonego odznacza), natywny input z accent-color
- Pakiet NIE wysylany w priceItems Softra (powodowalo HTTP 400) - zamiast tego
  doklejany do comments booking i zapisywany w _carei_protection_package meta
- Summary frontend dokorysowuje wiersz pakietu w tabeli cen i dolicza do
  total gross (grandGross = softraGross + protectionTotal)
- Plan 13-01 oznaczony jako superseded (klient zmienil zrodlo danych)
- Phase 13 Complete, Milestone v0.5 Complete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 00:45:24 +02:00
parent 9e4f47de25
commit 42efe93cdd
13 changed files with 987 additions and 25 deletions

View File

@@ -8,6 +8,8 @@ class Carei_Admin_Panel {
const POST_TYPE = 'carei_reservation';
const META_PREFIX = '_carei_';
const PROTECTION_OPTION = 'carei_protection_packages';
private static $statuses = array(
'nowe' => array( 'label' => 'Nowe', 'color' => '#2F2482' ),
'przeczytane' => array( 'label' => 'Przeczytane', 'color' => '#f59e0b' ),
@@ -24,6 +26,149 @@ class Carei_Admin_Panel {
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' ) );
add_action( 'admin_menu', array( $this, 'register_protection_packages_page' ) );
add_action( 'admin_post_carei_save_protection_packages', array( $this, 'handle_protection_packages_save' ) );
}
// ─── Protection Packages (SOFT / PREMIUM) ────────────────────
public static function get_protection_packages_defaults() {
return array(
'soft' => array(
'name' => 'Ubezpieczenie SOFT',
'pricePerDay' => 0,
'active' => true,
'description' => '',
),
'premium' => array(
'name' => 'Ubezpieczenie PREMIUM',
'pricePerDay' => 0,
'active' => true,
'description' => '',
),
);
}
public static function get_protection_packages() {
$defaults = self::get_protection_packages_defaults();
$stored = get_option( self::PROTECTION_OPTION, array() );
if ( ! is_array( $stored ) ) {
$stored = array();
}
$out = array();
foreach ( $defaults as $key => $def ) {
$item = isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ? $stored[ $key ] : array();
$out[ $key ] = array(
'name' => isset( $item['name'] ) && $item['name'] !== '' ? (string) $item['name'] : $def['name'],
'pricePerDay' => isset( $item['pricePerDay'] ) ? (float) $item['pricePerDay'] : (float) $def['pricePerDay'],
'active' => isset( $item['active'] ) ? (bool) $item['active'] : (bool) $def['active'],
'description' => isset( $item['description'] ) ? (string) $item['description'] : $def['description'],
);
}
return $out;
}
public function register_protection_packages_page() {
add_submenu_page(
'edit.php?post_type=' . self::POST_TYPE,
'Pakiety ochronne',
'Pakiety ochronne',
'manage_options',
'carei-protection-packages',
array( $this, 'render_protection_packages_page' )
);
}
public function render_protection_packages_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' );
}
$data = self::get_protection_packages();
$saved = isset( $_GET['carei_saved'] ) && $_GET['carei_saved'] === '1';
?>
<div class="wrap">
<h1>Pakiety ochronne</h1>
<p>Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji.</p>
<?php if ( $saved ) : ?>
<div class="notice notice-success is-dismissible"><p>Zapisano.</p></div>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="carei-protection-form">
<input type="hidden" name="action" value="carei_save_protection_packages">
<?php wp_nonce_field( 'carei_protection_packages', 'carei_protection_nonce' ); ?>
<?php foreach ( array( 'soft' => 'SOFT', 'premium' => 'PREMIUM' ) as $key => $label ) :
$pkg = $data[ $key ];
?>
<div class="carei-protection-card">
<h2>Pakiet <?php echo esc_html( $label ); ?></h2>
<table class="form-table">
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_name">Nazwa wyświetlana</label></th>
<td><input type="text" id="carei_<?php echo esc_attr( $key ); ?>_name" name="packages[<?php echo esc_attr( $key ); ?>][name]" value="<?php echo esc_attr( $pkg['name'] ); ?>" class="regular-text" required></td>
</tr>
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_price">Cena za dobę (zł)</label></th>
<td><input type="number" id="carei_<?php echo esc_attr( $key ); ?>_price" name="packages[<?php echo esc_attr( $key ); ?>][pricePerDay]" value="<?php echo esc_attr( $pkg['pricePerDay'] ); ?>" min="0" step="0.01" class="small-text" required></td>
</tr>
<tr>
<th>Status</th>
<td><label><input type="checkbox" name="packages[<?php echo esc_attr( $key ); ?>][active]" value="1" <?php checked( $pkg['active'] ); ?>> Aktywny (widoczny w modalu)</label></td>
</tr>
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_desc">Opis / zakres usług</label></th>
<td><textarea id="carei_<?php echo esc_attr( $key ); ?>_desc" name="packages[<?php echo esc_attr( $key ); ?>][description]" rows="3" cols="60" class="large-text"><?php echo esc_textarea( $pkg['description'] ); ?></textarea></td>
</tr>
</table>
</div>
<?php endforeach; ?>
<?php submit_button( 'Zapisz pakiety' ); ?>
</form>
</div>
<style>
.carei-protection-card { background:#fff; border:1px solid #c3c4c7; border-left:4px solid #2F2482; padding:10px 20px; margin:20px 0; }
.carei-protection-card h2 { margin-top:10px; color:#2F2482; }
</style>
<?php
}
public function handle_protection_packages_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' );
}
if ( ! isset( $_POST['carei_protection_nonce'] ) || ! wp_verify_nonce( $_POST['carei_protection_nonce'], 'carei_protection_packages' ) ) {
wp_die( 'Nieprawidłowy token.' );
}
$input = isset( $_POST['packages'] ) && is_array( $_POST['packages'] ) ? $_POST['packages'] : array();
$defaults = self::get_protection_packages_defaults();
$clean = array();
foreach ( $defaults as $key => $def ) {
$raw = isset( $input[ $key ] ) && is_array( $input[ $key ] ) ? $input[ $key ] : array();
$name = isset( $raw['name'] ) ? sanitize_text_field( wp_unslash( $raw['name'] ) ) : $def['name'];
$price = isset( $raw['pricePerDay'] ) ? (float) $raw['pricePerDay'] : 0;
if ( $price < 0 ) { $price = 0; }
$active = ! empty( $raw['active'] );
$desc = isset( $raw['description'] ) ? sanitize_textarea_field( wp_unslash( $raw['description'] ) ) : '';
$clean[ $key ] = array(
'name' => $name !== '' ? $name : $def['name'],
'pricePerDay' => $price,
'active' => $active,
'description' => $desc,
);
}
update_option( self::PROTECTION_OPTION, $clean );
$redirect = add_query_arg(
array(
'post_type' => self::POST_TYPE,
'page' => 'carei-protection-packages',
'carei_saved' => '1',
),
admin_url( 'edit.php' )
);
wp_safe_redirect( $redirect );
exit;
}
public function register_post_type() {
@@ -190,8 +335,19 @@ class Carei_Admin_Panel {
'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',
'protection' => get_post_meta( $post->ID, self::META_PREFIX . 'protection_package', true ),
);
$protection = $meta['protection'] ? json_decode( $meta['protection'], true ) : null;
$protection_str = '';
if ( is_array( $protection ) && ! empty( $protection['name'] ) ) {
$name = isset( $protection['name'] ) ? $protection['name'] : '';
$price = isset( $protection['pricePerDay'] ) ? (float) $protection['pricePerDay'] : 0;
$days = isset( $protection['days'] ) ? (int) $protection['days'] : 0;
$total = isset( $protection['total'] ) ? (float) $protection['total'] : ( $price * $days );
$protection_str = sprintf( '%s — %s zł/doba × %d = %s zł', $name, number_format( $price, 2, ',', ' ' ), $days, number_format( $total, 2, ',', ' ' ) );
}
$address = $meta['address'] ? json_decode( $meta['address'], true ) : null;
$address_str = '';
if ( $address ) {
@@ -235,6 +391,7 @@ class Carei_Admin_Panel {
<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>Pakiet ochronny</th><td><?php echo esc_html( $protection_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>
@@ -401,6 +558,9 @@ class Carei_Admin_Panel {
'comments' => isset( $booking_data['comments'] ) ? $booking_data['comments'] : '',
'status' => 'nowe',
'raw_response' => wp_json_encode( $api_result ),
'protection_package' => ( isset( $booking_data['protectionPackage'] ) && is_array( $booking_data['protectionPackage'] ) )
? wp_json_encode( $booking_data['protectionPackage'] )
: '',
);
foreach ( $meta as $key => $value ) {

View File

@@ -152,6 +152,10 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__divider"><span>Pakiety ochronne</span></div>
<div class="carei-form__section">
<div class="carei-form__row carei-form__row--protection-packages" id="carei-protection-packages-container">
<!-- Dynamicznie z panelu WP (SOFT, PREMIUM) -->
</div>
<div class="carei-form__protection-divider" aria-hidden="true"></div>
<div class="carei-form__row" id="carei-insurance-container">
<!-- Dynamicznie z API pricelist -->
</div>

View File

@@ -130,6 +130,13 @@ class Carei_REST_Proxy {
'callback' => array( $this, 'get_agreements' ),
'permission_callback' => '__return_true',
) );
// GET /protection-packages
register_rest_route( self::NAMESPACE, '/protection-packages', array(
'methods' => 'GET',
'callback' => array( $this, 'get_protection_packages' ),
'permission_callback' => '__return_true',
) );
}
/**
@@ -298,4 +305,20 @@ class Carei_REST_Proxy {
}
return $this->respond( $api->get_agreements() );
}
public function get_protection_packages( WP_REST_Request $request ) {
$all = Carei_Admin_Panel::get_protection_packages();
$out = array( 'soft' => null, 'premium' => null );
foreach ( array( 'soft', 'premium' ) as $key ) {
if ( isset( $all[ $key ] ) && ! empty( $all[ $key ]['active'] ) ) {
$out[ $key ] = array(
'key' => $key,
'name' => $all[ $key ]['name'],
'pricePerDay' => (float) $all[ $key ]['pricePerDay'],
'description' => $all[ $key ]['description'],
);
}
}
return rest_ensure_response( $out );
}
}