Add pricing system with tiers, seasons, and calculator (v0.2.0)
- Create PricingTier enum for short/mid/long-term pricing - Add Season class for seasonal pricing with date ranges - Implement Calculator for price calculations with breakdown - Add pricing meta box to Room post type - Create Seasons admin page for managing seasonal pricing - Add Pricing settings tab with tier thresholds - Support weekend surcharges and configurable weekend days - Add price column to room list admin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
45
CHANGELOG.md
45
CHANGELOG.md
@@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-01-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Pricing System with three tiers:
|
||||||
|
- Short-term (nightly) pricing for stays up to 6 nights
|
||||||
|
- Mid-term (weekly) pricing for stays 7-27 nights
|
||||||
|
- Long-term (monthly) pricing for stays 28+ nights
|
||||||
|
- PricingTier enum class with automatic tier detection
|
||||||
|
- Season class for seasonal pricing management
|
||||||
|
- Date range support (MM-DD format)
|
||||||
|
- Year-spanning seasons (e.g., winter holidays Dec-Jan)
|
||||||
|
- Price modifier (multiplier) per season
|
||||||
|
- Priority system for overlapping seasons
|
||||||
|
- Active/inactive status toggle
|
||||||
|
- Calculator class for price calculations
|
||||||
|
- Automatic tier detection based on stay duration
|
||||||
|
- Seasonal price adjustments
|
||||||
|
- Weekend surcharge support
|
||||||
|
- Price breakdown for detailed invoicing
|
||||||
|
- Currency formatting with symbol/suffix support
|
||||||
|
- Pricing meta box on Room edit screen
|
||||||
|
- Base prices for each tier (nightly, weekly, monthly)
|
||||||
|
- Weekend surcharge field
|
||||||
|
- Link to pricing settings
|
||||||
|
- Pricing Settings tab in plugin settings
|
||||||
|
- Configurable tier thresholds
|
||||||
|
- Weekend days selection
|
||||||
|
- Quick view of configured seasons
|
||||||
|
- Seasons admin page (WP BnB > Seasons)
|
||||||
|
- List view with all seasons
|
||||||
|
- Add/Edit season form
|
||||||
|
- Delete confirmation
|
||||||
|
- Create default seasons option
|
||||||
|
- Price column in room list admin
|
||||||
|
- Admin CSS for pricing UI
|
||||||
|
- Admin JavaScript for pricing interactions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Room post type now includes pricing fields
|
||||||
|
- Plugin settings page has new Pricing tab
|
||||||
|
- Enhanced asset localization with pricing i18n strings
|
||||||
|
|
||||||
## [0.1.0] - 2026-01-31
|
## [0.1.0] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -80,5 +124,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Input sanitization and output escaping
|
- Input sanitization and output escaping
|
||||||
- Server secret masking in license settings
|
- Server secret masking in license settings
|
||||||
|
|
||||||
|
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.2.0
|
||||||
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.1.0
|
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.1.0
|
||||||
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.0.1
|
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.0.1
|
||||||
|
|||||||
65
CLAUDE.md
65
CLAUDE.md
@@ -38,15 +38,6 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||||
|
|
||||||
### Version 0.1.0 - Core Data Structures
|
|
||||||
|
|
||||||
- Custom Post Type: Buildings (address, contact, description, images)
|
|
||||||
- Custom Post Type: Rooms (building reference, capacity, amenities)
|
|
||||||
- Custom Taxonomy: Room Types (Standard, Suite, Family, etc.)
|
|
||||||
- Custom Taxonomy: Amenities (WiFi, Parking, Breakfast, etc.)
|
|
||||||
- Gutenberg blocks for Buildings and Rooms
|
|
||||||
- See `PLAN.md` for full implementation roadmap
|
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **Language:** PHP 8.3.x
|
||||||
@@ -215,11 +206,17 @@ wp-bnb/
|
|||||||
│ └── release.yml # CI/CD release pipeline
|
│ └── release.yml # CI/CD release pipeline
|
||||||
├── src/ # PHP source code (PSR-4: Magdev\WpBnb)
|
├── src/ # PHP source code (PSR-4: Magdev\WpBnb)
|
||||||
│ ├── Plugin.php # Main plugin singleton
|
│ ├── Plugin.php # Main plugin singleton
|
||||||
|
│ ├── Admin/ # Admin pages
|
||||||
|
│ │ └── Seasons.php # Seasons management page
|
||||||
│ ├── License/
|
│ ├── License/
|
||||||
│ │ └── Manager.php # License management
|
│ │ └── Manager.php # License management
|
||||||
│ ├── PostTypes/ # Custom post types
|
│ ├── PostTypes/ # Custom post types
|
||||||
│ │ ├── Building.php # Building post type
|
│ │ ├── Building.php # Building post type
|
||||||
│ │ └── Room.php # Room post type
|
│ │ └── Room.php # Room post type
|
||||||
|
│ ├── Pricing/ # Pricing system
|
||||||
|
│ │ ├── Calculator.php # Price calculation
|
||||||
|
│ │ ├── PricingTier.php # Pricing tier enum
|
||||||
|
│ │ └── Season.php # Seasonal pricing
|
||||||
│ └── Taxonomies/ # Custom taxonomies
|
│ └── Taxonomies/ # Custom taxonomies
|
||||||
│ ├── Amenity.php # Amenity taxonomy (tags)
|
│ ├── Amenity.php # Amenity taxonomy (tags)
|
||||||
│ └── RoomType.php # Room type taxonomy (categories)
|
│ └── RoomType.php # Room type taxonomy (categories)
|
||||||
@@ -350,3 +347,53 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Gallery implementation uses `wp.media` frame with multiple selection
|
- Gallery implementation uses `wp.media` frame with multiple selection
|
||||||
- Admin assets need conditional loading based on both hook suffix and post type
|
- Admin assets need conditional loading based on both hook suffix and post type
|
||||||
- Status badges use inline styles for color coding (avoiding extra CSS complexity)
|
- Status badges use inline styles for color coding (avoiding extra CSS complexity)
|
||||||
|
|
||||||
|
### 2026-01-31 - Version 0.2.0 (Pricing System)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Pricing/PricingTier.php` enum class
|
||||||
|
- SHORT_TERM, MID_TERM, LONG_TERM cases
|
||||||
|
- Labels, units, default thresholds
|
||||||
|
- `fromNights()` for automatic tier detection
|
||||||
|
- Created `src/Pricing/Season.php` class
|
||||||
|
- Seasonal pricing with date ranges (MM-DD format)
|
||||||
|
- Year-spanning seasons support (e.g., Dec 20 to Jan 6)
|
||||||
|
- Price modifier (multiplier) per season
|
||||||
|
- Priority system for overlapping seasons
|
||||||
|
- CRUD operations stored in wp_options
|
||||||
|
- Default seasons: High Season, Winter Holidays, Low Season
|
||||||
|
- Created `src/Pricing/Calculator.php` class
|
||||||
|
- Price calculation with tier detection
|
||||||
|
- Seasonal modifier application
|
||||||
|
- Weekend surcharge support
|
||||||
|
- Detailed price breakdown
|
||||||
|
- Currency formatting with symbol/suffix
|
||||||
|
- Added pricing meta box to Room post type
|
||||||
|
- Base prices for each tier
|
||||||
|
- Weekend surcharge field
|
||||||
|
- Link to pricing settings
|
||||||
|
- Price column in admin list
|
||||||
|
- Added Pricing tab to settings page
|
||||||
|
- Configurable tier thresholds
|
||||||
|
- Weekend days selection (checkboxes)
|
||||||
|
- Quick view of configured seasons
|
||||||
|
- Created `src/Admin/Seasons.php` admin page
|
||||||
|
- List view with all seasons
|
||||||
|
- Add/Edit season forms
|
||||||
|
- Delete with confirmation
|
||||||
|
- Create default seasons option
|
||||||
|
- Date formatting for display
|
||||||
|
- Updated admin.css with pricing styles
|
||||||
|
- Updated admin.js with pricing interactions
|
||||||
|
- Updated Plugin.php to register Seasons admin page
|
||||||
|
- Updated version to 0.2.0
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- PHP 8.1+ enums work well for pricing tiers with methods
|
||||||
|
- Season date ranges use MM-DD format for annual recurrence
|
||||||
|
- Year-spanning seasons require special comparison logic
|
||||||
|
- Price modifiers as multipliers are more flexible than percentages
|
||||||
|
- Calculator class separates concerns from post type class
|
||||||
|
- Weekend days stored as comma-separated string in options
|
||||||
|
|||||||
24
PLAN.md
24
PLAN.md
@@ -38,26 +38,26 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- WiFi, Parking, Breakfast, etc.
|
- WiFi, Parking, Breakfast, etc.
|
||||||
- Non-hierarchical (tags)
|
- Non-hierarchical (tags)
|
||||||
|
|
||||||
## Phase 2: Pricing System (v0.2.0)
|
## Phase 2: Pricing System (v0.2.0) - Complete
|
||||||
|
|
||||||
### Pricing Classes
|
### Pricing Classes
|
||||||
|
|
||||||
- [ ] Short-term pricing (per night, 1-6 nights)
|
- [x] Short-term pricing (per night, 1-6 nights)
|
||||||
- [ ] Mid-term pricing (per week, 1-4 weeks)
|
- [x] Mid-term pricing (per week, 1-4 weeks)
|
||||||
- [ ] Long-term pricing (per month, 1+ months)
|
- [x] Long-term pricing (per month, 1+ months)
|
||||||
|
|
||||||
### Price Configuration
|
### Price Configuration
|
||||||
|
|
||||||
- [ ] Room-level price settings
|
- [x] Room-level price settings
|
||||||
- [ ] Seasonal pricing periods
|
- [x] Seasonal pricing periods
|
||||||
- [ ] Weekend/weekday differentiation
|
- [x] Weekend/weekday differentiation
|
||||||
- [ ] Currency formatting and display
|
- [x] Currency formatting and display
|
||||||
|
|
||||||
### Price Calculation
|
### Price Calculation
|
||||||
|
|
||||||
- [ ] Automatic tier detection based on duration
|
- [x] Automatic tier detection based on duration
|
||||||
- [ ] Price breakdown display
|
- [x] Price breakdown display
|
||||||
- [ ] Discount handling
|
- [x] Discount handling (via seasonal modifiers)
|
||||||
|
|
||||||
## Phase 3: Booking System (v0.3.0)
|
## Phase 3: Booking System (v0.3.0)
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| ------- | --------------- | -------- |
|
| ------- | --------------- | -------- |
|
||||||
| 0.0.1 | Initial setup | Complete |
|
| 0.0.1 | Initial setup | Complete |
|
||||||
| 0.1.0 | Data structures | Complete |
|
| 0.1.0 | Data structures | Complete |
|
||||||
| 0.2.0 | Pricing | TBD |
|
| 0.2.0 | Pricing | Complete |
|
||||||
| 0.3.0 | Bookings | TBD |
|
| 0.3.0 | Bookings | TBD |
|
||||||
| 0.4.0 | Guests | TBD |
|
| 0.4.0 | Guests | TBD |
|
||||||
| 0.5.0 | Services | TBD |
|
| 0.5.0 | Services | TBD |
|
||||||
|
|||||||
@@ -191,3 +191,135 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Price column */
|
||||||
|
.column-price {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-price {
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pricing Meta Box */
|
||||||
|
.bnb-pricing-table h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-pricing-table tr:first-child th,
|
||||||
|
.bnb-pricing-table tr:first-child td {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-price-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-price-input-wrapper input {
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-price-unit {
|
||||||
|
color: #646970;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seasons Page */
|
||||||
|
.bnb-seasons-description {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-left: 4px solid #72aee6;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-seasons-description p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-seasons {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-seasons p {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-seasons .button {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Season List Columns */
|
||||||
|
.column-name {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-dates {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-modifier {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-priority {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-status {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-modifier-visual {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-active {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #00a32a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-active::before {
|
||||||
|
content: "\f147";
|
||||||
|
font-family: dashicons;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-inactive {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-inactive::before {
|
||||||
|
content: "\f460";
|
||||||
|
font-family: dashicons;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Season Form */
|
||||||
|
.bnb-season-form {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-season-form .form-table th {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-season-form input[type="text"].small-text {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -183,10 +183,107 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize pricing settings functionality.
|
||||||
|
*/
|
||||||
|
function initPricingSettings() {
|
||||||
|
var $midTermInput = $('#wp_bnb_mid_term_max_nights');
|
||||||
|
var $longTermMin = $('#wp-bnb-long-term-min');
|
||||||
|
|
||||||
|
if (!$midTermInput.length || !$longTermMin.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update long-term minimum display when mid-term max changes.
|
||||||
|
$midTermInput.on('input', function() {
|
||||||
|
$longTermMin.text($(this).val());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize season form validation.
|
||||||
|
*/
|
||||||
|
function initSeasonForm() {
|
||||||
|
var $form = $('.bnb-season-form');
|
||||||
|
|
||||||
|
if (!$form.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format on input.
|
||||||
|
$form.find('#season_start_date, #season_end_date').on('input', function() {
|
||||||
|
var value = $(this).val();
|
||||||
|
var isValid = /^\d{2}-\d{2}$/.test(value);
|
||||||
|
|
||||||
|
if (value.length > 0 && !isValid) {
|
||||||
|
$(this).css('border-color', '#d63638');
|
||||||
|
} else {
|
||||||
|
$(this).css('border-color', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate modifier range.
|
||||||
|
$form.find('#season_modifier').on('input', function() {
|
||||||
|
var value = parseFloat($(this).val());
|
||||||
|
var $preview = $(this).siblings('.bnb-modifier-preview');
|
||||||
|
|
||||||
|
if ($preview.length === 0) {
|
||||||
|
$preview = $('<span class="bnb-modifier-preview"></span>');
|
||||||
|
$(this).after($preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNaN(value) && value > 0) {
|
||||||
|
var percentage = ((value - 1) * 100).toFixed(0);
|
||||||
|
var text = '';
|
||||||
|
if (percentage > 0) {
|
||||||
|
text = '+' + percentage + '% ' + (wpBnbAdmin.i18n.increase || 'increase');
|
||||||
|
$preview.css('color', '#d63638');
|
||||||
|
} else if (percentage < 0) {
|
||||||
|
text = percentage + '% ' + (wpBnbAdmin.i18n.discount || 'discount');
|
||||||
|
$preview.css('color', '#00a32a');
|
||||||
|
} else {
|
||||||
|
text = wpBnbAdmin.i18n.normalPrice || 'Normal price';
|
||||||
|
$preview.css('color', '#646970');
|
||||||
|
}
|
||||||
|
$preview.text(' = ' + text);
|
||||||
|
} else {
|
||||||
|
$preview.text('');
|
||||||
|
}
|
||||||
|
}).trigger('input');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize pricing meta box interactions.
|
||||||
|
*/
|
||||||
|
function initPricingMetaBox() {
|
||||||
|
var $pricingTable = $('.bnb-pricing-table');
|
||||||
|
|
||||||
|
if (!$pricingTable.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight empty required prices.
|
||||||
|
$pricingTable.find('input[type="number"]').on('blur', function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var value = $input.val();
|
||||||
|
var isShortTerm = $input.attr('id').indexOf('short_term') !== -1;
|
||||||
|
|
||||||
|
// Short-term price is recommended.
|
||||||
|
if (isShortTerm && (value === '' || parseFloat(value) <= 0)) {
|
||||||
|
$input.css('background-color', '#fcf0f1');
|
||||||
|
} else {
|
||||||
|
$input.css('background-color', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
initRoomGallery();
|
initRoomGallery();
|
||||||
|
initPricingSettings();
|
||||||
|
initSeasonForm();
|
||||||
|
initPricingMetaBox();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
419
src/Admin/Seasons.php
Normal file
419
src/Admin/Seasons.php
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Seasons admin page.
|
||||||
|
*
|
||||||
|
* Handles the admin interface for managing seasonal pricing.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Admin;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seasons admin page class.
|
||||||
|
*/
|
||||||
|
final class Seasons {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the admin page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'admin_menu', array( self::class, 'register_menu' ) );
|
||||||
|
add_action( 'admin_init', array( self::class, 'handle_actions' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the submenu page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_menu(): void {
|
||||||
|
add_submenu_page(
|
||||||
|
'wp-bnb',
|
||||||
|
__( 'Seasons', 'wp-bnb' ),
|
||||||
|
__( 'Seasons', 'wp-bnb' ),
|
||||||
|
'manage_options',
|
||||||
|
'wp-bnb-seasons',
|
||||||
|
array( self::class, 'render_page' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle form actions.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function handle_actions(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just checking page.
|
||||||
|
if ( ! isset( $_GET['page'] ) || 'wp-bnb-seasons' !== $_GET['page'] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle delete action.
|
||||||
|
if ( isset( $_GET['action'], $_GET['season_id'], $_GET['_wpnonce'] )
|
||||||
|
&& 'delete' === $_GET['action']
|
||||||
|
&& wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'delete_season' )
|
||||||
|
) {
|
||||||
|
$season_id = sanitize_text_field( wp_unslash( $_GET['season_id'] ) );
|
||||||
|
Season::delete( $season_id );
|
||||||
|
|
||||||
|
wp_safe_redirect( admin_url( 'admin.php?page=wp-bnb-seasons&deleted=1' ) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle create default seasons.
|
||||||
|
if ( isset( $_GET['action'], $_GET['_wpnonce'] )
|
||||||
|
&& 'create_defaults' === $_GET['action']
|
||||||
|
&& wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'create_default_seasons' )
|
||||||
|
) {
|
||||||
|
Season::createDefaults();
|
||||||
|
|
||||||
|
wp_safe_redirect( admin_url( 'admin.php?page=wp-bnb-seasons&defaults_created=1' ) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission.
|
||||||
|
if ( isset( $_POST['wp_bnb_season_nonce'] )
|
||||||
|
&& wp_verify_nonce( sanitize_key( $_POST['wp_bnb_season_nonce'] ), 'save_season' )
|
||||||
|
) {
|
||||||
|
self::save_season();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a season from form data.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function save_season(): void {
|
||||||
|
$data = array(
|
||||||
|
'id' => isset( $_POST['season_id'] ) && '' !== $_POST['season_id']
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['season_id'] ) )
|
||||||
|
: wp_generate_uuid4(),
|
||||||
|
'name' => isset( $_POST['season_name'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['season_name'] ) )
|
||||||
|
: '',
|
||||||
|
'start_date' => isset( $_POST['season_start_date'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['season_start_date'] ) )
|
||||||
|
: '',
|
||||||
|
'end_date' => isset( $_POST['season_end_date'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['season_end_date'] ) )
|
||||||
|
: '',
|
||||||
|
'modifier' => isset( $_POST['season_modifier'] )
|
||||||
|
? floatval( $_POST['season_modifier'] )
|
||||||
|
: 1.0,
|
||||||
|
'priority' => isset( $_POST['season_priority'] )
|
||||||
|
? absint( $_POST['season_priority'] )
|
||||||
|
: 0,
|
||||||
|
'active' => isset( $_POST['season_active'] ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$season = new Season( $data );
|
||||||
|
Season::save( $season );
|
||||||
|
|
||||||
|
$is_edit = isset( $_POST['season_id'] ) && '' !== $_POST['season_id'];
|
||||||
|
$message = $is_edit ? 'updated' : 'created';
|
||||||
|
|
||||||
|
wp_safe_redirect( admin_url( 'admin.php?page=wp-bnb-seasons&' . $message . '=1' ) );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the admin page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_page(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
|
||||||
|
$action = isset( $_GET['action'] ) ? sanitize_key( $_GET['action'] ) : 'list';
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
|
||||||
|
$season_id = isset( $_GET['season_id'] ) ? sanitize_text_field( wp_unslash( $_GET['season_id'] ) ) : '';
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1 class="wp-heading-inline"><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h1>
|
||||||
|
|
||||||
|
<?php if ( 'list' === $action || '' === $action ) : ?>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons&action=add' ) ); ?>" class="page-title-action">
|
||||||
|
<?php esc_html_e( 'Add Season', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<hr class="wp-header-end">
|
||||||
|
|
||||||
|
<?php self::render_notices(); ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
switch ( $action ) {
|
||||||
|
case 'add':
|
||||||
|
self::render_form();
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
self::render_form( $season_id );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
self::render_list();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render admin notices.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_notices(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
|
||||||
|
if ( isset( $_GET['deleted'] ) ) {
|
||||||
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Season deleted successfully.', 'wp-bnb' ) . '</p></div>';
|
||||||
|
}
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
|
||||||
|
if ( isset( $_GET['created'] ) ) {
|
||||||
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Season created successfully.', 'wp-bnb' ) . '</p></div>';
|
||||||
|
}
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
|
||||||
|
if ( isset( $_GET['updated'] ) ) {
|
||||||
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Season updated successfully.', 'wp-bnb' ) . '</p></div>';
|
||||||
|
}
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
|
||||||
|
if ( isset( $_GET['defaults_created'] ) ) {
|
||||||
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Default seasons created successfully.', 'wp-bnb' ) . '</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the seasons list.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_list(): void {
|
||||||
|
$seasons = Season::all();
|
||||||
|
?>
|
||||||
|
<div class="bnb-seasons-description">
|
||||||
|
<p><?php esc_html_e( 'Seasonal pricing allows you to automatically adjust room rates based on the time of year. Define seasons with date ranges and price modifiers.', 'wp-bnb' ); ?></p>
|
||||||
|
<p><?php esc_html_e( 'Seasons with higher priority take precedence when dates overlap (e.g., Winter Holidays overrides Low Season).', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( empty( $seasons ) ) : ?>
|
||||||
|
<div class="bnb-no-seasons">
|
||||||
|
<p><?php esc_html_e( 'No seasons have been configured yet.', 'wp-bnb' ); ?></p>
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons&action=add' ) ); ?>" class="button button-primary">
|
||||||
|
<?php esc_html_e( 'Add Your First Season', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=wp-bnb-seasons&action=create_defaults' ), 'create_default_seasons' ) ); ?>" class="button">
|
||||||
|
<?php esc_html_e( 'Create Default Seasons', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<table class="wp-list-table widefat fixed striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="column-name"><?php esc_html_e( 'Season Name', 'wp-bnb' ); ?></th>
|
||||||
|
<th scope="col" class="column-dates"><?php esc_html_e( 'Period', 'wp-bnb' ); ?></th>
|
||||||
|
<th scope="col" class="column-modifier"><?php esc_html_e( 'Price Modifier', 'wp-bnb' ); ?></th>
|
||||||
|
<th scope="col" class="column-priority"><?php esc_html_e( 'Priority', 'wp-bnb' ); ?></th>
|
||||||
|
<th scope="col" class="column-status"><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $seasons as $season ) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="column-name">
|
||||||
|
<strong>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons&action=edit&season_id=' . $season->id ) ); ?>">
|
||||||
|
<?php echo esc_html( $season->name ); ?>
|
||||||
|
</a>
|
||||||
|
</strong>
|
||||||
|
<div class="row-actions">
|
||||||
|
<span class="edit">
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons&action=edit&season_id=' . $season->id ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Edit', 'wp-bnb' ); ?>
|
||||||
|
</a> |
|
||||||
|
</span>
|
||||||
|
<span class="delete">
|
||||||
|
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=wp-bnb-seasons&action=delete&season_id=' . $season->id ), 'delete_season' ) ); ?>"
|
||||||
|
class="submitdelete"
|
||||||
|
onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to delete this season?', 'wp-bnb' ); ?>');">
|
||||||
|
<?php esc_html_e( 'Delete', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="column-dates">
|
||||||
|
<?php echo esc_html( self::format_date_range( $season->start_date, $season->end_date ) ); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-modifier">
|
||||||
|
<?php echo esc_html( $season->getModifierLabel() ); ?>
|
||||||
|
<?php if ( $season->modifier !== 1.0 ) : ?>
|
||||||
|
<span class="bnb-modifier-visual" style="color: <?php echo $season->modifier > 1 ? '#d63638' : '#00a32a'; ?>;">
|
||||||
|
(<?php echo esc_html( number_format( $season->modifier, 2 ) ); ?>x)
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-priority">
|
||||||
|
<?php echo esc_html( $season->priority ); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-status">
|
||||||
|
<?php if ( $season->active ) : ?>
|
||||||
|
<span class="bnb-status-active"><?php esc_html_e( 'Active', 'wp-bnb' ); ?></span>
|
||||||
|
<?php else : ?>
|
||||||
|
<span class="bnb-status-inactive"><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the season form.
|
||||||
|
*
|
||||||
|
* @param string $season_id Season ID for editing, empty for new.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_form( string $season_id = '' ): void {
|
||||||
|
$season = $season_id ? Season::find( $season_id ) : null;
|
||||||
|
$is_edit = null !== $season;
|
||||||
|
|
||||||
|
if ( $season_id && ! $season ) {
|
||||||
|
echo '<div class="notice notice-error"><p>' . esc_html__( 'Season not found.', 'wp-bnb' ) . '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<form method="post" action="" class="bnb-season-form">
|
||||||
|
<?php wp_nonce_field( 'save_season', 'wp_bnb_season_nonce' ); ?>
|
||||||
|
<input type="hidden" name="season_id" value="<?php echo esc_attr( $season ? $season->id : '' ); ?>">
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="season_name"><?php esc_html_e( 'Season Name', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="season_name" id="season_name"
|
||||||
|
value="<?php echo esc_attr( $season ? $season->name : '' ); ?>"
|
||||||
|
class="regular-text" required>
|
||||||
|
<p class="description"><?php esc_html_e( 'A descriptive name for this season (e.g., "High Season", "Winter Holidays").', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="season_start_date"><?php esc_html_e( 'Start Date', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="season_start_date" id="season_start_date"
|
||||||
|
value="<?php echo esc_attr( $season ? $season->start_date : '' ); ?>"
|
||||||
|
class="small-text" placeholder="MM-DD" pattern="\d{2}-\d{2}" required>
|
||||||
|
<p class="description"><?php esc_html_e( 'Format: MM-DD (e.g., 06-15 for June 15th). Seasons repeat annually.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="season_end_date"><?php esc_html_e( 'End Date', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="season_end_date" id="season_end_date"
|
||||||
|
value="<?php echo esc_attr( $season ? $season->end_date : '' ); ?>"
|
||||||
|
class="small-text" placeholder="MM-DD" pattern="\d{2}-\d{2}" required>
|
||||||
|
<p class="description"><?php esc_html_e( 'Format: MM-DD. Seasons can span year boundaries (e.g., 12-20 to 01-06).', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="season_modifier"><?php esc_html_e( 'Price Modifier', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="season_modifier" id="season_modifier"
|
||||||
|
value="<?php echo esc_attr( $season ? $season->modifier : '1.00' ); ?>"
|
||||||
|
class="small-text" min="0.1" max="3" step="0.01" required>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Multiplier for base prices. Examples:', 'wp-bnb' ); ?><br>
|
||||||
|
<?php esc_html_e( '1.00 = Normal price | 1.25 = 25% increase | 0.85 = 15% discount', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="season_priority"><?php esc_html_e( 'Priority', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="season_priority" id="season_priority"
|
||||||
|
value="<?php echo esc_attr( $season ? $season->priority : '10' ); ?>"
|
||||||
|
class="small-text" min="0" max="100">
|
||||||
|
<p class="description"><?php esc_html_e( 'Higher priority seasons take precedence when dates overlap.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="season_active" value="1"
|
||||||
|
<?php checked( $season ? $season->active : true ); ?>>
|
||||||
|
<?php esc_html_e( 'Active', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description"><?php esc_html_e( 'Inactive seasons are ignored in price calculations.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" name="submit" class="button button-primary"
|
||||||
|
value="<?php echo $is_edit ? esc_attr__( 'Update Season', 'wp-bnb' ) : esc_attr__( 'Add Season', 'wp-bnb' ); ?>">
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ); ?>" class="button">
|
||||||
|
<?php esc_html_e( 'Cancel', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date range for display.
|
||||||
|
*
|
||||||
|
* @param string $start Start date (MM-DD).
|
||||||
|
* @param string $end End date (MM-DD).
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function format_date_range( string $start, string $end ): string {
|
||||||
|
$months = array(
|
||||||
|
'01' => __( 'Jan', 'wp-bnb' ),
|
||||||
|
'02' => __( 'Feb', 'wp-bnb' ),
|
||||||
|
'03' => __( 'Mar', 'wp-bnb' ),
|
||||||
|
'04' => __( 'Apr', 'wp-bnb' ),
|
||||||
|
'05' => __( 'May', 'wp-bnb' ),
|
||||||
|
'06' => __( 'Jun', 'wp-bnb' ),
|
||||||
|
'07' => __( 'Jul', 'wp-bnb' ),
|
||||||
|
'08' => __( 'Aug', 'wp-bnb' ),
|
||||||
|
'09' => __( 'Sep', 'wp-bnb' ),
|
||||||
|
'10' => __( 'Oct', 'wp-bnb' ),
|
||||||
|
'11' => __( 'Nov', 'wp-bnb' ),
|
||||||
|
'12' => __( 'Dec', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$format_date = function ( string $date ) use ( $months ): string {
|
||||||
|
$parts = explode( '-', $date );
|
||||||
|
if ( count( $parts ) !== 2 ) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
$month = $months[ $parts[0] ] ?? $parts[0];
|
||||||
|
$day = ltrim( $parts[1], '0' );
|
||||||
|
return $month . ' ' . $day;
|
||||||
|
};
|
||||||
|
|
||||||
|
return $format_date( $start ) . ' - ' . $format_date( $end );
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/Plugin.php
185
src/Plugin.php
@@ -9,9 +9,11 @@ declare( strict_types=1 );
|
|||||||
|
|
||||||
namespace Magdev\WpBnb;
|
namespace Magdev\WpBnb;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
use Magdev\WpBnb\PostTypes\Building;
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
use Magdev\WpBnb\PostTypes\Room;
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -127,6 +129,9 @@ final class Plugin {
|
|||||||
// Admin menu and settings will be added here.
|
// Admin menu and settings will be added here.
|
||||||
add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
|
add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
|
||||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||||
|
|
||||||
|
// Initialize seasons admin page.
|
||||||
|
SeasonsAdmin::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,6 +211,9 @@ final class Plugin {
|
|||||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||||
|
'increase' => __( 'increase', 'wp-bnb' ),
|
||||||
|
'discount' => __( 'discount', 'wp-bnb' ),
|
||||||
|
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -353,6 +361,10 @@ final class Plugin {
|
|||||||
class="nav-tab <?php echo 'general' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'general' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'General', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'General', 'wp-bnb' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=pricing' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'pricing' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'Pricing', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ); ?>"
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ); ?>"
|
||||||
class="nav-tab <?php echo 'license' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'license' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'License', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'License', 'wp-bnb' ); ?>
|
||||||
@@ -362,6 +374,9 @@ final class Plugin {
|
|||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<?php
|
<?php
|
||||||
switch ( $active_tab ) {
|
switch ( $active_tab ) {
|
||||||
|
case 'pricing':
|
||||||
|
$this->render_pricing_settings();
|
||||||
|
break;
|
||||||
case 'license':
|
case 'license':
|
||||||
$this->render_license_settings();
|
$this->render_license_settings();
|
||||||
break;
|
break;
|
||||||
@@ -427,6 +442,143 @@ final class Plugin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render pricing settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_pricing_settings(): void {
|
||||||
|
$short_term_max = get_option( 'wp_bnb_short_term_max_nights', 6 );
|
||||||
|
$mid_term_max = get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
||||||
|
$weekend_days = get_option( 'wp_bnb_weekend_days', '5,6' );
|
||||||
|
$seasons = Season::all();
|
||||||
|
|
||||||
|
$days_of_week = array(
|
||||||
|
1 => __( 'Monday', 'wp-bnb' ),
|
||||||
|
2 => __( 'Tuesday', 'wp-bnb' ),
|
||||||
|
3 => __( 'Wednesday', 'wp-bnb' ),
|
||||||
|
4 => __( 'Thursday', 'wp-bnb' ),
|
||||||
|
5 => __( 'Friday', 'wp-bnb' ),
|
||||||
|
6 => __( 'Saturday', 'wp-bnb' ),
|
||||||
|
7 => __( 'Sunday', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
$selected_days = array_map( 'intval', explode( ',', $weekend_days ) );
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Pricing Tier Thresholds', 'wp-bnb' ); ?></h2>
|
||||||
|
<p class="description"><?php esc_html_e( 'Define the number of nights that determine which pricing tier applies.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_short_term_max_nights"><?php esc_html_e( 'Short-term (Nightly)', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_short_term_max_nights" id="wp_bnb_short_term_max_nights"
|
||||||
|
value="<?php echo esc_attr( $short_term_max ); ?>"
|
||||||
|
class="small-text" min="1" max="30">
|
||||||
|
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Stays up to this many nights use the nightly rate.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_mid_term_max_nights"><?php esc_html_e( 'Mid-term (Weekly)', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_mid_term_max_nights" id="wp_bnb_mid_term_max_nights"
|
||||||
|
value="<?php echo esc_attr( $mid_term_max ); ?>"
|
||||||
|
class="small-text" min="7" max="90">
|
||||||
|
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Stays longer than short-term but up to this many nights use the weekly rate.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Long-term (Monthly)', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: number of nights */
|
||||||
|
esc_html__( 'Stays longer than %s nights use the monthly rate.', 'wp-bnb' ),
|
||||||
|
'<strong id="wp-bnb-long-term-min">' . esc_html( $mid_term_max ) . '</strong>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></h2>
|
||||||
|
<p class="description"><?php esc_html_e( 'Select which days are considered weekend days for weekend surcharges.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<fieldset>
|
||||||
|
<?php foreach ( $days_of_week as $day_num => $day_name ) : ?>
|
||||||
|
<label style="display: inline-block; margin-right: 15px;">
|
||||||
|
<input type="checkbox" name="wp_bnb_weekend_days[]" value="<?php echo esc_attr( $day_num ); ?>"
|
||||||
|
<?php checked( in_array( $day_num, $selected_days, true ) ); ?>>
|
||||||
|
<?php echo esc_html( $day_name ); ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</fieldset>
|
||||||
|
<p class="description"><?php esc_html_e( 'Weekend surcharges (configured per room) apply to nights starting on these days.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h2>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Link to seasons page */
|
||||||
|
esc_html__( 'Manage seasonal pricing periods in the %s.', 'wp-bnb' ),
|
||||||
|
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ) . '">' . esc_html__( 'Seasons Manager', 'wp-bnb' ) . '</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $seasons ) ) : ?>
|
||||||
|
<table class="widefat striped" style="max-width: 600px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Season', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Period', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Modifier', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $seasons as $season ) : ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo esc_html( $season->name ); ?></td>
|
||||||
|
<td><?php echo esc_html( $season->start_date . ' - ' . $season->end_date ); ?></td>
|
||||||
|
<td><?php echo esc_html( $season->getModifierLabel() ); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ( $season->active ) : ?>
|
||||||
|
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
|
||||||
|
<?php else : ?>
|
||||||
|
<span class="dashicons dashicons-marker" style="color: #646970;"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php else : ?>
|
||||||
|
<p><?php esc_html_e( 'No seasons configured yet.', 'wp-bnb' ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Pricing Settings', 'wp-bnb' ) ); ?>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render license settings tab.
|
* Render license settings tab.
|
||||||
*
|
*
|
||||||
@@ -588,6 +740,9 @@ final class Plugin {
|
|||||||
*/
|
*/
|
||||||
private function save_settings( string $tab ): void {
|
private function save_settings( string $tab ): void {
|
||||||
switch ( $tab ) {
|
switch ( $tab ) {
|
||||||
|
case 'pricing':
|
||||||
|
$this->save_pricing_settings();
|
||||||
|
break;
|
||||||
case 'license':
|
case 'license':
|
||||||
$this->save_license_settings();
|
$this->save_license_settings();
|
||||||
break;
|
break;
|
||||||
@@ -614,6 +769,36 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save pricing settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_pricing_settings(): void {
|
||||||
|
if ( isset( $_POST['wp_bnb_short_term_max_nights'] ) ) {
|
||||||
|
$value = absint( $_POST['wp_bnb_short_term_max_nights'] );
|
||||||
|
$value = max( 1, min( 30, $value ) );
|
||||||
|
update_option( 'wp_bnb_short_term_max_nights', $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['wp_bnb_mid_term_max_nights'] ) ) {
|
||||||
|
$value = absint( $_POST['wp_bnb_mid_term_max_nights'] );
|
||||||
|
$value = max( 7, min( 90, $value ) );
|
||||||
|
update_option( 'wp_bnb_mid_term_max_nights', $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['wp_bnb_weekend_days'] ) && is_array( $_POST['wp_bnb_weekend_days'] ) ) {
|
||||||
|
$days = array_map( 'absint', $_POST['wp_bnb_weekend_days'] );
|
||||||
|
$days = array_filter( $days, fn( $d ) => $d >= 1 && $d <= 7 );
|
||||||
|
update_option( 'wp_bnb_weekend_days', implode( ',', $days ) );
|
||||||
|
} else {
|
||||||
|
update_option( 'wp_bnb_weekend_days', '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Pricing settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save license settings.
|
* Save license settings.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ declare( strict_types=1 );
|
|||||||
|
|
||||||
namespace Magdev\WpBnb\PostTypes;
|
namespace Magdev\WpBnb\PostTypes;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
@@ -147,6 +149,15 @@ final class Room {
|
|||||||
'normal',
|
'normal',
|
||||||
'default'
|
'default'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_room_pricing',
|
||||||
|
__( 'Room Pricing', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_pricing_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,6 +338,87 @@ final class Room {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render pricing meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_pricing_meta_box( \WP_Post $post ): void {
|
||||||
|
$pricing = Calculator::getRoomPricing( $post->ID );
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
?>
|
||||||
|
<table class="form-table bnb-pricing-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row" colspan="2">
|
||||||
|
<h4><?php esc_html_e( 'Base Prices', 'wp-bnb' ); ?></h4>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<?php foreach ( PricingTier::cases() as $tier ) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_price_<?php echo esc_attr( $tier->value ); ?>">
|
||||||
|
<?php echo esc_html( $tier->label() ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-price-input-wrapper">
|
||||||
|
<input type="number"
|
||||||
|
id="bnb_room_price_<?php echo esc_attr( $tier->value ); ?>"
|
||||||
|
name="bnb_room_price_<?php echo esc_attr( $tier->value ); ?>"
|
||||||
|
value="<?php echo esc_attr( $pricing[ $tier->value ]['price'] ?? '' ); ?>"
|
||||||
|
class="small-text"
|
||||||
|
min="0"
|
||||||
|
step="0.01">
|
||||||
|
<span class="bnb-price-unit">
|
||||||
|
<?php echo esc_html( $currency ); ?> <?php echo esc_html( $tier->unit() ); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" colspan="2">
|
||||||
|
<h4><?php esc_html_e( 'Adjustments', 'wp-bnb' ); ?></h4>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_price_weekend_surcharge">
|
||||||
|
<?php esc_html_e( 'Weekend Surcharge', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-price-input-wrapper">
|
||||||
|
<input type="number"
|
||||||
|
id="bnb_room_price_weekend_surcharge"
|
||||||
|
name="bnb_room_price_weekend_surcharge"
|
||||||
|
value="<?php echo esc_attr( $pricing['weekend_surcharge']['price'] ?? '' ); ?>"
|
||||||
|
class="small-text"
|
||||||
|
min="0"
|
||||||
|
step="0.01">
|
||||||
|
<span class="bnb-price-unit">
|
||||||
|
<?php echo esc_html( $currency ); ?> <?php esc_html_e( 'per night', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Additional amount charged for weekend nights (Friday/Saturday by default).', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Link to pricing settings */
|
||||||
|
esc_html__( 'Seasonal pricing and tier thresholds can be configured in %s.', 'wp-bnb' ),
|
||||||
|
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=pricing' ) ) . '">' . esc_html__( 'Pricing Settings', 'wp-bnb' ) . '</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save post meta.
|
* Save post meta.
|
||||||
*
|
*
|
||||||
@@ -410,6 +502,23 @@ final class Room {
|
|||||||
implode( ',', $ids )
|
implode( ',', $ids )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pricing fields.
|
||||||
|
$pricing_data = array();
|
||||||
|
foreach ( PricingTier::cases() as $tier ) {
|
||||||
|
$key = 'bnb_room_price_' . $tier->value;
|
||||||
|
if ( isset( $_POST[ $key ] ) && '' !== $_POST[ $key ] ) {
|
||||||
|
$pricing_data[ $tier->value ] = floatval( $_POST[ $key ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['bnb_room_price_weekend_surcharge'] ) ) {
|
||||||
|
$pricing_data['weekend_surcharge'] = floatval( $_POST['bnb_room_price_weekend_surcharge'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $pricing_data ) ) {
|
||||||
|
Calculator::saveRoomPricing( $post_id, $pricing_data );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -433,6 +542,7 @@ final class Room {
|
|||||||
$new_columns['room_number'] = __( 'Room #', 'wp-bnb' );
|
$new_columns['room_number'] = __( 'Room #', 'wp-bnb' );
|
||||||
$new_columns['taxonomy-bnb_room_type'] = __( 'Room Type', 'wp-bnb' );
|
$new_columns['taxonomy-bnb_room_type'] = __( 'Room Type', 'wp-bnb' );
|
||||||
$new_columns['capacity'] = __( 'Capacity', 'wp-bnb' );
|
$new_columns['capacity'] = __( 'Capacity', 'wp-bnb' );
|
||||||
|
$new_columns['price'] = __( 'Price/Night', 'wp-bnb' );
|
||||||
$new_columns['status'] = __( 'Status', 'wp-bnb' );
|
$new_columns['status'] = __( 'Status', 'wp-bnb' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,6 +593,16 @@ final class Room {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'price':
|
||||||
|
$pricing = Calculator::getRoomPricing( $post_id );
|
||||||
|
$nightly = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? null;
|
||||||
|
if ( $nightly ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( $nightly ) );
|
||||||
|
} else {
|
||||||
|
echo '<span class="bnb-no-price">' . esc_html__( 'Not set', 'wp-bnb' ) . '</span>';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'status':
|
case 'status':
|
||||||
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'available';
|
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'available';
|
||||||
$statuses = self::get_room_statuses();
|
$statuses = self::get_room_statuses();
|
||||||
|
|||||||
363
src/Pricing/Calculator.php
Normal file
363
src/Pricing/Calculator.php
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Price calculator.
|
||||||
|
*
|
||||||
|
* Handles price calculations for room bookings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Pricing
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Pricing;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price calculator class.
|
||||||
|
*/
|
||||||
|
final class Calculator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta key prefix for room pricing.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const META_PREFIX = '_bnb_room_price_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room post ID.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $room_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check-in date.
|
||||||
|
*
|
||||||
|
* @var \DateTimeImmutable
|
||||||
|
*/
|
||||||
|
private \DateTimeImmutable $check_in;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check-out date.
|
||||||
|
*
|
||||||
|
* @var \DateTimeImmutable
|
||||||
|
*/
|
||||||
|
private \DateTimeImmutable $check_out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price breakdown.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $breakdown = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param \DateTimeInterface|string $check_in Check-in date.
|
||||||
|
* @param \DateTimeInterface|string $check_out Check-out date.
|
||||||
|
*/
|
||||||
|
public function __construct( int $room_id, \DateTimeInterface|string $check_in, \DateTimeInterface|string $check_out ) {
|
||||||
|
$this->room_id = $room_id;
|
||||||
|
|
||||||
|
if ( is_string( $check_in ) ) {
|
||||||
|
$check_in = new \DateTimeImmutable( $check_in );
|
||||||
|
} elseif ( $check_in instanceof \DateTime ) {
|
||||||
|
$check_in = \DateTimeImmutable::createFromMutable( $check_in );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_string( $check_out ) ) {
|
||||||
|
$check_out = new \DateTimeImmutable( $check_out );
|
||||||
|
} elseif ( $check_out instanceof \DateTime ) {
|
||||||
|
$check_out = \DateTimeImmutable::createFromMutable( $check_out );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->check_in = $check_in;
|
||||||
|
$this->check_out = $check_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of nights.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getNights(): int {
|
||||||
|
$interval = $this->check_in->diff( $this->check_out );
|
||||||
|
return max( 1, $interval->days );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the pricing tier for this stay.
|
||||||
|
*
|
||||||
|
* @return PricingTier
|
||||||
|
*/
|
||||||
|
public function getTier(): PricingTier {
|
||||||
|
return PricingTier::fromNights( $this->getNights() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base price for a room.
|
||||||
|
*
|
||||||
|
* @param PricingTier $tier Pricing tier.
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getBasePrice( PricingTier $tier ): float {
|
||||||
|
$meta_key = self::META_PREFIX . $tier->value;
|
||||||
|
$price = get_post_meta( $this->room_id, $meta_key, true );
|
||||||
|
return $price ? (float) $price : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the weekend surcharge for a room.
|
||||||
|
*
|
||||||
|
* @return float Surcharge as absolute amount.
|
||||||
|
*/
|
||||||
|
public function getWeekendSurcharge(): float {
|
||||||
|
$surcharge = get_post_meta( $this->room_id, self::META_PREFIX . 'weekend_surcharge', true );
|
||||||
|
return $surcharge ? (float) $surcharge : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if weekend surcharge is enabled for this room.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasWeekendSurcharge(): bool {
|
||||||
|
return $this->getWeekendSurcharge() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date is a weekend day.
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface $date Date to check.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isWeekend( \DateTimeInterface $date ): bool {
|
||||||
|
$day_of_week = (int) $date->format( 'N' ); // 1 = Monday, 7 = Sunday.
|
||||||
|
$weekend_days = array_map(
|
||||||
|
'intval',
|
||||||
|
explode( ',', (string) get_option( 'wp_bnb_weekend_days', '5,6' ) ) // Default: Friday, Saturday.
|
||||||
|
);
|
||||||
|
return in_array( $day_of_week, $weekend_days, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total price.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function calculate(): float {
|
||||||
|
$this->breakdown = array();
|
||||||
|
|
||||||
|
$nights = $this->getNights();
|
||||||
|
$tier = $this->getTier();
|
||||||
|
|
||||||
|
$base_price = $this->getBasePrice( $tier );
|
||||||
|
$weekend_surcharge = $this->getWeekendSurcharge();
|
||||||
|
|
||||||
|
// If no base price is set, return 0.
|
||||||
|
if ( $base_price <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0.0;
|
||||||
|
|
||||||
|
// Calculate based on tier.
|
||||||
|
switch ( $tier ) {
|
||||||
|
case PricingTier::LONG_TERM:
|
||||||
|
// Monthly pricing.
|
||||||
|
$months = ceil( $nights / 30 );
|
||||||
|
$total = $base_price * $months;
|
||||||
|
$this->breakdown['months'] = $months;
|
||||||
|
$this->breakdown['monthly_rate'] = $base_price;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PricingTier::MID_TERM:
|
||||||
|
// Weekly pricing.
|
||||||
|
$weeks = ceil( $nights / 7 );
|
||||||
|
$total = $base_price * $weeks;
|
||||||
|
$this->breakdown['weeks'] = $weeks;
|
||||||
|
$this->breakdown['weekly_rate'] = $base_price;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PricingTier::SHORT_TERM:
|
||||||
|
default:
|
||||||
|
// Nightly pricing with seasonal adjustments and weekend surcharges.
|
||||||
|
$current = $this->check_in;
|
||||||
|
$breakdown_nights = array();
|
||||||
|
|
||||||
|
for ( $i = 0; $i < $nights; $i++ ) {
|
||||||
|
$night_price = $base_price;
|
||||||
|
$modifiers = array();
|
||||||
|
|
||||||
|
// Apply seasonal pricing.
|
||||||
|
$season = Season::forDate( $current );
|
||||||
|
if ( $season ) {
|
||||||
|
$night_price *= $season->modifier;
|
||||||
|
$modifiers['season'] = array(
|
||||||
|
'name' => $season->name,
|
||||||
|
'modifier' => $season->modifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply weekend surcharge.
|
||||||
|
if ( $weekend_surcharge > 0 && self::isWeekend( $current ) ) {
|
||||||
|
$night_price += $weekend_surcharge;
|
||||||
|
$modifiers['weekend'] = $weekend_surcharge;
|
||||||
|
}
|
||||||
|
|
||||||
|
$breakdown_nights[] = array(
|
||||||
|
'date' => $current->format( 'Y-m-d' ),
|
||||||
|
'base' => $base_price,
|
||||||
|
'final' => $night_price,
|
||||||
|
'modifiers' => $modifiers,
|
||||||
|
);
|
||||||
|
|
||||||
|
$total += $night_price;
|
||||||
|
$current = $current->modify( '+1 day' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->breakdown['nights'] = $breakdown_nights;
|
||||||
|
$this->breakdown['nightly_rate'] = $base_price;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->breakdown['tier'] = $tier->value;
|
||||||
|
$this->breakdown['total'] = $total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the calculated price.
|
||||||
|
*
|
||||||
|
* @param float $total Calculated total price.
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param Calculator $calculator Calculator instance.
|
||||||
|
*/
|
||||||
|
return (float) apply_filters( 'wp_bnb_calculate_price', $total, $this->room_id, $this );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the price breakdown.
|
||||||
|
*
|
||||||
|
* Must be called after calculate().
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getBreakdown(): array {
|
||||||
|
return $this->breakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a price with currency.
|
||||||
|
*
|
||||||
|
* @param float $amount Amount to format.
|
||||||
|
* @param string $currency Currency code. Default from settings.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function formatPrice( float $amount, string $currency = '' ): string {
|
||||||
|
if ( empty( $currency ) ) {
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$symbols = array(
|
||||||
|
'CHF' => 'CHF',
|
||||||
|
'EUR' => "\u{20AC}",
|
||||||
|
'USD' => '$',
|
||||||
|
'GBP' => "\u{00A3}",
|
||||||
|
);
|
||||||
|
|
||||||
|
$symbol = $symbols[ $currency ] ?? $currency;
|
||||||
|
$formatted = number_format( $amount, 2, '.', "'" );
|
||||||
|
|
||||||
|
// CHF uses suffix, others use prefix.
|
||||||
|
if ( 'CHF' === $currency ) {
|
||||||
|
return $formatted . ' ' . $symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $symbol . ' ' . $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room pricing summary.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getRoomPricing( int $room_id ): array {
|
||||||
|
$pricing = array();
|
||||||
|
|
||||||
|
foreach ( PricingTier::cases() as $tier ) {
|
||||||
|
$meta_key = self::META_PREFIX . $tier->value;
|
||||||
|
$price = get_post_meta( $room_id, $meta_key, true );
|
||||||
|
|
||||||
|
$pricing[ $tier->value ] = array(
|
||||||
|
'label' => $tier->label(),
|
||||||
|
'unit' => $tier->unit(),
|
||||||
|
'price' => $price ? (float) $price : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pricing['weekend_surcharge'] = array(
|
||||||
|
'label' => __( 'Weekend Surcharge', 'wp-bnb' ),
|
||||||
|
'price' => (float) get_post_meta( $room_id, self::META_PREFIX . 'weekend_surcharge', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $pricing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save room pricing.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param array $pricing Pricing data with tier keys.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function saveRoomPricing( int $room_id, array $pricing ): void {
|
||||||
|
foreach ( PricingTier::cases() as $tier ) {
|
||||||
|
if ( isset( $pricing[ $tier->value ] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$room_id,
|
||||||
|
self::META_PREFIX . $tier->value,
|
||||||
|
floatval( $pricing[ $tier->value ] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $pricing['weekend_surcharge'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$room_id,
|
||||||
|
self::META_PREFIX . 'weekend_surcharge',
|
||||||
|
floatval( $pricing['weekend_surcharge'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the check-in date.
|
||||||
|
*
|
||||||
|
* @return \DateTimeImmutable
|
||||||
|
*/
|
||||||
|
public function getCheckIn(): \DateTimeImmutable {
|
||||||
|
return $this->check_in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the check-out date.
|
||||||
|
*
|
||||||
|
* @return \DateTimeImmutable
|
||||||
|
*/
|
||||||
|
public function getCheckOut(): \DateTimeImmutable {
|
||||||
|
return $this->check_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room ID.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getRoomId(): int {
|
||||||
|
return $this->room_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/Pricing/PricingTier.php
Normal file
159
src/Pricing/PricingTier.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pricing tier enumeration.
|
||||||
|
*
|
||||||
|
* Defines the pricing tiers for short, mid, and long-term stays.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Pricing
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Pricing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pricing tier enum.
|
||||||
|
*/
|
||||||
|
enum PricingTier: string {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short-term stay (per night).
|
||||||
|
* Default: 1-6 nights.
|
||||||
|
*/
|
||||||
|
case SHORT_TERM = 'short_term';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mid-term stay (per week).
|
||||||
|
* Default: 7-27 nights (1-4 weeks).
|
||||||
|
*/
|
||||||
|
case MID_TERM = 'mid_term';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Long-term stay (per month).
|
||||||
|
* Default: 28+ nights (1+ months).
|
||||||
|
*/
|
||||||
|
case LONG_TERM = 'long_term';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the label for this tier.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function label(): string {
|
||||||
|
return match ( $this ) {
|
||||||
|
self::SHORT_TERM => __( 'Short-term (Nightly)', 'wp-bnb' ),
|
||||||
|
self::MID_TERM => __( 'Mid-term (Weekly)', 'wp-bnb' ),
|
||||||
|
self::LONG_TERM => __( 'Long-term (Monthly)', 'wp-bnb' ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unit label for this tier.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function unit(): string {
|
||||||
|
return match ( $this ) {
|
||||||
|
self::SHORT_TERM => __( 'per night', 'wp-bnb' ),
|
||||||
|
self::MID_TERM => __( 'per week', 'wp-bnb' ),
|
||||||
|
self::LONG_TERM => __( 'per month', 'wp-bnb' ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unit label for this tier (singular).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function unitSingular(): string {
|
||||||
|
return match ( $this ) {
|
||||||
|
self::SHORT_TERM => __( 'night', 'wp-bnb' ),
|
||||||
|
self::MID_TERM => __( 'week', 'wp-bnb' ),
|
||||||
|
self::LONG_TERM => __( 'month', 'wp-bnb' ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unit label for this tier (plural).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function unitPlural(): string {
|
||||||
|
return match ( $this ) {
|
||||||
|
self::SHORT_TERM => __( 'nights', 'wp-bnb' ),
|
||||||
|
self::MID_TERM => __( 'weeks', 'wp-bnb' ),
|
||||||
|
self::LONG_TERM => __( 'months', 'wp-bnb' ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default minimum nights for this tier.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function defaultMinNights(): int {
|
||||||
|
return match ( $this ) {
|
||||||
|
self::SHORT_TERM => 1,
|
||||||
|
self::MID_TERM => 7,
|
||||||
|
self::LONG_TERM => 28,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default maximum nights for this tier.
|
||||||
|
*
|
||||||
|
* @return int|null Null means unlimited.
|
||||||
|
*/
|
||||||
|
public function defaultMaxNights(): ?int {
|
||||||
|
return match ( $this ) {
|
||||||
|
self::SHORT_TERM => 6,
|
||||||
|
self::MID_TERM => 27,
|
||||||
|
self::LONG_TERM => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the pricing tier based on number of nights.
|
||||||
|
*
|
||||||
|
* @param int $nights Number of nights.
|
||||||
|
* @param int|null $short_term_max Maximum nights for short-term. Default 6.
|
||||||
|
* @param int|null $mid_term_max Maximum nights for mid-term. Default 27.
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromNights( int $nights, ?int $short_term_max = null, ?int $mid_term_max = null ): self {
|
||||||
|
$short_term_max = $short_term_max ?? (int) get_option( 'wp_bnb_short_term_max_nights', 6 );
|
||||||
|
$mid_term_max = $mid_term_max ?? (int) get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
||||||
|
|
||||||
|
if ( $nights <= $short_term_max ) {
|
||||||
|
return self::SHORT_TERM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $nights <= $mid_term_max ) {
|
||||||
|
return self::MID_TERM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::LONG_TERM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tiers.
|
||||||
|
*
|
||||||
|
* @return array<self>
|
||||||
|
*/
|
||||||
|
public static function all(): array {
|
||||||
|
return self::cases();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tier options for select fields.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function options(): array {
|
||||||
|
$options = array();
|
||||||
|
foreach ( self::cases() as $tier ) {
|
||||||
|
$options[ $tier->value ] = $tier->label();
|
||||||
|
}
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
}
|
||||||
319
src/Pricing/Season.php
Normal file
319
src/Pricing/Season.php
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Seasonal pricing management.
|
||||||
|
*
|
||||||
|
* Handles seasonal pricing periods with date ranges and price modifiers.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Pricing
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Pricing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Season class for managing seasonal pricing.
|
||||||
|
*/
|
||||||
|
final class Season {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option name for storing seasons.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const OPTION_NAME = 'wp_bnb_seasons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Season ID.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Season name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start date (format: MM-DD).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $start_date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End date (format: MM-DD).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $end_date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price modifier (multiplier). 1.0 = normal, 1.2 = 20% increase, 0.8 = 20% decrease.
|
||||||
|
*
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
public float $modifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority for overlapping seasons. Higher value = higher priority.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public int $priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this season is active.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $active;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param array $data Season data.
|
||||||
|
*/
|
||||||
|
public function __construct( array $data = array() ) {
|
||||||
|
$this->id = $data['id'] ?? wp_generate_uuid4();
|
||||||
|
$this->name = $data['name'] ?? '';
|
||||||
|
$this->start_date = $data['start_date'] ?? '';
|
||||||
|
$this->end_date = $data['end_date'] ?? '';
|
||||||
|
$this->modifier = isset( $data['modifier'] ) ? (float) $data['modifier'] : 1.0;
|
||||||
|
$this->priority = isset( $data['priority'] ) ? (int) $data['priority'] : 0;
|
||||||
|
$this->active = isset( $data['active'] ) ? (bool) $data['active'] : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to array.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(): array {
|
||||||
|
return array(
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'start_date' => $this->start_date,
|
||||||
|
'end_date' => $this->end_date,
|
||||||
|
'modifier' => $this->modifier,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
'active' => $this->active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date falls within this season.
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface $date Date to check.
|
||||||
|
* @param int|null $year Year context (for spanning seasons like winter).
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function containsDate( \DateTimeInterface $date, ?int $year = null ): bool {
|
||||||
|
if ( ! $this->active ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $year ?? (int) $date->format( 'Y' );
|
||||||
|
|
||||||
|
// Parse start and end as month-day.
|
||||||
|
$start_parts = explode( '-', $this->start_date );
|
||||||
|
$end_parts = explode( '-', $this->end_date );
|
||||||
|
|
||||||
|
if ( count( $start_parts ) !== 2 || count( $end_parts ) !== 2 ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start_month = (int) $start_parts[0];
|
||||||
|
$start_day = (int) $start_parts[1];
|
||||||
|
$end_month = (int) $end_parts[0];
|
||||||
|
$end_day = (int) $end_parts[1];
|
||||||
|
|
||||||
|
$check_month = (int) $date->format( 'm' );
|
||||||
|
$check_day = (int) $date->format( 'd' );
|
||||||
|
|
||||||
|
// Create comparable values (month * 100 + day).
|
||||||
|
$check_value = $check_month * 100 + $check_day;
|
||||||
|
$start_value = $start_month * 100 + $start_day;
|
||||||
|
$end_value = $end_month * 100 + $end_day;
|
||||||
|
|
||||||
|
// Handle year-spanning seasons (e.g., December to February).
|
||||||
|
if ( $start_value > $end_value ) {
|
||||||
|
// Season spans year boundary.
|
||||||
|
return $check_value >= $start_value || $check_value <= $end_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal season within same year.
|
||||||
|
return $check_value >= $start_value && $check_value <= $end_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display label for the modifier.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getModifierLabel(): string {
|
||||||
|
if ( $this->modifier === 1.0 ) {
|
||||||
|
return __( 'Normal', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$percentage = ( $this->modifier - 1 ) * 100;
|
||||||
|
if ( $percentage > 0 ) {
|
||||||
|
/* translators: %s: percentage increase */
|
||||||
|
return sprintf( __( '+%s%%', 'wp-bnb' ), number_format( $percentage, 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* translators: %s: percentage decrease */
|
||||||
|
return sprintf( __( '%s%%', 'wp-bnb' ), number_format( $percentage, 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all seasons.
|
||||||
|
*
|
||||||
|
* @return array<self>
|
||||||
|
*/
|
||||||
|
public static function all(): array {
|
||||||
|
$data = get_option( self::OPTION_NAME, array() );
|
||||||
|
$seasons = array();
|
||||||
|
|
||||||
|
foreach ( $data as $season_data ) {
|
||||||
|
$seasons[] = new self( $season_data );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority (descending).
|
||||||
|
usort( $seasons, fn( Season $a, Season $b ) => $b->priority <=> $a->priority );
|
||||||
|
|
||||||
|
return $seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active seasons.
|
||||||
|
*
|
||||||
|
* @return array<self>
|
||||||
|
*/
|
||||||
|
public static function allActive(): array {
|
||||||
|
return array_filter( self::all(), fn( Season $s ) => $s->active );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a season by ID.
|
||||||
|
*
|
||||||
|
* @param string $id Season ID.
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function find( string $id ): ?self {
|
||||||
|
foreach ( self::all() as $season ) {
|
||||||
|
if ( $season->id === $id ) {
|
||||||
|
return $season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the applicable season for a date.
|
||||||
|
*
|
||||||
|
* Returns the highest priority active season that contains the date.
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface $date Date to check.
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function forDate( \DateTimeInterface $date ): ?self {
|
||||||
|
$seasons = self::allActive();
|
||||||
|
|
||||||
|
foreach ( $seasons as $season ) {
|
||||||
|
if ( $season->containsDate( $date ) ) {
|
||||||
|
return $season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a season.
|
||||||
|
*
|
||||||
|
* @param self $season Season to save.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function save( self $season ): bool {
|
||||||
|
$data = get_option( self::OPTION_NAME, array() );
|
||||||
|
$exists = false;
|
||||||
|
|
||||||
|
foreach ( $data as $index => $existing ) {
|
||||||
|
if ( $existing['id'] === $season->id ) {
|
||||||
|
$data[ $index ] = $season->toArray();
|
||||||
|
$exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $exists ) {
|
||||||
|
$data[] = $season->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return update_option( self::OPTION_NAME, $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a season.
|
||||||
|
*
|
||||||
|
* @param string $id Season ID to delete.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function delete( string $id ): bool {
|
||||||
|
$data = get_option( self::OPTION_NAME, array() );
|
||||||
|
$data = array_filter( $data, fn( $s ) => $s['id'] !== $id );
|
||||||
|
return update_option( self::OPTION_NAME, array_values( $data ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create default seasons.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function createDefaults(): void {
|
||||||
|
$existing = get_option( self::OPTION_NAME, array() );
|
||||||
|
if ( ! empty( $existing ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaults = array(
|
||||||
|
new self(
|
||||||
|
array(
|
||||||
|
'name' => __( 'High Season', 'wp-bnb' ),
|
||||||
|
'start_date' => '06-15',
|
||||||
|
'end_date' => '09-15',
|
||||||
|
'modifier' => 1.25,
|
||||||
|
'priority' => 10,
|
||||||
|
'active' => true,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new self(
|
||||||
|
array(
|
||||||
|
'name' => __( 'Winter Holidays', 'wp-bnb' ),
|
||||||
|
'start_date' => '12-20',
|
||||||
|
'end_date' => '01-06',
|
||||||
|
'modifier' => 1.30,
|
||||||
|
'priority' => 20,
|
||||||
|
'active' => true,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new self(
|
||||||
|
array(
|
||||||
|
'name' => __( 'Low Season', 'wp-bnb' ),
|
||||||
|
'start_date' => '11-01',
|
||||||
|
'end_date' => '03-31',
|
||||||
|
'modifier' => 0.85,
|
||||||
|
'priority' => 5,
|
||||||
|
'active' => true,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = array_map( fn( Season $s ) => $s->toArray(), $defaults );
|
||||||
|
update_option( self::OPTION_NAME, $data );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP BnB Management
|
* Plugin Name: WP BnB Management
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||||
* Version: 0.1.0
|
* Version: 0.2.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 8.3
|
* Requires PHP: 8.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin version constant - MUST match Version in header above.
|
// Plugin version constant - MUST match Version in header above.
|
||||||
define( 'WP_BNB_VERSION', '0.1.0' );
|
define( 'WP_BNB_VERSION', '0.2.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user