Add additional services system (v0.5.0)
- Service CPT with pricing types: Included, Per Booking, Per Night - ServiceCategory taxonomy with default categories - Booking-services integration with service selector - Real-time price calculation based on nights and quantity - Services total and grand total display in booking admin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
43
CHANGELOG.md
43
CHANGELOG.md
@@ -5,6 +5,48 @@ 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.5.0] - 2026-01-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Additional Services System:
|
||||||
|
- Custom Post Type: Services (`bnb_service`)
|
||||||
|
- Service pricing types: Included (free), Per Booking (one-time), Per Night
|
||||||
|
- Service configuration: price, status, sort order, max quantity
|
||||||
|
- Custom admin columns with pricing type icons and status badges
|
||||||
|
- Filters by status and pricing type
|
||||||
|
- Service data helper methods for pricing calculations
|
||||||
|
- Service Categories Taxonomy (`bnb_service_category`)
|
||||||
|
- Non-hierarchical (tag-like) structure
|
||||||
|
- Icon selection per category
|
||||||
|
- Sort order for custom ordering
|
||||||
|
- Default categories: Food & Dining, Transportation, Wellness & Spa, Activities, Housekeeping
|
||||||
|
- Booking-Services Integration:
|
||||||
|
- Services meta box in Booking edit screen
|
||||||
|
- Checkbox-based service selection
|
||||||
|
- Quantity input for services with max_quantity > 1
|
||||||
|
- Real-time price calculation per service based on nights
|
||||||
|
- Services total display
|
||||||
|
- Price breakdown shows services cost
|
||||||
|
- Grand total (room + services) in admin list and pricing meta box
|
||||||
|
- Admin UI Enhancements:
|
||||||
|
- Service selector with pricing type indicators
|
||||||
|
- Included services badge
|
||||||
|
- Per-night price suffix display
|
||||||
|
- Service line totals with quantity support
|
||||||
|
- Services total summary in booking
|
||||||
|
- CSS styles for all service-related components
|
||||||
|
- JavaScript for dynamic service pricing calculations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to register Service CPT and ServiceCategory taxonomy
|
||||||
|
- Admin assets enqueued for Service post type screens
|
||||||
|
- Booking admin list shows total price including services
|
||||||
|
- Booking pricing meta box displays services breakdown and grand total
|
||||||
|
- Admin JavaScript extended with service pricing and selection logic
|
||||||
|
- Admin CSS includes comprehensive service styling
|
||||||
|
|
||||||
## [0.4.0] - 2026-01-31
|
## [0.4.0] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -248,6 +290,7 @@ 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.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
||||||
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
||||||
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
||||||
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.2.0
|
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.2.0
|
||||||
|
|||||||
58
CLAUDE.md
58
CLAUDE.md
@@ -502,3 +502,61 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Merged to main (fast-forward)
|
- Merged to main (fast-forward)
|
||||||
- Tagged: `v0.3.0`
|
- Tagged: `v0.3.0`
|
||||||
- Pushed to origin: dev, main, v0.3.0
|
- Pushed to origin: dev, main, v0.3.0
|
||||||
|
|
||||||
|
### 2026-01-31 - Version 0.5.0 (Additional Services)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created Custom Taxonomy: Service Categories (`bnb_service_category`)
|
||||||
|
- Non-hierarchical (tag-like) structure
|
||||||
|
- Dashicon selection for visual display
|
||||||
|
- Sort order meta field for custom ordering
|
||||||
|
- Default categories: Food & Dining, Transportation, Wellness & Spa, Activities, Housekeeping
|
||||||
|
- Created Custom Post Type: Services (`bnb_service`)
|
||||||
|
- Three pricing types: Included (free), Per Booking, Per Night
|
||||||
|
- Price configuration per service
|
||||||
|
- Service status (active/inactive)
|
||||||
|
- Sort order for display ordering
|
||||||
|
- Maximum quantity setting per service
|
||||||
|
- Custom admin columns: pricing type, price, status
|
||||||
|
- Filters by status and pricing type
|
||||||
|
- Helper methods: `get_service_data()`, `calculate_service_price()`, `get_services_for_booking()`, `format_service_price()`
|
||||||
|
- Updated Booking post type with services integration
|
||||||
|
- Added `SERVICES_META_KEY` constant for services storage
|
||||||
|
- New meta box: Additional Services with checkbox selection
|
||||||
|
- Quantity input for services with max_quantity > 1
|
||||||
|
- Real-time per-service line total calculation
|
||||||
|
- Services total display
|
||||||
|
- Price breakdown now shows services cost
|
||||||
|
- Grand total (room + services) in pricing meta box
|
||||||
|
- Admin list price column shows total including services
|
||||||
|
- Helper methods: `calculate_booking_services_total()`, `get_booking_services()`
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Registered ServiceCategory taxonomy
|
||||||
|
- Registered Service post type
|
||||||
|
- Added Service post type to asset enqueuing
|
||||||
|
- Added i18n strings for service pricing descriptions
|
||||||
|
- Updated `assets/css/admin.css`
|
||||||
|
- Service status badges
|
||||||
|
- Service pricing meta box styles
|
||||||
|
- Booking services selector styles
|
||||||
|
- Service item with selected state
|
||||||
|
- Quantity inputs and line totals
|
||||||
|
- Services total summary
|
||||||
|
- Grand total display
|
||||||
|
- Updated `assets/js/admin.js`
|
||||||
|
- `initServicePricing()`: Toggle price row based on pricing type
|
||||||
|
- `initBookingServices()`: Service selection with real-time price calculation
|
||||||
|
- Quantity change handlers with min/max enforcement
|
||||||
|
- Automatic recalculation when booking dates change
|
||||||
|
- Updated version to 0.5.0
|
||||||
|
- Updated CHANGELOG.md with Phase 5 changes
|
||||||
|
- Updated PLAN.md to mark Phase 5 complete
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Service pricing calculation depends on pricing_type: included=0, per_booking=price*qty, per_night=price*qty*nights
|
||||||
|
- Services are stored as JSON array in booking meta with service_id, quantity, price, pricing_type
|
||||||
|
- Same namespace classes can reference each other directly without use statements
|
||||||
|
- Services meta box renders before pricing meta box so services total is available
|
||||||
|
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
|
||||||
|
|||||||
40
PLAN.md
40
PLAN.md
@@ -84,37 +84,37 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Email notifications
|
- [x] Email notifications
|
||||||
- [x] Booking confirmation
|
- [x] Booking confirmation
|
||||||
|
|
||||||
## Phase 4: Guest Management (v0.4.0)
|
## Phase 4: Guest Management (v0.4.0) - Complete
|
||||||
|
|
||||||
### Custom Post Type: Guests
|
### Custom Post Type: Guests
|
||||||
|
|
||||||
- [ ] Personal information (name, email, phone)
|
- [x] Personal information (name, email, phone)
|
||||||
- [ ] Address fields
|
- [x] Address fields
|
||||||
- [ ] ID/Passport information
|
- [x] ID/Passport information
|
||||||
- [ ] Booking history reference
|
- [x] Booking history reference
|
||||||
- [ ] Notes and preferences
|
- [x] Notes and preferences
|
||||||
|
|
||||||
### Privacy & Compliance
|
### Privacy & Compliance
|
||||||
|
|
||||||
- [ ] GDPR compliance features
|
- [x] GDPR compliance features
|
||||||
- [ ] Data export functionality
|
- [x] Data export functionality
|
||||||
- [ ] Data deletion on request
|
- [x] Data deletion on request
|
||||||
- [ ] Consent tracking
|
- [x] Consent tracking
|
||||||
|
|
||||||
## Phase 5: Additional Services (v0.5.0)
|
## Phase 5: Additional Services (v0.5.0) - Complete
|
||||||
|
|
||||||
### Service Options
|
### Service Options
|
||||||
|
|
||||||
- [ ] Custom Post Type: Services
|
- [x] Custom Post Type: Services
|
||||||
- [ ] Price per service (or included)
|
- [x] Price per service (or included)
|
||||||
- [ ] Per-booking or per-night pricing
|
- [x] Per-booking or per-night pricing
|
||||||
- [ ] Service categories
|
- [x] Service categories
|
||||||
|
|
||||||
### Booking Services
|
### Booking Services
|
||||||
|
|
||||||
- [ ] Service selection during booking
|
- [x] Service selection during booking
|
||||||
- [ ] Automatic price calculation
|
- [x] Automatic price calculation
|
||||||
- [ ] Service summary display
|
- [x] Service summary display
|
||||||
|
|
||||||
## Phase 6: Frontend Features (v0.6.0)
|
## Phase 6: Frontend Features (v0.6.0)
|
||||||
|
|
||||||
@@ -291,8 +291,8 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| 0.1.0 | Data structures | Complete |
|
| 0.1.0 | Data structures | Complete |
|
||||||
| 0.2.0 | Pricing | Complete |
|
| 0.2.0 | Pricing | Complete |
|
||||||
| 0.3.0 | Bookings | Complete |
|
| 0.3.0 | Bookings | Complete |
|
||||||
| 0.4.0 | Guests | TBD |
|
| 0.4.0 | Guests | Complete |
|
||||||
| 0.5.0 | Services | TBD |
|
| 0.5.0 | Services | Complete |
|
||||||
| 0.6.0 | Frontend | TBD |
|
| 0.6.0 | Frontend | TBD |
|
||||||
| 0.7.0 | CF7 Integration | TBD |
|
| 0.7.0 | CF7 Integration | TBD |
|
||||||
| 0.8.0 | Dashboard | TBD |
|
| 0.8.0 | Dashboard | TBD |
|
||||||
|
|||||||
@@ -1088,3 +1088,208 @@
|
|||||||
color: #646970;
|
color: #646970;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Services Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Services List in Admin */
|
||||||
|
.column-pricing_type,
|
||||||
|
.column-service_status {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Service Status Badges */
|
||||||
|
.bnb-service-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-status-active {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-status-inactive {
|
||||||
|
background: #f6f7f7;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-included {
|
||||||
|
color: #00a32a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Service Pricing Meta Box */
|
||||||
|
.bnb-service-pricing-type fieldset label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-pricing-type fieldset label input {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-pricing-type fieldset p.description {
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Booking Services Selector
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.bnb-services-selector {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-services-list {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-item:hover {
|
||||||
|
background: #f0f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-item.selected {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-checkbox input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-price-label {
|
||||||
|
color: #135e96;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-included-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-quantity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-quantity label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-qty-input {
|
||||||
|
width: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-line-total {
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-total-value {
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services Total */
|
||||||
|
.bnb-services-total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-services-total strong {
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bnb-services-total-amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Services Message */
|
||||||
|
.bnb-no-services-message {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-services-message a {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Pricing with Services */
|
||||||
|
.bnb-booking-services-summary {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-grand-total {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-grand-total strong {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|||||||
@@ -755,6 +755,185 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize service pricing type toggle.
|
||||||
|
*/
|
||||||
|
function initServicePricing() {
|
||||||
|
var $pricingTypeInputs = $('input[name="bnb_service_pricing_type"]');
|
||||||
|
var $priceRow = $('#bnb-service-price-row');
|
||||||
|
var $priceSuffix = $('#bnb-service-price-suffix');
|
||||||
|
var $priceDescription = $('#bnb-service-price-description');
|
||||||
|
|
||||||
|
if (!$pricingTypeInputs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pricingTypeInputs.on('change', function() {
|
||||||
|
var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val();
|
||||||
|
|
||||||
|
if (pricingType === 'included') {
|
||||||
|
$priceRow.hide();
|
||||||
|
} else {
|
||||||
|
$priceRow.show();
|
||||||
|
|
||||||
|
if (pricingType === 'per_night') {
|
||||||
|
$priceSuffix.text(' / ' + (wpBnbAdmin.i18n.night || 'night'));
|
||||||
|
$priceDescription.text(wpBnbAdmin.i18n.perNightDescription || 'This price will be charged per night of the stay.');
|
||||||
|
} else {
|
||||||
|
$priceSuffix.text('');
|
||||||
|
$priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize booking services selector.
|
||||||
|
*/
|
||||||
|
function initBookingServices() {
|
||||||
|
var $servicesSelector = $('.bnb-services-selector');
|
||||||
|
var $servicesList = $servicesSelector.find('.bnb-services-list');
|
||||||
|
var $totalDisplay = $('#bnb-services-total-amount');
|
||||||
|
|
||||||
|
if (!$servicesSelector.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current number of nights from booking form.
|
||||||
|
*
|
||||||
|
* @return {number} Number of nights.
|
||||||
|
*/
|
||||||
|
function getNights() {
|
||||||
|
var checkIn = $('#bnb_booking_check_in').val();
|
||||||
|
var checkOut = $('#bnb_booking_check_out').val();
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
var startDate = new Date(checkIn);
|
||||||
|
var endDate = new Date(checkOut);
|
||||||
|
var nights = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
||||||
|
return Math.max(1, nights);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt($servicesSelector.data('nights'), 10) || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate service line total.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $item Service item element.
|
||||||
|
* @param {number} nights Number of nights.
|
||||||
|
* @return {number} Calculated price.
|
||||||
|
*/
|
||||||
|
function calculateServiceTotal($item, nights) {
|
||||||
|
var price = parseFloat($item.data('price')) || 0;
|
||||||
|
var pricingType = $item.data('pricing-type');
|
||||||
|
var quantity = parseInt($item.find('.bnb-service-qty-input').val(), 10) || 1;
|
||||||
|
|
||||||
|
if (pricingType === 'included') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingType === 'per_night') {
|
||||||
|
return price * quantity * nights;
|
||||||
|
}
|
||||||
|
|
||||||
|
return price * quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update service line total display.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $item Service item element.
|
||||||
|
*/
|
||||||
|
function updateServiceLineTotal($item) {
|
||||||
|
var nights = getNights();
|
||||||
|
var total = calculateServiceTotal($item, nights);
|
||||||
|
var $lineTotal = $item.find('.bnb-service-line-total');
|
||||||
|
var $totalValue = $item.find('.bnb-service-total-value');
|
||||||
|
var isSelected = $item.find('input[type="checkbox"]').is(':checked');
|
||||||
|
var pricingType = $item.data('pricing-type');
|
||||||
|
|
||||||
|
if (isSelected && pricingType !== 'included' && total > 0) {
|
||||||
|
$totalValue.text(formatPrice(total));
|
||||||
|
$lineTotal.show();
|
||||||
|
} else {
|
||||||
|
$lineTotal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update total services amount.
|
||||||
|
*/
|
||||||
|
function updateServicesTotal() {
|
||||||
|
var nights = getNights();
|
||||||
|
var total = 0;
|
||||||
|
|
||||||
|
$servicesList.find('.bnb-service-item').each(function() {
|
||||||
|
var $item = $(this);
|
||||||
|
var isSelected = $item.find('input[type="checkbox"]').is(':checked');
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
total += calculateServiceTotal($item, nights);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$totalDisplay.text(formatPrice(total));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price for display (simple formatting).
|
||||||
|
*
|
||||||
|
* @param {number} price Price value.
|
||||||
|
* @return {string} Formatted price.
|
||||||
|
*/
|
||||||
|
function formatPrice(price) {
|
||||||
|
return parseFloat(price).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle service checkbox change.
|
||||||
|
$servicesList.on('change', 'input[type="checkbox"]', function() {
|
||||||
|
var $item = $(this).closest('.bnb-service-item');
|
||||||
|
var isSelected = $(this).is(':checked');
|
||||||
|
|
||||||
|
$item.toggleClass('selected', isSelected);
|
||||||
|
|
||||||
|
// Show/hide quantity input.
|
||||||
|
var $quantity = $item.find('.bnb-service-quantity');
|
||||||
|
if ($quantity.length) {
|
||||||
|
$quantity.toggle(isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateServiceLineTotal($item);
|
||||||
|
updateServicesTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle quantity change.
|
||||||
|
$servicesList.on('change input', '.bnb-service-qty-input', function() {
|
||||||
|
var $item = $(this).closest('.bnb-service-item');
|
||||||
|
var maxQty = parseInt($item.data('max-quantity'), 10) || 1;
|
||||||
|
var value = parseInt($(this).val(), 10) || 1;
|
||||||
|
|
||||||
|
// Enforce min/max.
|
||||||
|
value = Math.max(1, Math.min(value, maxQty));
|
||||||
|
$(this).val(value);
|
||||||
|
|
||||||
|
updateServiceLineTotal($item);
|
||||||
|
updateServicesTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update when booking dates change.
|
||||||
|
$('#bnb_booking_check_in, #bnb_booking_check_out').on('change', function() {
|
||||||
|
$servicesList.find('.bnb-service-item.selected').each(function() {
|
||||||
|
updateServiceLineTotal($(this));
|
||||||
|
});
|
||||||
|
updateServicesTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial calculation.
|
||||||
|
updateServicesTotal();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
@@ -765,6 +944,8 @@
|
|||||||
initBookingForm();
|
initBookingForm();
|
||||||
initCalendarPage();
|
initCalendarPage();
|
||||||
initGuestSearch();
|
initGuestSearch();
|
||||||
|
initServicePricing();
|
||||||
|
initBookingServices();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ use Magdev\WpBnb\PostTypes\Booking;
|
|||||||
use Magdev\WpBnb\PostTypes\Building;
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
use Magdev\WpBnb\PostTypes\Guest;
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
use Magdev\WpBnb\PostTypes\Room;
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
||||||
use Magdev\WpBnb\Pricing\Season;
|
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 Magdev\WpBnb\Taxonomies\ServiceCategory;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Loader\FilesystemLoader;
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ final class Plugin {
|
|||||||
Room::init();
|
Room::init();
|
||||||
Booking::init();
|
Booking::init();
|
||||||
Guest::init();
|
Guest::init();
|
||||||
|
Service::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,6 +109,7 @@ final class Plugin {
|
|||||||
// Taxonomies must be registered before post types that use them.
|
// Taxonomies must be registered before post types that use them.
|
||||||
Amenity::init();
|
Amenity::init();
|
||||||
RoomType::init();
|
RoomType::init();
|
||||||
|
ServiceCategory::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,7 +192,7 @@ final class Plugin {
|
|||||||
|
|
||||||
// Check if we're on plugin pages or editing our custom post types.
|
// Check if we're on plugin pages or editing our custom post types.
|
||||||
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
||||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE ), true );
|
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
||||||
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
||||||
|
|
||||||
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
||||||
@@ -246,6 +250,8 @@ final class Plugin {
|
|||||||
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
||||||
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
||||||
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
|
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
||||||
|
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ use Magdev\WpBnb\Pricing\Calculator;
|
|||||||
*/
|
*/
|
||||||
final class Booking {
|
final class Booking {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services meta key.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const SERVICES_META_KEY = '_bnb_booking_services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post type slug.
|
* Post type slug.
|
||||||
*
|
*
|
||||||
@@ -125,6 +132,15 @@ final class Booking {
|
|||||||
'high'
|
'high'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_booking_services',
|
||||||
|
__( 'Additional Services', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_services_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
|
||||||
add_meta_box(
|
add_meta_box(
|
||||||
'bnb_booking_pricing',
|
'bnb_booking_pricing',
|
||||||
__( 'Pricing', 'wp-bnb' ),
|
__( 'Pricing', 'wp-bnb' ),
|
||||||
@@ -399,6 +415,111 @@ final class Booking {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render services meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_services_meta_box( \WP_Post $post ): void {
|
||||||
|
$selected_services = get_post_meta( $post->ID, self::SERVICES_META_KEY, true ) ?: array();
|
||||||
|
$check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in', true );
|
||||||
|
$check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out', true );
|
||||||
|
$nights = ( $check_in && $check_out ) ? self::calculate_nights( $check_in, $check_out ) : 1;
|
||||||
|
|
||||||
|
// Get all active services.
|
||||||
|
$available_services = Service::get_services_for_booking();
|
||||||
|
|
||||||
|
if ( empty( $available_services ) ) {
|
||||||
|
?>
|
||||||
|
<p class="bnb-no-services-message">
|
||||||
|
<?php esc_html_e( 'No services available.', 'wp-bnb' ); ?>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Service::POST_TYPE ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Add a service', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a lookup map for selected services.
|
||||||
|
$selected_map = array();
|
||||||
|
if ( is_array( $selected_services ) ) {
|
||||||
|
foreach ( $selected_services as $service ) {
|
||||||
|
if ( isset( $service['service_id'] ) ) {
|
||||||
|
$selected_map[ $service['service_id'] ] = $service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="bnb-services-selector" data-nights="<?php echo esc_attr( $nights ); ?>">
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Select additional services for this booking.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bnb-services-list">
|
||||||
|
<?php foreach ( $available_services as $service ) : ?>
|
||||||
|
<?php
|
||||||
|
$is_selected = isset( $selected_map[ $service['id'] ] );
|
||||||
|
$quantity = $is_selected ? ( $selected_map[ $service['id'] ]['quantity'] ?? 1 ) : 1;
|
||||||
|
$service_total = $is_selected
|
||||||
|
? Service::calculate_service_price( $service['id'], $quantity, $nights )
|
||||||
|
: 0;
|
||||||
|
?>
|
||||||
|
<div class="bnb-service-item <?php echo $is_selected ? 'selected' : ''; ?>"
|
||||||
|
data-service-id="<?php echo esc_attr( $service['id'] ); ?>"
|
||||||
|
data-price="<?php echo esc_attr( $service['price'] ); ?>"
|
||||||
|
data-pricing-type="<?php echo esc_attr( $service['pricing_type'] ); ?>"
|
||||||
|
data-max-quantity="<?php echo esc_attr( $service['max_quantity'] ); ?>">
|
||||||
|
<label class="bnb-service-checkbox">
|
||||||
|
<input type="checkbox" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][selected]"
|
||||||
|
value="1" <?php checked( $is_selected ); ?>>
|
||||||
|
<span class="bnb-service-name"><?php echo esc_html( $service['name'] ); ?></span>
|
||||||
|
</label>
|
||||||
|
<div class="bnb-service-details">
|
||||||
|
<span class="bnb-service-price-label">
|
||||||
|
<?php
|
||||||
|
if ( 'included' === $service['pricing_type'] ) {
|
||||||
|
echo '<span class="bnb-service-included-badge">' . esc_html__( 'Included', 'wp-bnb' ) . '</span>';
|
||||||
|
} else {
|
||||||
|
echo esc_html( $service['formatted_price'] );
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php if ( $service['max_quantity'] > 1 && 'included' !== $service['pricing_type'] ) : ?>
|
||||||
|
<span class="bnb-service-quantity" <?php echo ! $is_selected ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<label>
|
||||||
|
<?php esc_html_e( 'Qty:', 'wp-bnb' ); ?>
|
||||||
|
<input type="number" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][quantity]"
|
||||||
|
value="<?php echo esc_attr( $quantity ); ?>"
|
||||||
|
min="1" max="<?php echo esc_attr( $service['max_quantity'] ); ?>"
|
||||||
|
class="small-text bnb-service-qty-input">
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<?php else : ?>
|
||||||
|
<input type="hidden" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][quantity]" value="1">
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="bnb-service-line-total" <?php echo ( ! $is_selected || $service_total <= 0 ) ? 'style="display:none;"' : ''; ?>>
|
||||||
|
= <strong class="bnb-service-total-value"><?php echo esc_html( Calculator::formatPrice( $service_total ) ); ?></strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bnb-services-total">
|
||||||
|
<strong><?php esc_html_e( 'Services Total:', 'wp-bnb' ); ?></strong>
|
||||||
|
<span id="bnb-services-total-amount">
|
||||||
|
<?php
|
||||||
|
$services_total = self::calculate_booking_services_total( $post->ID );
|
||||||
|
echo esc_html( Calculator::formatPrice( $services_total ) );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render pricing meta box.
|
* Render pricing meta box.
|
||||||
*
|
*
|
||||||
@@ -447,6 +568,36 @@ final class Booking {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$services_total = self::calculate_booking_services_total( $post->ID );
|
||||||
|
if ( $services_total > 0 ) :
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<?php esc_html_e( 'Services', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-booking-services-summary">
|
||||||
|
<?php echo esc_html( Calculator::formatPrice( $services_total ) ); ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<?php esc_html_e( 'Grand Total', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-booking-grand-total">
|
||||||
|
<strong>
|
||||||
|
<?php
|
||||||
|
$room_price = (float) $calculated_price;
|
||||||
|
echo esc_html( Calculator::formatPrice( $room_price + $services_total ) );
|
||||||
|
?>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="bnb_booking_override_price"><?php esc_html_e( 'Override Price', 'wp-bnb' ); ?></label>
|
<label for="bnb_booking_override_price"><?php esc_html_e( 'Override Price', 'wp-bnb' ); ?></label>
|
||||||
@@ -678,6 +829,43 @@ final class Booking {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services.
|
||||||
|
$services_data = array();
|
||||||
|
if ( isset( $_POST['bnb_booking_services'] ) && is_array( $_POST['bnb_booking_services'] ) ) {
|
||||||
|
$nights = ( $check_in && $check_out ) ? self::calculate_nights( $check_in, $check_out ) : 1;
|
||||||
|
|
||||||
|
foreach ( $_POST['bnb_booking_services'] as $service_id => $service_input ) {
|
||||||
|
$service_id = absint( $service_id );
|
||||||
|
if ( ! $service_id ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include if selected checkbox is checked.
|
||||||
|
if ( empty( $service_input['selected'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify service exists and is active.
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
if ( ! $service_data || 'active' !== $service_data['status'] ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = isset( $service_input['quantity'] ) ? absint( $service_input['quantity'] ) : 1;
|
||||||
|
$quantity = max( 1, min( $quantity, $service_data['max_quantity'] ) );
|
||||||
|
|
||||||
|
$price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
|
||||||
|
$services_data[] = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $price,
|
||||||
|
'pricing_type' => $service_data['pricing_type'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_post_meta( $post_id, self::SERVICES_META_KEY, $services_data );
|
||||||
|
|
||||||
// Trigger status change action.
|
// Trigger status change action.
|
||||||
if ( $old_status && $status !== $old_status ) {
|
if ( $old_status && $status !== $old_status ) {
|
||||||
/**
|
/**
|
||||||
@@ -794,13 +982,21 @@ final class Booking {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'price':
|
case 'price':
|
||||||
$price = get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
|
$room_price = (float) get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
|
||||||
if ( $price ) {
|
$services_total = self::calculate_booking_services_total( $post_id );
|
||||||
echo esc_html( Calculator::formatPrice( (float) $price ) );
|
$total_price = $room_price + $services_total;
|
||||||
|
|
||||||
|
if ( $total_price > 0 ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( $total_price ) );
|
||||||
$override = get_post_meta( $post_id, self::META_PREFIX . 'override_price', true );
|
$override = get_post_meta( $post_id, self::META_PREFIX . 'override_price', true );
|
||||||
if ( $override ) {
|
if ( $override ) {
|
||||||
echo ' <span class="bnb-price-override" title="' . esc_attr__( 'Price manually overridden', 'wp-bnb' ) . '">*</span>';
|
echo ' <span class="bnb-price-override" title="' . esc_attr__( 'Price manually overridden', 'wp-bnb' ) . '">*</span>';
|
||||||
}
|
}
|
||||||
|
if ( $services_total > 0 ) {
|
||||||
|
echo '<br><small style="color: #646970;">' . esc_html__( 'incl. services', 'wp-bnb' ) . '</small>';
|
||||||
|
}
|
||||||
|
} elseif ( $room_price > 0 ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( $room_price ) );
|
||||||
} else {
|
} else {
|
||||||
echo '—';
|
echo '—';
|
||||||
}
|
}
|
||||||
@@ -1232,6 +1428,56 @@ final class Booking {
|
|||||||
return get_posts( array_merge( $defaults, $args ) );
|
return get_posts( array_merge( $defaults, $args ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total services cost for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return float Total services cost.
|
||||||
|
*/
|
||||||
|
public static function calculate_booking_services_total( int $booking_id ): float {
|
||||||
|
$services = get_post_meta( $booking_id, self::SERVICES_META_KEY, true );
|
||||||
|
|
||||||
|
if ( ! is_array( $services ) || empty( $services ) ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0.0;
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
if ( isset( $service['price'] ) ) {
|
||||||
|
$total += (float) $service['price'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selected services for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return array Array of service data with names.
|
||||||
|
*/
|
||||||
|
public static function get_booking_services( int $booking_id ): array {
|
||||||
|
$services = get_post_meta( $booking_id, self::SERVICES_META_KEY, true );
|
||||||
|
|
||||||
|
if ( ! is_array( $services ) || empty( $services ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$service_post = get_post( $service['service_id'] ?? 0 );
|
||||||
|
if ( $service_post ) {
|
||||||
|
$result[] = array_merge(
|
||||||
|
$service,
|
||||||
|
array( 'name' => $service_post->post_title )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format price breakdown for display.
|
* Format price breakdown for display.
|
||||||
*
|
*
|
||||||
|
|||||||
623
src/PostTypes/Service.php
Normal file
623
src/PostTypes/Service.php
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Service post type.
|
||||||
|
*
|
||||||
|
* Custom post type for BnB additional services.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\PostTypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\PostTypes;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service post type class.
|
||||||
|
*/
|
||||||
|
final class Service {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post type slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const POST_TYPE = 'bnb_service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta key prefix.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const META_PREFIX = '_bnb_service_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the post type.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register' ) );
|
||||||
|
add_action( 'add_meta_boxes', array( self::class, 'add_meta_boxes' ) );
|
||||||
|
add_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10, 2 );
|
||||||
|
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( self::class, 'add_columns' ) );
|
||||||
|
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( self::class, 'render_column' ), 10, 2 );
|
||||||
|
add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( self::class, 'sortable_columns' ) );
|
||||||
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the post type.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _x( 'Services', 'post type general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Service', 'post type singular name', 'wp-bnb' ),
|
||||||
|
'menu_name' => _x( 'Services', 'admin menu', 'wp-bnb' ),
|
||||||
|
'name_admin_bar' => _x( 'Service', 'add new on admin bar', 'wp-bnb' ),
|
||||||
|
'add_new' => _x( 'Add New', 'service', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Service', 'wp-bnb' ),
|
||||||
|
'new_item' => __( 'New Service', 'wp-bnb' ),
|
||||||
|
'edit_item' => __( 'Edit Service', 'wp-bnb' ),
|
||||||
|
'view_item' => __( 'View Service', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'Services', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Services', 'wp-bnb' ),
|
||||||
|
'parent_item_colon' => __( 'Parent Services:', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No services found.', 'wp-bnb' ),
|
||||||
|
'not_found_in_trash' => __( 'No services found in Trash.', 'wp-bnb' ),
|
||||||
|
'archives' => __( 'Service archives', 'wp-bnb' ),
|
||||||
|
'insert_into_item' => __( 'Insert into service', 'wp-bnb' ),
|
||||||
|
'uploaded_to_this_item' => __( 'Uploaded to this service', 'wp-bnb' ),
|
||||||
|
'filter_items_list' => __( 'Filter services list', 'wp-bnb' ),
|
||||||
|
'items_list_navigation' => __( 'Services list navigation', 'wp-bnb' ),
|
||||||
|
'items_list' => __( 'Services list', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'public' => false,
|
||||||
|
'publicly_queryable' => false,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => 'wp-bnb',
|
||||||
|
'query_var' => false,
|
||||||
|
'capability_type' => 'post',
|
||||||
|
'has_archive' => false,
|
||||||
|
'hierarchical' => false,
|
||||||
|
'menu_position' => null,
|
||||||
|
'menu_icon' => 'dashicons-plus-alt',
|
||||||
|
'supports' => array( 'title', 'editor', 'thumbnail' ),
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'rest_base' => 'services',
|
||||||
|
'rest_controller_class' => 'WP_REST_Posts_Controller',
|
||||||
|
);
|
||||||
|
|
||||||
|
register_post_type( self::POST_TYPE, $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add meta boxes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_meta_boxes(): void {
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_service_pricing',
|
||||||
|
__( 'Pricing', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_pricing_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_service_settings',
|
||||||
|
__( 'Service Settings', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_settings_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'side',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render pricing meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_pricing_meta_box( \WP_Post $post ): void {
|
||||||
|
wp_nonce_field( 'bnb_service_meta', 'bnb_service_meta_nonce' );
|
||||||
|
|
||||||
|
$pricing_type = get_post_meta( $post->ID, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||||
|
$price = get_post_meta( $post->ID, self::META_PREFIX . 'price', true );
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_service_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="bnb_service_pricing_type" value="included"
|
||||||
|
<?php checked( $pricing_type, 'included' ); ?>>
|
||||||
|
<?php esc_html_e( 'Included (Free)', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Service is included at no extra cost.', 'wp-bnb' ); ?></p>
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="bnb_service_pricing_type" value="per_booking"
|
||||||
|
<?php checked( $pricing_type, 'per_booking' ); ?>>
|
||||||
|
<?php esc_html_e( 'Per Booking (One-time)', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Fixed price charged once per booking.', 'wp-bnb' ); ?></p>
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="bnb_service_pricing_type" value="per_night"
|
||||||
|
<?php checked( $pricing_type, 'per_night' ); ?>>
|
||||||
|
<?php esc_html_e( 'Per Night', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Price multiplied by the number of nights.', 'wp-bnb' ); ?></p>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="bnb-service-price-row" <?php echo 'included' === $pricing_type ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_service_price"><?php esc_html_e( 'Price', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-price-input-wrapper">
|
||||||
|
<input type="number" id="bnb_service_price" name="bnb_service_price"
|
||||||
|
value="<?php echo esc_attr( $price ); ?>" class="small-text"
|
||||||
|
min="0" step="0.01">
|
||||||
|
<span class="bnb-price-unit"><?php echo esc_html( $currency ); ?></span>
|
||||||
|
<span id="bnb-service-price-suffix"></span>
|
||||||
|
</div>
|
||||||
|
<p class="description" id="bnb-service-price-description">
|
||||||
|
<?php
|
||||||
|
if ( 'per_night' === $pricing_type ) {
|
||||||
|
esc_html_e( 'This price will be charged per night of the stay.', 'wp-bnb' );
|
||||||
|
} else {
|
||||||
|
esc_html_e( 'This price will be charged once for the booking.', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render settings meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_settings_meta_box( \WP_Post $post ): void {
|
||||||
|
$status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'active';
|
||||||
|
$sort_order = get_post_meta( $post->ID, self::META_PREFIX . 'sort_order', true ) ?: 0;
|
||||||
|
$max_qty = get_post_meta( $post->ID, self::META_PREFIX . 'max_quantity', true ) ?: 1;
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_service_status"><strong><?php esc_html_e( 'Status', 'wp-bnb' ); ?></strong></label>
|
||||||
|
</p>
|
||||||
|
<select id="bnb_service_status" name="bnb_service_status" class="widefat">
|
||||||
|
<option value="active" <?php selected( $status, 'active' ); ?>><?php esc_html_e( 'Active', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="inactive" <?php selected( $status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php esc_html_e( 'Inactive services cannot be added to bookings.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="bnb_service_sort_order"><strong><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></strong></label>
|
||||||
|
</p>
|
||||||
|
<input type="number" id="bnb_service_sort_order" name="bnb_service_sort_order"
|
||||||
|
value="<?php echo esc_attr( $sort_order ); ?>" class="small-text" min="0">
|
||||||
|
<p class="description"><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="bnb_service_max_quantity"><strong><?php esc_html_e( 'Maximum Quantity', 'wp-bnb' ); ?></strong></label>
|
||||||
|
</p>
|
||||||
|
<input type="number" id="bnb_service_max_quantity" name="bnb_service_max_quantity"
|
||||||
|
value="<?php echo esc_attr( $max_qty ); ?>" class="small-text" min="1" max="99">
|
||||||
|
<p class="description"><?php esc_html_e( 'Maximum times this service can be added to a booking.', 'wp-bnb' ); ?></p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save post meta.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function save_meta( int $post_id, \WP_Post $post ): void {
|
||||||
|
// Verify nonce.
|
||||||
|
if ( ! isset( $_POST['bnb_service_meta_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( sanitize_key( $_POST['bnb_service_meta_nonce'] ), 'bnb_service_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosave.
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions.
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pricing type.
|
||||||
|
$valid_pricing_types = array( 'included', 'per_booking', 'per_night' );
|
||||||
|
$pricing_type = isset( $_POST['bnb_service_pricing_type'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['bnb_service_pricing_type'] ) )
|
||||||
|
: 'per_booking';
|
||||||
|
if ( in_array( $pricing_type, $valid_pricing_types, true ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'pricing_type', $pricing_type );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price (not required for 'included').
|
||||||
|
if ( 'included' === $pricing_type ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'price', 0 );
|
||||||
|
} elseif ( isset( $_POST['bnb_service_price'] ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'price', floatval( $_POST['bnb_service_price'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status.
|
||||||
|
$valid_statuses = array( 'active', 'inactive' );
|
||||||
|
$status = isset( $_POST['bnb_service_status'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['bnb_service_status'] ) )
|
||||||
|
: 'active';
|
||||||
|
if ( in_array( $status, $valid_statuses, true ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'status', $status );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort order.
|
||||||
|
if ( isset( $_POST['bnb_service_sort_order'] ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'sort_order', absint( $_POST['bnb_service_sort_order'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max quantity.
|
||||||
|
if ( isset( $_POST['bnb_service_max_quantity'] ) ) {
|
||||||
|
$max_qty = absint( $_POST['bnb_service_max_quantity'] );
|
||||||
|
$max_qty = max( 1, min( 99, $max_qty ) );
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'max_quantity', $max_qty );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom columns to the post list.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function add_columns( array $columns ): array {
|
||||||
|
$new_columns = array();
|
||||||
|
foreach ( $columns as $key => $value ) {
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
if ( 'title' === $key ) {
|
||||||
|
$new_columns['pricing_type'] = __( 'Pricing Type', 'wp-bnb' );
|
||||||
|
$new_columns['price'] = __( 'Price', 'wp-bnb' );
|
||||||
|
$new_columns['service_status'] = __( 'Status', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove date column.
|
||||||
|
unset( $new_columns['date'] );
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render custom column content.
|
||||||
|
*
|
||||||
|
* @param string $column Column name.
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_column( string $column, int $post_id ): void {
|
||||||
|
switch ( $column ) {
|
||||||
|
case 'pricing_type':
|
||||||
|
$pricing_type = get_post_meta( $post_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||||
|
$labels = self::get_pricing_type_labels();
|
||||||
|
$icons = array(
|
||||||
|
'included' => 'yes-alt',
|
||||||
|
'per_booking' => 'tag',
|
||||||
|
'per_night' => 'calendar-alt',
|
||||||
|
);
|
||||||
|
$colors = array(
|
||||||
|
'included' => '#00a32a',
|
||||||
|
'per_booking' => '#135e96',
|
||||||
|
'per_night' => '#dba617',
|
||||||
|
);
|
||||||
|
echo '<span class="dashicons dashicons-' . esc_attr( $icons[ $pricing_type ] ?? 'admin-generic' ) . '" style="color: ' . esc_attr( $colors[ $pricing_type ] ?? '#646970' ) . '; vertical-align: middle; margin-right: 3px;"></span>';
|
||||||
|
echo esc_html( $labels[ $pricing_type ] ?? $pricing_type );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'price':
|
||||||
|
$pricing_type = get_post_meta( $post_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||||
|
if ( 'included' === $pricing_type ) {
|
||||||
|
echo '<span class="bnb-service-included">' . esc_html__( 'Included', 'wp-bnb' ) . '</span>';
|
||||||
|
} else {
|
||||||
|
$price = get_post_meta( $post_id, self::META_PREFIX . 'price', true );
|
||||||
|
if ( $price ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( (float) $price ) );
|
||||||
|
if ( 'per_night' === $pricing_type ) {
|
||||||
|
echo ' <small style="color: #646970;">' . esc_html__( '/ night', 'wp-bnb' ) . '</small>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<span class="bnb-no-price">' . esc_html__( 'Not set', 'wp-bnb' ) . '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'service_status':
|
||||||
|
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'active';
|
||||||
|
$classes = array(
|
||||||
|
'active' => 'bnb-service-status-active',
|
||||||
|
'inactive' => 'bnb-service-status-inactive',
|
||||||
|
);
|
||||||
|
$labels = array(
|
||||||
|
'active' => __( 'Active', 'wp-bnb' ),
|
||||||
|
'inactive' => __( 'Inactive', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
echo '<span class="bnb-service-status ' . esc_attr( $classes[ $status ] ?? '' ) . '">';
|
||||||
|
echo esc_html( $labels[ $status ] ?? $status );
|
||||||
|
echo '</span>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add sortable columns.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing sortable columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function sortable_columns( array $columns ): array {
|
||||||
|
$columns['price'] = 'price';
|
||||||
|
$columns['service_status'] = 'status';
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add filter dropdowns to admin list.
|
||||||
|
*
|
||||||
|
* @param string $post_type Current post type.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_filters( string $post_type ): void {
|
||||||
|
if ( self::POST_TYPE !== $post_type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||||||
|
$selected_status = isset( $_GET['service_status'] ) ? sanitize_text_field( wp_unslash( $_GET['service_status'] ) ) : '';
|
||||||
|
?>
|
||||||
|
<select name="service_status">
|
||||||
|
<option value=""><?php esc_html_e( 'All Statuses', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="active" <?php selected( $selected_status, 'active' ); ?>><?php esc_html_e( 'Active', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="inactive" <?php selected( $selected_status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></option>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Pricing type filter.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||||||
|
$selected_pricing = isset( $_GET['pricing_type'] ) ? sanitize_text_field( wp_unslash( $_GET['pricing_type'] ) ) : '';
|
||||||
|
$labels = self::get_pricing_type_labels();
|
||||||
|
?>
|
||||||
|
<select name="pricing_type">
|
||||||
|
<option value=""><?php esc_html_e( 'All Pricing Types', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $labels as $value => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_pricing, $value ); ?>>
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter services by status and pricing type in admin list.
|
||||||
|
*
|
||||||
|
* @param \WP_Query $query Current query.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function filter_query( \WP_Query $query ): void {
|
||||||
|
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta_query = array();
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
if ( ! empty( $_GET['service_status'] ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => self::META_PREFIX . 'status',
|
||||||
|
'value' => sanitize_text_field( wp_unslash( $_GET['service_status'] ) ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
if ( ! empty( $_GET['pricing_type'] ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => self::META_PREFIX . 'pricing_type',
|
||||||
|
'value' => sanitize_text_field( wp_unslash( $_GET['pricing_type'] ) ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta_query ) ) {
|
||||||
|
$meta_query['relation'] = 'AND';
|
||||||
|
$query->set( 'meta_query', $meta_query );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sorting.
|
||||||
|
$orderby = $query->get( 'orderby' );
|
||||||
|
if ( 'price' === $orderby ) {
|
||||||
|
$query->set( 'meta_key', self::META_PREFIX . 'price' );
|
||||||
|
$query->set( 'orderby', 'meta_value_num' );
|
||||||
|
} elseif ( 'status' === $orderby ) {
|
||||||
|
$query->set( 'meta_key', self::META_PREFIX . 'status' );
|
||||||
|
$query->set( 'orderby', 'meta_value' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change title placeholder.
|
||||||
|
*
|
||||||
|
* @param string $placeholder Default placeholder.
|
||||||
|
* @param \WP_Post $post Current post.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
||||||
|
if ( self::POST_TYPE === $post->post_type ) {
|
||||||
|
return __( 'Service name', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
return $placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pricing type labels.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_pricing_type_labels(): array {
|
||||||
|
return array(
|
||||||
|
'included' => __( 'Included (Free)', 'wp-bnb' ),
|
||||||
|
'per_booking' => __( 'Per Booking', 'wp-bnb' ),
|
||||||
|
'per_night' => __( 'Per Night', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active services.
|
||||||
|
*
|
||||||
|
* @param array $args Additional query args.
|
||||||
|
* @return array<\WP_Post>
|
||||||
|
*/
|
||||||
|
public static function get_active_services( array $args = array() ): array {
|
||||||
|
$defaults = array(
|
||||||
|
'post_type' => self::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => self::META_PREFIX . 'status',
|
||||||
|
'value' => 'active',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'meta_key' => self::META_PREFIX . 'sort_order',
|
||||||
|
'orderby' => 'meta_value_num',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
return get_posts( array_merge( $defaults, $args ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get service data for a service.
|
||||||
|
*
|
||||||
|
* @param int $service_id Service post ID.
|
||||||
|
* @return array|null Service data or null if not found.
|
||||||
|
*/
|
||||||
|
public static function get_service_data( int $service_id ): ?array {
|
||||||
|
$service = get_post( $service_id );
|
||||||
|
if ( ! $service || self::POST_TYPE !== $service->post_type ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $service_id,
|
||||||
|
'name' => $service->post_title,
|
||||||
|
'description' => $service->post_content,
|
||||||
|
'pricing_type' => get_post_meta( $service_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking',
|
||||||
|
'price' => (float) get_post_meta( $service_id, self::META_PREFIX . 'price', true ),
|
||||||
|
'status' => get_post_meta( $service_id, self::META_PREFIX . 'status', true ) ?: 'active',
|
||||||
|
'sort_order' => (int) get_post_meta( $service_id, self::META_PREFIX . 'sort_order', true ),
|
||||||
|
'max_quantity' => (int) get_post_meta( $service_id, self::META_PREFIX . 'max_quantity', true ) ?: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate service price for a booking.
|
||||||
|
*
|
||||||
|
* @param int $service_id Service post ID.
|
||||||
|
* @param int $quantity Quantity of the service.
|
||||||
|
* @param int $nights Number of nights (for per-night pricing).
|
||||||
|
* @return float Calculated price.
|
||||||
|
*/
|
||||||
|
public static function calculate_service_price( int $service_id, int $quantity = 1, int $nights = 1 ): float {
|
||||||
|
$data = self::get_service_data( $service_id );
|
||||||
|
if ( ! $data ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'included' === $data['pricing_type'] ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$base_price = $data['price'];
|
||||||
|
|
||||||
|
if ( 'per_night' === $data['pricing_type'] ) {
|
||||||
|
return $base_price * $quantity * max( 1, $nights );
|
||||||
|
}
|
||||||
|
|
||||||
|
// per_booking.
|
||||||
|
return $base_price * $quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get services for booking display/selection.
|
||||||
|
*
|
||||||
|
* @return array Array of services with their data.
|
||||||
|
*/
|
||||||
|
public static function get_services_for_booking(): array {
|
||||||
|
$services = self::get_active_services();
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$data = self::get_service_data( $service->ID );
|
||||||
|
if ( $data ) {
|
||||||
|
$data['formatted_price'] = self::format_service_price( $data );
|
||||||
|
$result[] = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format service price for display.
|
||||||
|
*
|
||||||
|
* @param array $service_data Service data array.
|
||||||
|
* @return string Formatted price string.
|
||||||
|
*/
|
||||||
|
public static function format_service_price( array $service_data ): string {
|
||||||
|
if ( 'included' === $service_data['pricing_type'] ) {
|
||||||
|
return __( 'Included', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatted = Calculator::formatPrice( $service_data['price'] );
|
||||||
|
|
||||||
|
if ( 'per_night' === $service_data['pricing_type'] ) {
|
||||||
|
/* translators: %s: Formatted price */
|
||||||
|
return sprintf( __( '%s / night', 'wp-bnb' ), $formatted );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
276
src/Taxonomies/ServiceCategory.php
Normal file
276
src/Taxonomies/ServiceCategory.php
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Service Category taxonomy.
|
||||||
|
*
|
||||||
|
* Non-hierarchical taxonomy for categorizing additional services.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Taxonomies
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Taxonomies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Category taxonomy class.
|
||||||
|
*/
|
||||||
|
final class ServiceCategory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taxonomy slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const TAXONOMY = 'bnb_service_category';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register' ) );
|
||||||
|
add_action( 'bnb_service_category_add_form_fields', array( self::class, 'add_form_fields' ) );
|
||||||
|
add_action( 'bnb_service_category_edit_form_fields', array( self::class, 'edit_form_fields' ), 10, 2 );
|
||||||
|
add_action( 'created_bnb_service_category', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_action( 'edited_bnb_service_category', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_filter( 'manage_edit-bnb_service_category_columns', array( self::class, 'add_columns' ) );
|
||||||
|
add_filter( 'manage_bnb_service_category_custom_column', array( self::class, 'render_column' ), 10, 3 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _x( 'Service Categories', 'taxonomy general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Service Category', 'taxonomy singular name', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Service Categories', 'wp-bnb' ),
|
||||||
|
'popular_items' => __( 'Popular Service Categories', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'All Service Categories', 'wp-bnb' ),
|
||||||
|
'parent_item' => null,
|
||||||
|
'parent_item_colon' => null,
|
||||||
|
'edit_item' => __( 'Edit Service Category', 'wp-bnb' ),
|
||||||
|
'update_item' => __( 'Update Service Category', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Service Category', 'wp-bnb' ),
|
||||||
|
'new_item_name' => __( 'New Service Category Name', 'wp-bnb' ),
|
||||||
|
'separate_items_with_commas' => __( 'Separate categories with commas', 'wp-bnb' ),
|
||||||
|
'add_or_remove_items' => __( 'Add or remove categories', 'wp-bnb' ),
|
||||||
|
'choose_from_most_used' => __( 'Choose from the most used categories', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No service categories found.', 'wp-bnb' ),
|
||||||
|
'menu_name' => __( 'Categories', 'wp-bnb' ),
|
||||||
|
'back_to_items' => __( '← Back to Categories', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'hierarchical' => false, // Non-hierarchical (like tags).
|
||||||
|
'public' => true,
|
||||||
|
'publicly_queryable' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => true,
|
||||||
|
'show_in_nav_menus' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'show_tagcloud' => false,
|
||||||
|
'show_in_quick_edit' => true,
|
||||||
|
'show_admin_column' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'service-category',
|
||||||
|
'with_front' => false,
|
||||||
|
),
|
||||||
|
'query_var' => true,
|
||||||
|
'capabilities' => array(
|
||||||
|
'manage_terms' => 'manage_options',
|
||||||
|
'edit_terms' => 'manage_options',
|
||||||
|
'delete_terms' => 'manage_options',
|
||||||
|
'assign_terms' => 'edit_posts',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
register_taxonomy( self::TAXONOMY, array( 'bnb_service' ), $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom fields to the add term form.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_form_fields(): void {
|
||||||
|
?>
|
||||||
|
<div class="form-field term-icon-wrap">
|
||||||
|
<label for="service-category-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||||
|
<select name="service_category_icon" id="service-category-icon">
|
||||||
|
<?php foreach ( self::get_icon_options() as $value => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<p><?php esc_html_e( 'Select an icon to represent this category.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-field term-sort-order-wrap">
|
||||||
|
<label for="service-category-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" name="service_category_sort_order" id="service-category-sort-order" value="0" min="0">
|
||||||
|
<p><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom fields to the edit term form.
|
||||||
|
*
|
||||||
|
* @param \WP_Term $term Current term object.
|
||||||
|
* @param string $taxonomy Current taxonomy slug.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function edit_form_fields( \WP_Term $term, string $taxonomy ): void {
|
||||||
|
$icon = get_term_meta( $term->term_id, 'service_category_icon', true );
|
||||||
|
$sort_order = get_term_meta( $term->term_id, 'service_category_sort_order', true );
|
||||||
|
?>
|
||||||
|
<tr class="form-field term-icon-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="service-category-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="service_category_icon" id="service-category-icon">
|
||||||
|
<?php foreach ( self::get_icon_options() as $value => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $icon, $value ); ?>>
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php esc_html_e( 'Select an icon to represent this category.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="form-field term-sort-order-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="service-category-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="service_category_sort_order" id="service-category-sort-order"
|
||||||
|
value="<?php echo esc_attr( $sort_order ?: '0' ); ?>" min="0">
|
||||||
|
<p class="description"><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save term meta data.
|
||||||
|
*
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @param int $tt_id Term taxonomy ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function save_term_meta( int $term_id, int $tt_id ): void {
|
||||||
|
if ( isset( $_POST['service_category_icon'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'service_category_icon',
|
||||||
|
sanitize_text_field( wp_unslash( $_POST['service_category_icon'] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['service_category_sort_order'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'service_category_sort_order',
|
||||||
|
absint( $_POST['service_category_sort_order'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom columns to the taxonomy list.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function add_columns( array $columns ): array {
|
||||||
|
$new_columns = array();
|
||||||
|
foreach ( $columns as $key => $value ) {
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
if ( 'name' === $key ) {
|
||||||
|
$new_columns['icon'] = __( 'Icon', 'wp-bnb' );
|
||||||
|
$new_columns['sort_order'] = __( 'Sort Order', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render custom column content.
|
||||||
|
*
|
||||||
|
* @param string $content Column content.
|
||||||
|
* @param string $column_name Column name.
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function render_column( string $content, string $column_name, int $term_id ): string {
|
||||||
|
if ( 'icon' === $column_name ) {
|
||||||
|
$icon = get_term_meta( $term_id, 'service_category_icon', true );
|
||||||
|
if ( $icon ) {
|
||||||
|
return '<span class="dashicons dashicons-' . esc_attr( $icon ) . '"></span>';
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
if ( 'sort_order' === $column_name ) {
|
||||||
|
$sort_order = get_term_meta( $term_id, 'service_category_sort_order', true );
|
||||||
|
return esc_html( $sort_order ?: '0' );
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available icon options.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_icon_options(): array {
|
||||||
|
return array(
|
||||||
|
'' => __( '— Select Icon —', 'wp-bnb' ),
|
||||||
|
'food' => __( 'Food & Dining', 'wp-bnb' ),
|
||||||
|
'car' => __( 'Transportation', 'wp-bnb' ),
|
||||||
|
'heart' => __( 'Wellness & Spa', 'wp-bnb' ),
|
||||||
|
'tickets-alt' => __( 'Activities', 'wp-bnb' ),
|
||||||
|
'admin-home' => __( 'Housekeeping', 'wp-bnb' ),
|
||||||
|
'admin-appearance' => __( 'Room Service', 'wp-bnb' ),
|
||||||
|
'store' => __( 'Shopping', 'wp-bnb' ),
|
||||||
|
'groups' => __( 'Childcare', 'wp-bnb' ),
|
||||||
|
'pets' => __( 'Pet Services', 'wp-bnb' ),
|
||||||
|
'businessman' => __( 'Business', 'wp-bnb' ),
|
||||||
|
'calendar' => __( 'Events', 'wp-bnb' ),
|
||||||
|
'camera' => __( 'Photography', 'wp-bnb' ),
|
||||||
|
'admin-generic' => __( 'Other', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default service categories to seed on activation.
|
||||||
|
*
|
||||||
|
* @return array<string, array{icon: string, sort_order: int}>
|
||||||
|
*/
|
||||||
|
public static function get_default_terms(): array {
|
||||||
|
return array(
|
||||||
|
__( 'Food & Dining', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'food',
|
||||||
|
'sort_order' => 10,
|
||||||
|
),
|
||||||
|
__( 'Transportation', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'car',
|
||||||
|
'sort_order' => 20,
|
||||||
|
),
|
||||||
|
__( 'Wellness & Spa', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'heart',
|
||||||
|
'sort_order' => 30,
|
||||||
|
),
|
||||||
|
__( 'Activities', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'tickets-alt',
|
||||||
|
'sort_order' => 40,
|
||||||
|
),
|
||||||
|
__( 'Housekeeping', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'admin-home',
|
||||||
|
'sort_order' => 50,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.4.0
|
* Version: 0.5.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.4.0' );
|
define( 'WP_BNB_VERSION', '0.5.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