Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c601df568 | |||
| dabfe1e826 | |||
| f24a347bb1 |
159
CHANGELOG.md
159
CHANGELOG.md
@@ -5,6 +5,162 @@ 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.3.0] - 2026-01-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Booking System with full management features:
|
||||||
|
- Custom Post Type: Bookings (`bnb_booking`)
|
||||||
|
- Room and guest relationship tracking
|
||||||
|
- Check-in/check-out date management
|
||||||
|
- Booking status workflow (pending, confirmed, checked_in, checked_out, cancelled)
|
||||||
|
- Status transitions with validation
|
||||||
|
- Automatic price calculation using existing Calculator
|
||||||
|
- Price override option for manual adjustments
|
||||||
|
- Guest information storage (name, email, phone, notes)
|
||||||
|
- Guest count tracking (adults, children)
|
||||||
|
- Internal notes field for staff
|
||||||
|
- Auto-generated booking references (BNB-YYYY-NNNNN)
|
||||||
|
- Availability System (`src/Booking/Availability.php`)
|
||||||
|
- Real-time availability checking
|
||||||
|
- Conflict detection for overlapping bookings
|
||||||
|
- AJAX endpoint for instant availability validation
|
||||||
|
- Calendar data generation for rooms and buildings
|
||||||
|
- Support for excluding booking from conflict check (for editing)
|
||||||
|
- Utility methods: get upcoming bookings, current bookings, today's check-ins/outs
|
||||||
|
- Calendar Admin Page (WP BnB > Calendar)
|
||||||
|
- Monthly calendar view with availability visualization
|
||||||
|
- Room and building filter dropdowns
|
||||||
|
- Color-coded booking status display
|
||||||
|
- Month navigation (previous/next/today)
|
||||||
|
- Click-to-edit booking functionality
|
||||||
|
- Hover tooltips with booking details
|
||||||
|
- Legend for status colors
|
||||||
|
- Single room and multi-room views
|
||||||
|
- Email Notifications (`src/Booking/EmailNotifier.php`)
|
||||||
|
- Admin notification for new bookings
|
||||||
|
- Guest confirmation email on booking confirmation
|
||||||
|
- Admin notification on booking confirmation
|
||||||
|
- Cancellation emails to guest and admin
|
||||||
|
- HTML email templates with styling
|
||||||
|
- Placeholder-based template system
|
||||||
|
- Filter hooks for customizing recipients, subject, and content
|
||||||
|
- Booking Admin List Enhancements
|
||||||
|
- Custom columns: room, guest, dates, nights, price, status
|
||||||
|
- Status badges with color coding
|
||||||
|
- Filter by room and status
|
||||||
|
- Sortable columns for dates, guest, status
|
||||||
|
- Price override indicator
|
||||||
|
- Booking Meta Boxes
|
||||||
|
- Room & Dates: room selection, date pickers, nights display, availability check
|
||||||
|
- Guest Information: contact details, guest count, notes
|
||||||
|
- Pricing: calculated price, breakdown display, recalculate button, override
|
||||||
|
- Status & Notes: status dropdown with preview, internal notes
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php enhanced with AJAX handlers and component initialization
|
||||||
|
- Admin JavaScript updated with booking form functionality
|
||||||
|
- Admin CSS updated with booking and calendar styles
|
||||||
|
- Asset enqueuing now includes Booking post type screens
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Conflict detection prevents double-booking
|
||||||
|
- Date validation ensures check-out is after check-in
|
||||||
|
- Status transition validation prevents invalid state changes
|
||||||
|
- Nonce verification on availability AJAX requests
|
||||||
|
- Capability checks on all booking operations
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Custom Post Type: Buildings (`bnb_building`)
|
||||||
|
- Address fields (street, city, state, ZIP, country)
|
||||||
|
- Contact information (phone, email, website)
|
||||||
|
- Building details (total rooms, floors, year built)
|
||||||
|
- Check-in/check-out time configuration
|
||||||
|
- Featured image support
|
||||||
|
- Custom admin columns (city, country, room count)
|
||||||
|
- Sortable columns
|
||||||
|
- Custom Post Type: Rooms (`bnb_room`)
|
||||||
|
- Building relationship (parent building selection)
|
||||||
|
- Room details (number, floor, size, capacity)
|
||||||
|
- Guest capacity (total, max adults, max children)
|
||||||
|
- Beds description and bathroom count
|
||||||
|
- Room status (available, occupied, maintenance, blocked)
|
||||||
|
- Image gallery with drag-and-drop sorting
|
||||||
|
- Featured image support
|
||||||
|
- Custom admin columns (building, room number, type, capacity, status)
|
||||||
|
- Building filter dropdown in admin list
|
||||||
|
- Custom Taxonomy: Room Types (`bnb_room_type`)
|
||||||
|
- Hierarchical (category-like) structure
|
||||||
|
- Default types: Standard, Superior, Suite, Family, Accessible, Apartment
|
||||||
|
- Subtypes: Single, Double, Twin, Junior Suite, Executive Suite
|
||||||
|
- Base capacity meta field
|
||||||
|
- Sort order meta field
|
||||||
|
- Custom Taxonomy: Amenities (`bnb_amenity`)
|
||||||
|
- Non-hierarchical (tag-like) structure
|
||||||
|
- Default amenities: WiFi, Parking, Breakfast, TV, A/C, Pet Friendly, etc.
|
||||||
|
- Dashicon selection for visual display
|
||||||
|
- Custom column showing icon
|
||||||
|
- Admin enhancements
|
||||||
|
- Gallery meta box with media library integration
|
||||||
|
- Status badges with color coding
|
||||||
|
- Custom title placeholders for each post type
|
||||||
|
- Post type edit screens with proper asset loading
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated admin assets to handle post type edit screens
|
||||||
|
- Enhanced asset enqueuing to include jQuery UI Sortable for galleries
|
||||||
|
- Improved localization with additional i18n strings
|
||||||
|
|
||||||
## [0.0.1] - 2026-01-31
|
## [0.0.1] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -35,4 +191,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.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.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
|
||||||
|
|||||||
187
CLAUDE.md
187
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,8 +206,25 @@ 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
|
||||||
│ └── License/
|
│ ├── Admin/ # Admin pages
|
||||||
│ └── Manager.php # License management
|
│ │ ├── Calendar.php # Availability calendar page
|
||||||
|
│ │ └── Seasons.php # Seasons management page
|
||||||
|
│ ├── Booking/ # Booking system
|
||||||
|
│ │ ├── Availability.php # Availability checking
|
||||||
|
│ │ └── EmailNotifier.php # Email notifications
|
||||||
|
│ ├── License/
|
||||||
|
│ │ └── Manager.php # License management
|
||||||
|
│ ├── PostTypes/ # Custom post types
|
||||||
|
│ │ ├── Booking.php # Booking post type
|
||||||
|
│ │ ├── Building.php # Building 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
|
||||||
|
│ ├── Amenity.php # Amenity taxonomy (tags)
|
||||||
|
│ └── RoomType.php # Room type taxonomy (categories)
|
||||||
├── lib/ # Git submodules
|
├── lib/ # Git submodules
|
||||||
│ └── wc-licensed-product-client/ # License client library
|
│ └── wc-licensed-product-client/ # License client library
|
||||||
├── vendor/ # Composer dependencies (auto-generated)
|
├── vendor/ # Composer dependencies (auto-generated)
|
||||||
@@ -303,3 +311,160 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Settings page uses tabs with nonce-protected form submission
|
- Settings page uses tabs with nonce-protected form submission
|
||||||
- AJAX handlers require `check_ajax_referer()` and `current_user_can()` checks
|
- AJAX handlers require `check_ajax_referer()` and `current_user_can()` checks
|
||||||
- CI/CD workflow excludes `lib/` directory but includes `vendor/` in releases
|
- CI/CD workflow excludes `lib/` directory but includes `vendor/` in releases
|
||||||
|
|
||||||
|
### 2026-01-31 - Version 0.1.0 (Core Data Structures)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created Custom Post Type: Buildings (`bnb_building`)
|
||||||
|
- Address meta box with full address fields
|
||||||
|
- Contact meta box with phone, email, website
|
||||||
|
- Details meta box with rooms count, floors, year built, check-in/out times
|
||||||
|
- Custom admin columns (city, country, room count)
|
||||||
|
- Sortable columns and country dropdown
|
||||||
|
- Created Custom Post Type: Rooms (`bnb_room`)
|
||||||
|
- Building relationship via meta field
|
||||||
|
- Room details: number, floor, size, capacity, beds, bathrooms
|
||||||
|
- Room status with color-coded badges
|
||||||
|
- Image gallery with media library and drag-and-drop sorting
|
||||||
|
- Building filter dropdown in admin list
|
||||||
|
- Custom admin columns with building link
|
||||||
|
- Created Custom Taxonomy: Room Types (`bnb_room_type`)
|
||||||
|
- Hierarchical structure with parent/child support
|
||||||
|
- Base capacity and sort order meta fields
|
||||||
|
- Default terms with subtypes (Standard > Single/Double/Twin, etc.)
|
||||||
|
- Created Custom Taxonomy: Amenities (`bnb_amenity`)
|
||||||
|
- Non-hierarchical (tag-like) structure
|
||||||
|
- Dashicon selection for visual display
|
||||||
|
- Icon column in taxonomy list
|
||||||
|
- Default amenities: WiFi, Parking, Breakfast, etc.
|
||||||
|
- Updated Plugin class to register post types and taxonomies
|
||||||
|
- Enhanced admin assets for post type edit screens
|
||||||
|
- Added gallery JavaScript with media library integration
|
||||||
|
- Updated activation hook to register CPTs before flushing rewrites
|
||||||
|
- Updated version to 0.1.0
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Taxonomies must be registered before post types that use them
|
||||||
|
- `show_in_menu => 'wp-bnb'` adds post types under the plugin's main menu
|
||||||
|
- Room-building relationship uses post meta, not hierarchical post types
|
||||||
|
- Gallery implementation uses `wp.media` frame with multiple selection
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 2026-01-31 - Version 0.3.0 (Booking System)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/PostTypes/Booking.php` custom post type
|
||||||
|
- Room and guest relationship tracking
|
||||||
|
- Check-in/check-out date management with validation
|
||||||
|
- Status workflow (pending, confirmed, checked_in, checked_out, cancelled)
|
||||||
|
- Auto-generated booking references (BNB-YYYY-NNNNN)
|
||||||
|
- Four meta boxes: Room & Dates, Guest Info, Pricing, Status & Notes
|
||||||
|
- Conflict detection prevents double-booking
|
||||||
|
- Price calculation using existing Calculator class
|
||||||
|
- Admin columns with room, guest, dates, nights, price, status
|
||||||
|
- Filters by room and status
|
||||||
|
- Status badges with color coding
|
||||||
|
- Created `src/Booking/Availability.php` class
|
||||||
|
- Real-time availability checking via AJAX
|
||||||
|
- Conflict detection algorithm
|
||||||
|
- Calendar data generation for rooms and buildings
|
||||||
|
- Utility methods for upcoming bookings, today's check-ins/outs
|
||||||
|
- Created `src/Admin/Calendar.php` admin page
|
||||||
|
- Monthly calendar view with room/building filters
|
||||||
|
- Color-coded booking status display
|
||||||
|
- Month navigation (previous/next/today)
|
||||||
|
- Click-to-edit booking functionality
|
||||||
|
- Hover tooltips with booking details
|
||||||
|
- Legend for status colors
|
||||||
|
- Created `src/Booking/EmailNotifier.php` class
|
||||||
|
- Admin notification for new bookings
|
||||||
|
- Guest confirmation email on booking confirmation
|
||||||
|
- Cancellation emails to guest and admin
|
||||||
|
- HTML email templates with inline styles
|
||||||
|
- Placeholder-based template system
|
||||||
|
- Filter hooks for customizing emails
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Registered Booking post type
|
||||||
|
- Initialized Calendar admin page
|
||||||
|
- Initialized EmailNotifier
|
||||||
|
- Added AJAX handler for availability checking
|
||||||
|
- Updated asset enqueuing for Booking screens
|
||||||
|
- Updated `assets/js/admin.js`
|
||||||
|
- Booking form with AJAX availability checking
|
||||||
|
- Real-time nights display
|
||||||
|
- Price calculation and display
|
||||||
|
- Status preview update
|
||||||
|
- Date validation (check-out after check-in)
|
||||||
|
- Calendar page interactivity
|
||||||
|
- Updated `assets/css/admin.css`
|
||||||
|
- Booking info display styles
|
||||||
|
- Availability status indicators
|
||||||
|
- Price breakdown styles
|
||||||
|
- Calendar grid and cell styles
|
||||||
|
- Legend and filter styles
|
||||||
|
- Responsive design for calendar
|
||||||
|
- Updated version to 0.3.0
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Booking conflicts use overlap detection: `A.check_in < B.check_out AND A.check_out > B.check_in`
|
||||||
|
- Excluding cancelled bookings from conflict checks allows rebooking same dates
|
||||||
|
- Guest info stored in booking meta (Phase 4 will add separate Guest CPT)
|
||||||
|
- AJAX availability check returns price calculation for immediate feedback
|
||||||
|
- Calendar displays bookings color-coded by status for quick visual overview
|
||||||
|
- HTML email templates with inline CSS for better email client compatibility
|
||||||
|
- Status transitions can trigger different email notifications via hooks
|
||||||
|
|||||||
90
PLAN.md
90
PLAN.md
@@ -17,72 +17,72 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Basic CSS and JS assets
|
- [x] Basic CSS and JS assets
|
||||||
- [x] Documentation (README, PLAN, CLAUDE)
|
- [x] Documentation (README, PLAN, CLAUDE)
|
||||||
|
|
||||||
### v0.1.0 - Core Data Structures
|
### v0.1.0 - Core Data Structures (Current)
|
||||||
|
|
||||||
- [ ] Custom Post Type: Buildings
|
- [x] Custom Post Type: Buildings
|
||||||
- Meta fields: address, contact, description, images
|
- Meta fields: address, contact, description, images
|
||||||
- Admin columns and filtering
|
- Admin columns and filtering
|
||||||
- Gutenberg block for display
|
- Gutenberg block for display (planned for Phase 6)
|
||||||
|
|
||||||
- [ ] Custom Post Type: Rooms
|
- [x] Custom Post Type: Rooms
|
||||||
- Meta fields: building reference, capacity, amenities, images
|
- Meta fields: building reference, capacity, amenities, images
|
||||||
- Relationship to Buildings (parent)
|
- Relationship to Buildings (parent)
|
||||||
- Admin columns with building filter
|
- Admin columns with building filter
|
||||||
- Gutenberg block for display
|
- Gutenberg block for display (planned for Phase 6)
|
||||||
|
|
||||||
- [ ] Custom Taxonomy: Room Types
|
- [x] Custom Taxonomy: Room Types
|
||||||
- Standard, Suite, Family, Accessible, etc.
|
- Standard, Suite, Family, Accessible, etc.
|
||||||
- Hierarchical structure
|
- Hierarchical structure
|
||||||
|
|
||||||
- [ ] Custom Taxonomy: Amenities
|
- [x] Custom Taxonomy: Amenities
|
||||||
- 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) - Complete
|
||||||
|
|
||||||
### Custom Post Type: Bookings
|
### Custom Post Type: Bookings
|
||||||
|
|
||||||
- [ ] Guest reference
|
- [x] Guest reference
|
||||||
- [ ] Room reference
|
- [x] Room reference
|
||||||
- [ ] Check-in/check-out dates
|
- [x] Check-in/check-out dates
|
||||||
- [ ] Status (pending, confirmed, checked-in, checked-out, cancelled)
|
- [x] Status (pending, confirmed, checked-in, checked-out, cancelled)
|
||||||
- [ ] Price calculation and storage
|
- [x] Price calculation and storage
|
||||||
- [ ] Notes field
|
- [x] Notes field
|
||||||
|
|
||||||
### Calendar Integration
|
### Calendar Integration
|
||||||
|
|
||||||
- [ ] Availability calendar per room
|
- [x] Availability calendar per room
|
||||||
- [ ] Availability calendar per building
|
- [x] Availability calendar per building
|
||||||
- [ ] Date range picker for bookings
|
- [x] Date range picker for bookings
|
||||||
- [ ] Conflict detection
|
- [x] Conflict detection
|
||||||
|
|
||||||
### Booking Workflow
|
### Booking Workflow
|
||||||
|
|
||||||
- [ ] Booking creation (admin)
|
- [x] Booking creation (admin)
|
||||||
- [ ] Status transitions
|
- [x] Status transitions
|
||||||
- [ ] Email notifications
|
- [x] Email notifications
|
||||||
- [ ] Booking confirmation
|
- [x] Booking confirmation
|
||||||
|
|
||||||
## Phase 4: Guest Management (v0.4.0)
|
## Phase 4: Guest Management (v0.4.0)
|
||||||
|
|
||||||
@@ -285,15 +285,15 @@ The plugin will provide extensive hooks for customization:
|
|||||||
|
|
||||||
## Version Milestones
|
## Version Milestones
|
||||||
|
|
||||||
| Version | Focus | Target |
|
| Version | Focus | Target |
|
||||||
|---------|-------|--------|
|
| ------- | --------------- | -------- |
|
||||||
| 0.0.1 | Initial setup | Complete |
|
| 0.0.1 | Initial setup | Complete |
|
||||||
| 0.1.0 | Data structures | TBD |
|
| 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 | Complete |
|
||||||
| 0.4.0 | Guests | TBD |
|
| 0.4.0 | Guests | TBD |
|
||||||
| 0.5.0 | Services | TBD |
|
| 0.5.0 | Services | TBD |
|
||||||
| 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 |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
@@ -79,3 +79,612 @@
|
|||||||
.submit .button {
|
.submit .button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Room Gallery */
|
||||||
|
.bnb-gallery-container {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-gallery-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-gallery-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-gallery-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-gallery-image .bnb-remove-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-gallery-image:hover .bnb-remove-image {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-gallery-image .bnb-remove-image:hover {
|
||||||
|
background: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badge */
|
||||||
|
.bnb-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Room Details Meta Box */
|
||||||
|
#bnb_room_details .form-table td label {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Building Details Meta Box */
|
||||||
|
#bnb_building_details p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bnb_building_details label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bnb_building_details input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Columns */
|
||||||
|
.column-city,
|
||||||
|
.column-country,
|
||||||
|
.column-room_number,
|
||||||
|
.column-capacity,
|
||||||
|
.column-status {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-building {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-rooms {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashicons in columns */
|
||||||
|
.column-capacity .dashicons {
|
||||||
|
color: #646970;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Booking System Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Booking Info Display */
|
||||||
|
.bnb-booking-info {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info.bnb-checking {
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info.bnb-available {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info.bnb-not-available {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Price Display */
|
||||||
|
.bnb-booking-price {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-price strong {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Breakdown */
|
||||||
|
.bnb-booking-breakdown,
|
||||||
|
.bnb-breakdown-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-breakdown-list li {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px dotted #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-breakdown-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Override Indicator */
|
||||||
|
.bnb-price-override {
|
||||||
|
color: #dba617;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Preview */
|
||||||
|
.bnb-status-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-timestamp {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Required Field Indicator */
|
||||||
|
.required {
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Admin Columns */
|
||||||
|
.column-room,
|
||||||
|
.column-guest,
|
||||||
|
.column-dates,
|
||||||
|
.column-nights,
|
||||||
|
.column-price {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-room small,
|
||||||
|
.column-guest small,
|
||||||
|
.column-dates small {
|
||||||
|
display: block;
|
||||||
|
color: #646970;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Calendar Page Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Calendar Container */
|
||||||
|
.bnb-calendar-container {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Header */
|
||||||
|
.bnb-calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Filters */
|
||||||
|
.bnb-calendar-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-filters label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-filters select {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Grid */
|
||||||
|
.bnb-calendar-grid {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table th,
|
||||||
|
.bnb-calendar-table td {
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table th {
|
||||||
|
background: #f6f7f7;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table th.room-header {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Day Cell */
|
||||||
|
.bnb-calendar-day {
|
||||||
|
height: 35px;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.past {
|
||||||
|
background: #f0f0f1;
|
||||||
|
color: #a7aaad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.today {
|
||||||
|
background: #f0f6fc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.today::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: #2271b1;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.available {
|
||||||
|
background: #d4edda;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked {
|
||||||
|
background: #d63638;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked.booking-hover {
|
||||||
|
background: #a02424;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked-start {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked-end {
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Status Colors in Calendar */
|
||||||
|
.bnb-calendar-day.status-pending {
|
||||||
|
background: #dba617;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.status-confirmed {
|
||||||
|
background: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.status-checked_in {
|
||||||
|
background: #72aee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Room Row in Multi-Room Calendar */
|
||||||
|
.bnb-calendar-room {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-room small {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Legend */
|
||||||
|
.bnb-calendar-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid #c3c4c7;
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.available {
|
||||||
|
background: #d4edda;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.booked {
|
||||||
|
background: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.pending {
|
||||||
|
background: #dba617;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.confirmed {
|
||||||
|
background: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.checked-in {
|
||||||
|
background: #72aee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip for booking details */
|
||||||
|
.bnb-calendar-day[title] {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Rooms Message */
|
||||||
|
.bnb-no-rooms {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-rooms .dashicons {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Single Room View */
|
||||||
|
.bnb-calendar-single-room .bnb-calendar-day {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-single-room .bnb-calendar-day.booked .guest-name {
|
||||||
|
font-size: 10px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.bnb-calendar-filters {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-filters select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -91,9 +91,501 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize room gallery functionality.
|
||||||
|
*/
|
||||||
|
function initRoomGallery() {
|
||||||
|
var $container = $('#bnb-room-gallery');
|
||||||
|
var $addButton = $('#bnb-add-gallery-images');
|
||||||
|
var $input = $('#bnb_room_gallery');
|
||||||
|
var $imagesContainer = $container.find('.bnb-gallery-images');
|
||||||
|
|
||||||
|
if (!$addButton.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media frame for selecting images.
|
||||||
|
var mediaFrame;
|
||||||
|
|
||||||
|
// Add images button click.
|
||||||
|
$addButton.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// If frame exists, reopen it.
|
||||||
|
if (mediaFrame) {
|
||||||
|
mediaFrame.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create media frame.
|
||||||
|
mediaFrame = wp.media({
|
||||||
|
title: wpBnbAdmin.i18n.selectImages,
|
||||||
|
button: {
|
||||||
|
text: wpBnbAdmin.i18n.addToGallery
|
||||||
|
},
|
||||||
|
multiple: true,
|
||||||
|
library: {
|
||||||
|
type: 'image'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle selection.
|
||||||
|
mediaFrame.on('select', function() {
|
||||||
|
var selection = mediaFrame.state().get('selection');
|
||||||
|
selection.each(function(attachment) {
|
||||||
|
var data = attachment.toJSON();
|
||||||
|
var thumbnail = data.sizes.thumbnail ? data.sizes.thumbnail.url : data.url;
|
||||||
|
|
||||||
|
// Check if already in gallery.
|
||||||
|
if ($imagesContainer.find('[data-id="' + data.id + '"]').length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add image to gallery.
|
||||||
|
var $image = $('<div class="bnb-gallery-image" data-id="' + data.id + '">' +
|
||||||
|
'<img src="' + thumbnail + '" alt="">' +
|
||||||
|
'<button type="button" class="bnb-remove-image">×</button>' +
|
||||||
|
'</div>');
|
||||||
|
$imagesContainer.append($image);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateGalleryInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaFrame.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove image button click.
|
||||||
|
$imagesContainer.on('click', '.bnb-remove-image', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).closest('.bnb-gallery-image').remove();
|
||||||
|
updateGalleryInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make gallery sortable.
|
||||||
|
$imagesContainer.sortable({
|
||||||
|
items: '.bnb-gallery-image',
|
||||||
|
cursor: 'move',
|
||||||
|
update: function() {
|
||||||
|
updateGalleryInput();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the hidden input with gallery image IDs.
|
||||||
|
*/
|
||||||
|
function updateGalleryInput() {
|
||||||
|
var ids = [];
|
||||||
|
$imagesContainer.find('.bnb-gallery-image').each(function() {
|
||||||
|
ids.push($(this).data('id'));
|
||||||
|
});
|
||||||
|
$input.val(ids.join(','));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 booking form functionality.
|
||||||
|
*/
|
||||||
|
function initBookingForm() {
|
||||||
|
var $roomSelect = $('#bnb_booking_room_id');
|
||||||
|
var $checkInInput = $('#bnb_booking_check_in');
|
||||||
|
var $checkOutInput = $('#bnb_booking_check_out');
|
||||||
|
var $nightsDisplay = $('#bnb-booking-nights-display');
|
||||||
|
var $availabilityDisplay = $('#bnb-booking-availability-display');
|
||||||
|
var $priceDisplay = $('#bnb-booking-price-display');
|
||||||
|
var $calculatedPriceInput = $('#bnb_booking_calculated_price');
|
||||||
|
var $priceBreakdownInput = $('#bnb_booking_price_breakdown');
|
||||||
|
var $breakdownDisplay = $('#bnb-booking-breakdown-display');
|
||||||
|
var $recalculateBtn = $('#bnb-recalculate-price');
|
||||||
|
var $statusSelect = $('#bnb_booking_status');
|
||||||
|
var $statusPreview = $('#bnb-status-preview .bnb-status-badge');
|
||||||
|
|
||||||
|
// Check if we're on a booking edit page.
|
||||||
|
if (!$roomSelect.length || !$checkInInput.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current booking ID if editing.
|
||||||
|
var bookingId = null;
|
||||||
|
var $postId = $('input[name="post_ID"]');
|
||||||
|
if ($postId.length) {
|
||||||
|
bookingId = parseInt($postId.val(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce timer for availability check.
|
||||||
|
var availabilityTimer = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update nights display based on selected dates.
|
||||||
|
*/
|
||||||
|
function updateNightsDisplay() {
|
||||||
|
var checkIn = $checkInInput.val();
|
||||||
|
var checkOut = $checkOutInput.val();
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
var startDate = new Date(checkIn);
|
||||||
|
var endDate = new Date(checkOut);
|
||||||
|
var nights = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (nights > 0) {
|
||||||
|
var nightText = nights === 1 ? wpBnbAdmin.i18n.night : wpBnbAdmin.i18n.nights;
|
||||||
|
$nightsDisplay.text(nights + ' ' + nightText);
|
||||||
|
} else {
|
||||||
|
$nightsDisplay.text(wpBnbAdmin.i18n.error || 'Invalid date range');
|
||||||
|
$nightsDisplay.css('color', '#d63638');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$nightsDisplay.text(wpBnbAdmin.i18n.selectRoomAndDates);
|
||||||
|
$nightsDisplay.css('color', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check availability via AJAX.
|
||||||
|
*/
|
||||||
|
function checkAvailability() {
|
||||||
|
var roomId = $roomSelect.val();
|
||||||
|
var checkIn = $checkInInput.val();
|
||||||
|
var checkOut = $checkOutInput.val();
|
||||||
|
|
||||||
|
if (!roomId || !checkIn || !checkOut) {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.selectRoomAndDates)
|
||||||
|
.removeClass('bnb-available bnb-not-available')
|
||||||
|
.addClass('bnb-checking');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
var startDate = new Date(checkIn);
|
||||||
|
var endDate = new Date(checkOut);
|
||||||
|
if (endDate <= startDate) {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.error || 'Check-out must be after check-in')
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show checking status.
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.checking)
|
||||||
|
.removeClass('bnb-available bnb-not-available')
|
||||||
|
.addClass('bnb-checking');
|
||||||
|
|
||||||
|
// Make AJAX request.
|
||||||
|
$.ajax({
|
||||||
|
url: wpBnbAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_check_availability',
|
||||||
|
nonce: wpBnbAdmin.nonce,
|
||||||
|
room_id: roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut,
|
||||||
|
exclude_booking: bookingId
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
|
||||||
|
if (data.available) {
|
||||||
|
$availabilityDisplay
|
||||||
|
.html('<span class="dashicons dashicons-yes-alt"></span> ' + wpBnbAdmin.i18n.available)
|
||||||
|
.removeClass('bnb-not-available bnb-checking')
|
||||||
|
.addClass('bnb-available');
|
||||||
|
|
||||||
|
// Update price display.
|
||||||
|
if (data.price_formatted) {
|
||||||
|
$priceDisplay.html('<strong>' + data.price_formatted + '</strong>');
|
||||||
|
$calculatedPriceInput.val(data.price);
|
||||||
|
|
||||||
|
if (data.breakdown) {
|
||||||
|
$priceBreakdownInput.val(JSON.stringify(data.breakdown));
|
||||||
|
updateBreakdownDisplay(data.breakdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var conflictText = wpBnbAdmin.i18n.notAvailable;
|
||||||
|
if (data.conflicts && data.conflicts.length > 0) {
|
||||||
|
conflictText += ' (' + data.conflicts[0].reference + ')';
|
||||||
|
}
|
||||||
|
$availabilityDisplay
|
||||||
|
.html('<span class="dashicons dashicons-dismiss"></span> ' + conflictText)
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update nights display with response data.
|
||||||
|
if (data.nights) {
|
||||||
|
var nightText = data.nights === 1 ? wpBnbAdmin.i18n.night : wpBnbAdmin.i18n.nights;
|
||||||
|
$nightsDisplay.text(data.nights + ' ' + nightText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(response.data.message || wpBnbAdmin.i18n.error)
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.error)
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update breakdown display with formatted data.
|
||||||
|
*
|
||||||
|
* @param {Object} breakdown Price breakdown data.
|
||||||
|
*/
|
||||||
|
function updateBreakdownDisplay(breakdown) {
|
||||||
|
if (!$breakdownDisplay.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<ul class="bnb-breakdown-list">';
|
||||||
|
|
||||||
|
if (breakdown.tier) {
|
||||||
|
html += '<li><strong>Pricing Tier:</strong> ' + breakdown.tier.replace('_', ' ') + '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakdown.nights && Array.isArray(breakdown.nights)) {
|
||||||
|
html += '<li><strong>Nights:</strong> ' + breakdown.nights.length + '</li>';
|
||||||
|
if (breakdown.nightly_rate) {
|
||||||
|
html += '<li><strong>Nightly Rate:</strong> ' + formatPrice(breakdown.nightly_rate) + '</li>';
|
||||||
|
}
|
||||||
|
} else if (breakdown.weeks) {
|
||||||
|
html += '<li><strong>Weeks:</strong> ' + breakdown.weeks + '</li>';
|
||||||
|
if (breakdown.weekly_rate) {
|
||||||
|
html += '<li><strong>Weekly Rate:</strong> ' + formatPrice(breakdown.weekly_rate) + '</li>';
|
||||||
|
}
|
||||||
|
} else if (breakdown.months) {
|
||||||
|
html += '<li><strong>Months:</strong> ' + breakdown.months + '</li>';
|
||||||
|
if (breakdown.monthly_rate) {
|
||||||
|
html += '<li><strong>Monthly Rate:</strong> ' + formatPrice(breakdown.monthly_rate) + '</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakdown.total) {
|
||||||
|
html += '<li><strong>Total:</strong> ' + formatPrice(breakdown.total) + '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</ul>';
|
||||||
|
|
||||||
|
$breakdownDisplay.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price for display.
|
||||||
|
*
|
||||||
|
* @param {number} price Price value.
|
||||||
|
* @return {string} Formatted price.
|
||||||
|
*/
|
||||||
|
function formatPrice(price) {
|
||||||
|
// Simple formatting - server-side Calculator::formatPrice is more complete.
|
||||||
|
return parseFloat(price).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced availability check.
|
||||||
|
*/
|
||||||
|
function debouncedAvailabilityCheck() {
|
||||||
|
updateNightsDisplay();
|
||||||
|
|
||||||
|
// Clear existing timer.
|
||||||
|
if (availabilityTimer) {
|
||||||
|
clearTimeout(availabilityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timer to check availability after 500ms.
|
||||||
|
availabilityTimer = setTimeout(checkAvailability, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind change events to trigger availability check.
|
||||||
|
$roomSelect.on('change', debouncedAvailabilityCheck);
|
||||||
|
$checkInInput.on('change', debouncedAvailabilityCheck);
|
||||||
|
$checkOutInput.on('change', debouncedAvailabilityCheck);
|
||||||
|
|
||||||
|
// Recalculate price button.
|
||||||
|
if ($recalculateBtn.length) {
|
||||||
|
$recalculateBtn.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
checkAvailability();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status preview update.
|
||||||
|
if ($statusSelect.length && $statusPreview.length) {
|
||||||
|
$statusSelect.on('change', function() {
|
||||||
|
var $selected = $(this).find('option:selected');
|
||||||
|
var color = $selected.data('color') || '#ccc';
|
||||||
|
var text = $selected.text();
|
||||||
|
|
||||||
|
$statusPreview
|
||||||
|
.css('background-color', color)
|
||||||
|
.text(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set min date for check-in to today.
|
||||||
|
var today = new Date().toISOString().split('T')[0];
|
||||||
|
$checkInInput.attr('min', today);
|
||||||
|
|
||||||
|
// Update check-out min date when check-in changes.
|
||||||
|
$checkInInput.on('change', function() {
|
||||||
|
var checkIn = $(this).val();
|
||||||
|
if (checkIn) {
|
||||||
|
var nextDay = new Date(checkIn);
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
|
var minCheckOut = nextDay.toISOString().split('T')[0];
|
||||||
|
$checkOutInput.attr('min', minCheckOut);
|
||||||
|
|
||||||
|
// If check-out is before new min, update it.
|
||||||
|
if ($checkOutInput.val() && $checkOutInput.val() <= checkIn) {
|
||||||
|
$checkOutInput.val(minCheckOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize calendar page functionality.
|
||||||
|
*/
|
||||||
|
function initCalendarPage() {
|
||||||
|
var $calendar = $('.bnb-calendar-grid');
|
||||||
|
|
||||||
|
if (!$calendar.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hover effect for booking cells.
|
||||||
|
$calendar.on('mouseenter', '.bnb-calendar-day.booked', function() {
|
||||||
|
var bookingId = $(this).data('booking-id');
|
||||||
|
if (bookingId) {
|
||||||
|
$calendar.find('.bnb-calendar-day[data-booking-id="' + bookingId + '"]')
|
||||||
|
.addClass('booking-hover');
|
||||||
|
}
|
||||||
|
}).on('mouseleave', '.bnb-calendar-day.booked', function() {
|
||||||
|
$calendar.find('.bnb-calendar-day.booking-hover')
|
||||||
|
.removeClass('booking-hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click to edit booking.
|
||||||
|
$calendar.on('click', '.bnb-calendar-day.booked', function() {
|
||||||
|
var bookingId = $(this).data('booking-id');
|
||||||
|
if (bookingId) {
|
||||||
|
window.location.href = wpBnbAdmin.ajaxUrl.replace('admin-ajax.php', 'post.php?post=' + bookingId + '&action=edit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
|
initRoomGallery();
|
||||||
|
initPricingSettings();
|
||||||
|
initSeasonForm();
|
||||||
|
initPricingMetaBox();
|
||||||
|
initBookingForm();
|
||||||
|
initCalendarPage();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
359
src/Admin/Calendar.php
Normal file
359
src/Admin/Calendar.php
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Calendar admin page.
|
||||||
|
*
|
||||||
|
* Displays availability calendar for rooms and buildings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Admin;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar admin page class.
|
||||||
|
*/
|
||||||
|
final class Calendar {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the calendar page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'admin_menu', array( self::class, 'register_menu' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the admin menu item.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_menu(): void {
|
||||||
|
add_submenu_page(
|
||||||
|
'wp-bnb',
|
||||||
|
__( 'Calendar', 'wp-bnb' ),
|
||||||
|
__( 'Calendar', 'wp-bnb' ),
|
||||||
|
'edit_posts',
|
||||||
|
'wp-bnb-calendar',
|
||||||
|
array( self::class, 'render_page' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the calendar page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_page(): void {
|
||||||
|
// Get filter parameters.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$building_id = isset( $_GET['building_id'] ) ? absint( $_GET['building_id'] ) : 0;
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$room_id = isset( $_GET['room_id'] ) ? absint( $_GET['room_id'] ) : 0;
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$year = isset( $_GET['year'] ) ? absint( $_GET['year'] ) : (int) gmdate( 'Y' );
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$month = isset( $_GET['month'] ) ? absint( $_GET['month'] ) : (int) gmdate( 'n' );
|
||||||
|
|
||||||
|
// Validate month.
|
||||||
|
if ( $month < 1 || $month > 12 ) {
|
||||||
|
$month = (int) gmdate( 'n' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get buildings and rooms for filters.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If room is selected, get its building.
|
||||||
|
if ( $room_id && ! $building_id ) {
|
||||||
|
$building = Room::get_building( $room_id );
|
||||||
|
if ( $building ) {
|
||||||
|
$building_id = $building->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rooms to display.
|
||||||
|
$display_rooms = array();
|
||||||
|
if ( $room_id ) {
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( $room ) {
|
||||||
|
$display_rooms[] = $room;
|
||||||
|
}
|
||||||
|
} elseif ( $building_id ) {
|
||||||
|
$display_rooms = Room::get_rooms_for_building( $building_id );
|
||||||
|
} else {
|
||||||
|
$display_rooms = $rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate navigation dates.
|
||||||
|
$prev_month = $month === 1 ? 12 : $month - 1;
|
||||||
|
$prev_year = $month === 1 ? $year - 1 : $year;
|
||||||
|
$next_month = $month === 12 ? 1 : $month + 1;
|
||||||
|
$next_year = $month === 12 ? $year + 1 : $year;
|
||||||
|
|
||||||
|
$month_name = gmdate( 'F', mktime( 0, 0, 0, $month, 1, $year ) );
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Availability Calendar', 'wp-bnb' ); ?></h1>
|
||||||
|
|
||||||
|
<div class="bnb-calendar-container">
|
||||||
|
<!-- Calendar Header -->
|
||||||
|
<div class="bnb-calendar-header">
|
||||||
|
<div class="bnb-calendar-nav">
|
||||||
|
<a href="<?php echo esc_url( self::get_calendar_url( $prev_year, $prev_month, $building_id, $room_id ) ); ?>"
|
||||||
|
class="button">
|
||||||
|
« <?php esc_html_e( 'Previous', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( self::get_calendar_url( (int) gmdate( 'Y' ), (int) gmdate( 'n' ), $building_id, $room_id ) ); ?>"
|
||||||
|
class="button">
|
||||||
|
<?php esc_html_e( 'Today', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( self::get_calendar_url( $next_year, $next_month, $building_id, $room_id ) ); ?>"
|
||||||
|
class="button">
|
||||||
|
<?php esc_html_e( 'Next', 'wp-bnb' ); ?> »
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h2><?php echo esc_html( $month_name . ' ' . $year ); ?></h2>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="bnb-calendar-filters">
|
||||||
|
<form method="get" action="">
|
||||||
|
<input type="hidden" name="page" value="wp-bnb-calendar">
|
||||||
|
<input type="hidden" name="year" value="<?php echo esc_attr( $year ); ?>">
|
||||||
|
<input type="hidden" name="month" value="<?php echo esc_attr( $month ); ?>">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php esc_html_e( 'Building:', 'wp-bnb' ); ?>
|
||||||
|
<select name="building_id" onchange="this.form.submit()">
|
||||||
|
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $building_id, $building->ID ); ?>>
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php esc_html_e( 'Room:', 'wp-bnb' ); ?>
|
||||||
|
<select name="room_id" onchange="this.form.submit()">
|
||||||
|
<option value=""><?php esc_html_e( 'All Rooms', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $rooms as $room ) : ?>
|
||||||
|
<?php
|
||||||
|
$room_building = Room::get_building( $room->ID );
|
||||||
|
$room_label = $room->post_title;
|
||||||
|
if ( $room_building ) {
|
||||||
|
$room_label .= ' (' . $room_building->post_title . ')';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
||||||
|
<?php echo esc_html( $room_label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar Grid -->
|
||||||
|
<div class="bnb-calendar-grid">
|
||||||
|
<?php if ( empty( $display_rooms ) ) : ?>
|
||||||
|
<div class="bnb-no-rooms">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<p><?php esc_html_e( 'No rooms found. Please add rooms first.', 'wp-bnb' ); ?></p>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Room::POST_TYPE ) ); ?>" class="button button-primary">
|
||||||
|
<?php esc_html_e( 'Add Room', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php self::render_calendar_table( $display_rooms, $year, $month, (bool) $room_id ); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="bnb-calendar-legend">
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color available"></span>
|
||||||
|
<?php esc_html_e( 'Available', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color pending"></span>
|
||||||
|
<?php esc_html_e( 'Pending', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color confirmed"></span>
|
||||||
|
<?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color checked-in"></span>
|
||||||
|
<?php esc_html_e( 'Checked In', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the calendar table.
|
||||||
|
*
|
||||||
|
* @param array $rooms Rooms to display.
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month.
|
||||||
|
* @param bool $single_room Whether showing single room (more detail).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_calendar_table( array $rooms, int $year, int $month, bool $single_room = false ): void {
|
||||||
|
$days_in_month = (int) gmdate( 't', mktime( 0, 0, 0, $month, 1, $year ) );
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
$class = $single_room ? 'bnb-calendar-table bnb-calendar-single-room' : 'bnb-calendar-table';
|
||||||
|
?>
|
||||||
|
<table class="<?php echo esc_attr( $class ); ?>">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="room-header"><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
|
||||||
|
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||||
|
$day_of_week = gmdate( 'D', strtotime( $date_str ) );
|
||||||
|
$is_weekend = in_array( gmdate( 'N', strtotime( $date_str ) ), array( 6, 7 ), true );
|
||||||
|
?>
|
||||||
|
<th class="<?php echo $is_weekend ? 'weekend' : ''; ?>">
|
||||||
|
<?php echo esc_html( $day ); ?><br>
|
||||||
|
<small><?php echo esc_html( $day_of_week ); ?></small>
|
||||||
|
</th>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $rooms as $room ) : ?>
|
||||||
|
<?php
|
||||||
|
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||||
|
$booked_dates = Availability::get_booked_dates( $room->ID, $year, $month );
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="bnb-calendar-room">
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $room->ID ) ); ?>">
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( $room_number ) : ?>
|
||||||
|
<br><small>#<?php echo esc_html( $room_number ); ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||||
|
$is_past = $date_str < $today;
|
||||||
|
$is_today = $date_str === $today;
|
||||||
|
$is_booked = isset( $booked_dates[ $date_str ] );
|
||||||
|
|
||||||
|
$classes = array( 'bnb-calendar-day' );
|
||||||
|
$title = '';
|
||||||
|
$booking_id = 0;
|
||||||
|
|
||||||
|
if ( $is_past ) {
|
||||||
|
$classes[] = 'past';
|
||||||
|
}
|
||||||
|
if ( $is_today ) {
|
||||||
|
$classes[] = 'today';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $is_booked ) {
|
||||||
|
$booking_data = $booked_dates[ $date_str ];
|
||||||
|
$booking_id = $booking_data['booking_id'];
|
||||||
|
$classes[] = 'booked';
|
||||||
|
$classes[] = 'status-' . $booking_data['status'];
|
||||||
|
|
||||||
|
if ( $booking_data['is_start'] ) {
|
||||||
|
$classes[] = 'booked-start';
|
||||||
|
}
|
||||||
|
if ( $booking_data['is_end'] ) {
|
||||||
|
$classes[] = 'booked-end';
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = sprintf(
|
||||||
|
/* translators: 1: Booking reference, 2: Guest name, 3: Check-in date, 4: Check-out date */
|
||||||
|
__( '%1$s - %2$s (%3$s to %4$s)', 'wp-bnb' ),
|
||||||
|
$booking_data['reference'],
|
||||||
|
$booking_data['guest'],
|
||||||
|
wp_date( get_option( 'date_format' ), strtotime( $booking_data['check_in'] ) ),
|
||||||
|
wp_date( get_option( 'date_format' ), strtotime( $booking_data['check_out'] ) )
|
||||||
|
);
|
||||||
|
} elseif ( ! $is_past ) {
|
||||||
|
$classes[] = 'available';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<td class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
|
||||||
|
<?php if ( $booking_id ) : ?>
|
||||||
|
data-booking-id="<?php echo esc_attr( $booking_id ); ?>"
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $title ) : ?>
|
||||||
|
title="<?php echo esc_attr( $title ); ?>"
|
||||||
|
<?php endif; ?>>
|
||||||
|
<?php if ( $single_room && $is_booked && $booking_data['is_start'] ) : ?>
|
||||||
|
<span class="guest-name"><?php echo esc_html( $booking_data['guest'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar URL with parameters.
|
||||||
|
*
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month.
|
||||||
|
* @param int $building_id Building ID (optional).
|
||||||
|
* @param int $room_id Room ID (optional).
|
||||||
|
* @return string URL.
|
||||||
|
*/
|
||||||
|
private static function get_calendar_url( int $year, int $month, int $building_id = 0, int $room_id = 0 ): string {
|
||||||
|
$args = array(
|
||||||
|
'page' => 'wp-bnb-calendar',
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $building_id ) {
|
||||||
|
$args['building_id'] = $building_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $room_id ) {
|
||||||
|
$args['room_id'] = $room_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return add_query_arg( $args, admin_url( 'admin.php' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
444
src/Booking/Availability.php
Normal file
444
src/Booking/Availability.php
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Availability checker.
|
||||||
|
*
|
||||||
|
* Handles availability checks and calendar data for rooms and bookings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Booking
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Booking;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability class.
|
||||||
|
*/
|
||||||
|
final class Availability {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a room is available for a date range.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int|null $exclude_booking Booking ID to exclude (for editing).
|
||||||
|
* @return bool True if available, false if conflicts exist.
|
||||||
|
*/
|
||||||
|
public static function is_available( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): bool {
|
||||||
|
return ! Booking::has_conflict( $room_id, $check_in, $check_out, $exclude_booking );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all booked dates for a room in a specific month.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $year Year (e.g., 2024).
|
||||||
|
* @param int $month Month (1-12).
|
||||||
|
* @return array<string, array> Array of dates (Y-m-d) with booking info.
|
||||||
|
*/
|
||||||
|
public static function get_booked_dates( int $room_id, int $year, int $month ): array {
|
||||||
|
$month_start = sprintf( '%04d-%02d-01', $year, $month );
|
||||||
|
$month_end = gmdate( 'Y-m-t', strtotime( $month_start ) );
|
||||||
|
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'cancelled',
|
||||||
|
'compare' => '!=',
|
||||||
|
),
|
||||||
|
// Booking overlaps with month.
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $month_end,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $month_start,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$booked_dates = array();
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
|
$guest = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
|
||||||
|
// Iterate through each night of the booking.
|
||||||
|
$current = new \DateTimeImmutable( $check_in );
|
||||||
|
$end = new \DateTimeImmutable( $check_out );
|
||||||
|
|
||||||
|
while ( $current < $end ) {
|
||||||
|
$date_str = $current->format( 'Y-m-d' );
|
||||||
|
|
||||||
|
// Only include dates within the requested month.
|
||||||
|
if ( $current->format( 'Y-m' ) === sprintf( '%04d-%02d', $year, $month ) ) {
|
||||||
|
$booked_dates[ $date_str ] = array(
|
||||||
|
'booking_id' => $booking->ID,
|
||||||
|
'reference' => $booking->post_title,
|
||||||
|
'guest' => $guest,
|
||||||
|
'status' => $status,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'is_start' => $date_str === $check_in,
|
||||||
|
'is_end' => $current->modify( '+1 day' )->format( 'Y-m-d' ) === $check_out,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = $current->modify( '+1 day' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $booked_dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar data for a room for a specific month.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month (1-12).
|
||||||
|
* @return array Calendar data including days and bookings.
|
||||||
|
*/
|
||||||
|
public static function get_calendar_data( int $room_id, int $year, int $month ): array {
|
||||||
|
$month_start = new \DateTimeImmutable( sprintf( '%04d-%02d-01', $year, $month ) );
|
||||||
|
$days_in_month = (int) $month_start->format( 't' );
|
||||||
|
$first_day_of_week = (int) $month_start->format( 'w' ); // 0 = Sunday.
|
||||||
|
|
||||||
|
$booked_dates = self::get_booked_dates( $room_id, $year, $month );
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
$days = array();
|
||||||
|
for ( $day = 1; $day <= $days_in_month; $day++ ) {
|
||||||
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||||
|
$is_booked = isset( $booked_dates[ $date_str ] );
|
||||||
|
|
||||||
|
$days[ $day ] = array(
|
||||||
|
'date' => $date_str,
|
||||||
|
'day' => $day,
|
||||||
|
'is_booked' => $is_booked,
|
||||||
|
'is_past' => $date_str < $today,
|
||||||
|
'is_today' => $date_str === $today,
|
||||||
|
'booking' => $is_booked ? $booked_dates[ $date_str ] : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'month_name' => $month_start->format( 'F' ),
|
||||||
|
'days_in_month' => $days_in_month,
|
||||||
|
'first_day_of_week' => $first_day_of_week,
|
||||||
|
'days' => $days,
|
||||||
|
'prev_month' => array(
|
||||||
|
'year' => $month === 1 ? $year - 1 : $year,
|
||||||
|
'month' => $month === 1 ? 12 : $month - 1,
|
||||||
|
),
|
||||||
|
'next_month' => array(
|
||||||
|
'year' => $month === 12 ? $year + 1 : $year,
|
||||||
|
'month' => $month === 12 ? 1 : $month + 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get availability summary for a building (all rooms).
|
||||||
|
*
|
||||||
|
* @param int $building_id Building post ID.
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month (1-12).
|
||||||
|
* @return array Availability data for all rooms in the building.
|
||||||
|
*/
|
||||||
|
public static function get_building_availability( int $building_id, int $year, int $month ): array {
|
||||||
|
$rooms = Room::get_rooms_for_building( $building_id );
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||||
|
$data[ $room->ID ] = array(
|
||||||
|
'room_id' => $room->ID,
|
||||||
|
'room_name' => $room->post_title,
|
||||||
|
'room_number' => $room_number,
|
||||||
|
'calendar' => self::get_calendar_data( $room->ID, $year, $month ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conflicts for a proposed booking.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int|null $exclude_booking Booking ID to exclude.
|
||||||
|
* @return array<\WP_Post> Array of conflicting booking posts.
|
||||||
|
*/
|
||||||
|
public static function get_conflicts( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): array {
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'cancelled',
|
||||||
|
'compare' => '!=',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $check_out,
|
||||||
|
'compare' => '<',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $check_in,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $exclude_booking ) {
|
||||||
|
$args['post__not_in'] = array( $exclude_booking );
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_posts( $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get availability check result with pricing.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int|null $exclude_booking Booking ID to exclude.
|
||||||
|
* @return array Result with availability, pricing, and conflicts.
|
||||||
|
*/
|
||||||
|
public static function check_availability_with_price( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): array {
|
||||||
|
$conflicts = self::get_conflicts( $room_id, $check_in, $check_out, $exclude_booking );
|
||||||
|
$available = empty( $conflicts );
|
||||||
|
|
||||||
|
$result = array(
|
||||||
|
'available' => $available,
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => Booking::calculate_nights( $check_in, $check_out ),
|
||||||
|
'conflicts' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $available ) {
|
||||||
|
foreach ( $conflicts as $conflict ) {
|
||||||
|
$result['conflicts'][] = array(
|
||||||
|
'booking_id' => $conflict->ID,
|
||||||
|
'reference' => $conflict->post_title,
|
||||||
|
'check_in' => get_post_meta( $conflict->ID, '_bnb_booking_check_in', true ),
|
||||||
|
'check_out' => get_post_meta( $conflict->ID, '_bnb_booking_check_out', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate price if available.
|
||||||
|
if ( $available ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
$breakdown = $calculator->getBreakdown();
|
||||||
|
|
||||||
|
$result['price'] = $price;
|
||||||
|
$result['price_formatted'] = Calculator::formatPrice( $price );
|
||||||
|
$result['breakdown'] = $breakdown;
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$result['price'] = null;
|
||||||
|
$result['price_formatted'] = null;
|
||||||
|
$result['price_error'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upcoming bookings for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $limit Maximum number of bookings to return.
|
||||||
|
* @return array<\WP_Post> Array of upcoming bookings.
|
||||||
|
*/
|
||||||
|
public static function get_upcoming_bookings( int $room_id, int $limit = 5 ): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $limit,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'pending', 'confirmed' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_check_in',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current bookings (guests currently checked in).
|
||||||
|
*
|
||||||
|
* @param int|null $room_id Optional room ID to filter by.
|
||||||
|
* @return array<\WP_Post> Array of current bookings.
|
||||||
|
*/
|
||||||
|
public static function get_current_bookings( ?int $room_id = null ): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
$meta_query = array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'checked_in',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $room_id ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => $meta_query,
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_check_out',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's check-ins.
|
||||||
|
*
|
||||||
|
* @return array<\WP_Post> Array of bookings with check-in today.
|
||||||
|
*/
|
||||||
|
public static function get_todays_checkins(): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'confirmed',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_guest_name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's check-outs.
|
||||||
|
*
|
||||||
|
* @return array<\WP_Post> Array of bookings with check-out today.
|
||||||
|
*/
|
||||||
|
public static function get_todays_checkouts(): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'checked_in',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $today,
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_guest_name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
587
src/Booking/EmailNotifier.php
Normal file
587
src/Booking/EmailNotifier.php
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Email notifier for bookings.
|
||||||
|
*
|
||||||
|
* Handles sending email notifications for booking events.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Booking
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Booking;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailNotifier class.
|
||||||
|
*/
|
||||||
|
final class EmailNotifier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the email notifier.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'wp_bnb_booking_status_changed', array( self::class, 'on_status_change' ), 10, 3 );
|
||||||
|
add_action( 'save_post_' . Booking::POST_TYPE, array( self::class, 'on_booking_created' ), 20, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle status change event.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @param string $old_status Previous status.
|
||||||
|
* @param string $new_status New status.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_status_change( int $booking_id, string $old_status, string $new_status ): void {
|
||||||
|
switch ( $new_status ) {
|
||||||
|
case 'confirmed':
|
||||||
|
self::send_guest_confirmation( $booking_id );
|
||||||
|
self::send_admin_confirmation( $booking_id );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancelled':
|
||||||
|
self::send_cancellation( $booking_id );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle booking created event.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_booking_created( int $post_id, \WP_Post $post ): void {
|
||||||
|
// Skip if autosave or revision.
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( wp_is_post_revision( $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a new booking (created in the last 30 seconds).
|
||||||
|
$created = get_post_time( 'U', true, $post_id );
|
||||||
|
if ( time() - $created > 30 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've already sent this notification.
|
||||||
|
$sent = get_post_meta( $post_id, '_bnb_booking_new_email_sent', true );
|
||||||
|
if ( $sent ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as sent before sending to prevent duplicates.
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_new_email_sent', '1' );
|
||||||
|
|
||||||
|
// Send admin notification for new booking.
|
||||||
|
self::send_admin_new_booking( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send new booking notification to admin.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
public static function send_admin_new_booking( int $booking_id ): bool {
|
||||||
|
$booking = get_post( $booking_id );
|
||||||
|
if ( ! $booking ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
$to = get_option( 'admin_email' );
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] New Booking: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = self::get_email_template( 'admin-new-booking', $data );
|
||||||
|
|
||||||
|
return self::send_email( $to, $subject, $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send confirmation email to guest.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
public static function send_guest_confirmation( int $booking_id ): bool {
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
|
||||||
|
if ( empty( $data['guest_email'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = self::get_email_template( 'booking-confirmed', $data );
|
||||||
|
|
||||||
|
return self::send_email( $data['guest_email'], $subject, $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send confirmation notification to admin.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
public static function send_admin_confirmation( int $booking_id ): bool {
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
$to = get_option( 'admin_email' );
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = self::get_email_template( 'admin-booking-confirmed', $data );
|
||||||
|
|
||||||
|
return self::send_email( $to, $subject, $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send cancellation email.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether emails were sent.
|
||||||
|
*/
|
||||||
|
public static function send_cancellation( int $booking_id ): bool {
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
$result = true;
|
||||||
|
|
||||||
|
// Send to admin.
|
||||||
|
$admin_subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$admin_message = self::get_email_template( 'admin-booking-cancelled', $data );
|
||||||
|
$result = self::send_email( get_option( 'admin_email' ), $admin_subject, $admin_message ) && $result;
|
||||||
|
|
||||||
|
// Send to guest if email exists.
|
||||||
|
if ( ! empty( $data['guest_email'] ) ) {
|
||||||
|
$guest_subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$guest_message = self::get_email_template( 'booking-cancelled', $data );
|
||||||
|
$result = self::send_email( $data['guest_email'], $guest_subject, $guest_message ) && $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get booking data for email templates.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return array Booking data.
|
||||||
|
*/
|
||||||
|
private static function get_booking_data( int $booking_id ): array {
|
||||||
|
$booking = get_post( $booking_id );
|
||||||
|
$room = Booking::get_room( $booking_id );
|
||||||
|
$building = Booking::get_building( $booking_id );
|
||||||
|
|
||||||
|
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
$price = get_post_meta( $booking_id, '_bnb_booking_calculated_price', true );
|
||||||
|
$adults = get_post_meta( $booking_id, '_bnb_booking_adults', true );
|
||||||
|
$children = get_post_meta( $booking_id, '_bnb_booking_children', true );
|
||||||
|
|
||||||
|
$nights = 0;
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$nights = Booking::calculate_nights( $check_in, $check_out );
|
||||||
|
}
|
||||||
|
|
||||||
|
$statuses = Booking::get_booking_statuses();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'booking_id' => $booking_id,
|
||||||
|
'booking_reference' => $booking ? $booking->post_title : '',
|
||||||
|
'guest_name' => get_post_meta( $booking_id, '_bnb_booking_guest_name', true ),
|
||||||
|
'guest_email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ),
|
||||||
|
'guest_phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ),
|
||||||
|
'guest_notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ),
|
||||||
|
'adults' => $adults ?: 1,
|
||||||
|
'children' => $children ?: 0,
|
||||||
|
'room_name' => $room ? $room->post_title : '',
|
||||||
|
'room_id' => $room ? $room->ID : 0,
|
||||||
|
'building_name' => $building ? $building->post_title : '',
|
||||||
|
'building_id' => $building ? $building->ID : 0,
|
||||||
|
'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '',
|
||||||
|
'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '',
|
||||||
|
'check_in_raw' => $check_in,
|
||||||
|
'check_out_raw' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '',
|
||||||
|
'status' => $statuses[ $status ] ?? $status,
|
||||||
|
'status_raw' => $status,
|
||||||
|
'site_name' => get_bloginfo( 'name' ),
|
||||||
|
'site_url' => home_url(),
|
||||||
|
'admin_email' => get_option( 'admin_email' ),
|
||||||
|
'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email template with placeholders replaced.
|
||||||
|
*
|
||||||
|
* @param string $template_name Template name.
|
||||||
|
* @param array $data Template data.
|
||||||
|
* @return string HTML email content.
|
||||||
|
*/
|
||||||
|
private static function get_email_template( string $template_name, array $data ): string {
|
||||||
|
$template = self::get_template_content( $template_name );
|
||||||
|
|
||||||
|
// Replace placeholders.
|
||||||
|
foreach ( $data as $key => $value ) {
|
||||||
|
$template = str_replace( '{' . $key . '}', (string) $value, $template );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template content.
|
||||||
|
*
|
||||||
|
* @param string $template_name Template name.
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function get_template_content( string $template_name ): string {
|
||||||
|
// Built-in templates. Could be extended to load from files.
|
||||||
|
$templates = array(
|
||||||
|
'admin-new-booking' => self::template_admin_new_booking(),
|
||||||
|
'booking-confirmed' => self::template_booking_confirmed(),
|
||||||
|
'admin-booking-confirmed' => self::template_admin_booking_confirmed(),
|
||||||
|
'booking-cancelled' => self::template_booking_cancelled(),
|
||||||
|
'admin-booking-cancelled' => self::template_admin_booking_cancelled(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $templates[ $template_name ] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send HTML email.
|
||||||
|
*
|
||||||
|
* @param string $to Recipient email.
|
||||||
|
* @param string $subject Email subject.
|
||||||
|
* @param string $message HTML message.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
private static function send_email( string $to, string $subject, string $message ): bool {
|
||||||
|
$headers = array(
|
||||||
|
'Content-Type: text/html; charset=UTF-8',
|
||||||
|
'From: ' . get_bloginfo( 'name' ) . ' <' . get_option( 'admin_email' ) . '>',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter email recipients.
|
||||||
|
*
|
||||||
|
* @param string $to Recipient email.
|
||||||
|
* @param string $subject Email subject.
|
||||||
|
* @param string $message Email message.
|
||||||
|
*/
|
||||||
|
$to = apply_filters( 'wp_bnb_booking_email_recipients', $to, $subject, $message );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter email subject.
|
||||||
|
*
|
||||||
|
* @param string $subject Email subject.
|
||||||
|
*/
|
||||||
|
$subject = apply_filters( 'wp_bnb_booking_email_subject', $subject );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter email content.
|
||||||
|
*
|
||||||
|
* @param string $message Email message.
|
||||||
|
*/
|
||||||
|
$message = apply_filters( 'wp_bnb_booking_email_content', $message );
|
||||||
|
|
||||||
|
return wp_mail( $to, $subject, $message, $headers );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base email styles.
|
||||||
|
*
|
||||||
|
* @return string CSS styles.
|
||||||
|
*/
|
||||||
|
private static function get_email_styles(): string {
|
||||||
|
return '
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 14px; line-height: 1.6; color: #333; }
|
||||||
|
.email-container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.email-header { background: #135e96; color: #fff; padding: 20px; text-align: center; }
|
||||||
|
.email-header h1 { margin: 0; font-size: 24px; }
|
||||||
|
.email-body { background: #fff; padding: 30px; border: 1px solid #ddd; }
|
||||||
|
.email-footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
|
||||||
|
.booking-details { background: #f9f9f9; padding: 15px; margin: 20px 0; border-left: 4px solid #135e96; }
|
||||||
|
.booking-details h3 { margin-top: 0; color: #135e96; }
|
||||||
|
.detail-row { margin: 8px 0; }
|
||||||
|
.detail-label { font-weight: 600; display: inline-block; min-width: 120px; }
|
||||||
|
.btn { display: inline-block; padding: 10px 20px; background: #135e96; color: #fff; text-decoration: none; border-radius: 4px; margin-top: 15px; }
|
||||||
|
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
||||||
|
.status-pending { background: #dba617; color: #fff; }
|
||||||
|
.status-confirmed { background: #00a32a; color: #fff; }
|
||||||
|
.status-cancelled { background: #d63638; color: #fff; }
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Admin new booking notification.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_admin_new_booking(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>New Booking Received</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>A new booking has been created and is awaiting confirmation.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Booking Details</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Status:</span> <span class="status-badge status-{status_raw}">{status}</span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Building:</span> {building_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Nights:</span> {nights}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Guest Information</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Name:</span> {guest_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Phone:</span> {guest_phone}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Adults:</span> {adults}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Children:</span> {children}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{booking_url}" class="btn">View Booking</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>This email was sent from {site_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Guest booking confirmed.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_booking_confirmed(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>Booking Confirmed</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Dear {guest_name},</p>
|
||||||
|
|
||||||
|
<p>Great news! Your booking has been confirmed. We look forward to welcoming you.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Your Booking Details</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Confirmation:</span> {booking_reference}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Location:</span> {building_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Duration:</span> {nights} nights</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Guests:</span> {adults} adults, {children} children</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Total:</span> <strong>{total_price}</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>If you have any questions or need to make changes to your reservation, please contact us at {admin_email}.</p>
|
||||||
|
|
||||||
|
<p>Thank you for choosing us!</p>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>{site_name}<br>{site_url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Admin booking confirmed.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_admin_booking_confirmed(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>Booking Confirmed</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Booking {booking_reference} has been confirmed.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Booking Summary</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{booking_url}" class="btn">View Booking</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>This email was sent from {site_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Guest booking cancelled.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_booking_cancelled(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header" style="background: #d63638;">
|
||||||
|
<h1>Booking Cancelled</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Dear {guest_name},</p>
|
||||||
|
|
||||||
|
<p>We're writing to confirm that your booking has been cancelled.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Cancelled Booking</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>If you have any questions or would like to make a new reservation, please contact us at {admin_email}.</p>
|
||||||
|
|
||||||
|
<p>We hope to welcome you in the future.</p>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>{site_name}<br>{site_url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Admin booking cancelled.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_admin_booking_cancelled(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header" style="background: #d63638;">
|
||||||
|
<h1>Booking Cancelled</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Booking {booking_reference} has been cancelled.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Cancelled Booking</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{booking_url}" class="btn">View Booking</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>This email was sent from {site_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
308
src/Plugin.php
308
src/Plugin.php
@@ -9,7 +9,17 @@ declare( strict_types=1 );
|
|||||||
|
|
||||||
namespace Magdev\WpBnb;
|
namespace Magdev\WpBnb;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||||
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Loader\FilesystemLoader;
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
@@ -67,6 +77,32 @@ final class Plugin {
|
|||||||
|
|
||||||
// Add plugin action links.
|
// Add plugin action links.
|
||||||
add_filter( 'plugin_action_links_' . WP_BNB_BASENAME, array( $this, 'add_action_links' ) );
|
add_filter( 'plugin_action_links_' . WP_BNB_BASENAME, array( $this, 'add_action_links' ) );
|
||||||
|
|
||||||
|
// Register custom post types and taxonomies.
|
||||||
|
$this->register_post_types();
|
||||||
|
$this->register_taxonomies();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom post types.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function register_post_types(): void {
|
||||||
|
Building::init();
|
||||||
|
Room::init();
|
||||||
|
Booking::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom taxonomies.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function register_taxonomies(): void {
|
||||||
|
// Taxonomies must be registered before post types that use them.
|
||||||
|
Amenity::init();
|
||||||
|
RoomType::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,6 +134,18 @@ 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();
|
||||||
|
|
||||||
|
// Initialize calendar admin page.
|
||||||
|
CalendarAdmin::init();
|
||||||
|
|
||||||
|
// Initialize email notifier.
|
||||||
|
EmailNotifier::init();
|
||||||
|
|
||||||
|
// Register AJAX handlers.
|
||||||
|
add_action( 'wp_ajax_wp_bnb_check_availability', array( $this, 'ajax_check_availability' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,8 +177,14 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function enqueue_admin_assets( string $hook_suffix ): void {
|
public function enqueue_admin_assets( string $hook_suffix ): void {
|
||||||
// Only load on plugin pages.
|
global $post_type;
|
||||||
if ( strpos( $hook_suffix, 'wp-bnb' ) === false ) {
|
|
||||||
|
// Check if we're on plugin pages or editing our custom post types.
|
||||||
|
$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 ), 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 ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,10 +195,18 @@ final class Plugin {
|
|||||||
WP_BNB_VERSION
|
WP_BNB_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$script_deps = array( 'jquery' );
|
||||||
|
|
||||||
|
// Add media dependencies for room gallery.
|
||||||
|
if ( Room::POST_TYPE === $post_type && $is_edit_screen ) {
|
||||||
|
wp_enqueue_media();
|
||||||
|
$script_deps[] = 'jquery-ui-sortable';
|
||||||
|
}
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wp-bnb-admin',
|
'wp-bnb-admin',
|
||||||
WP_BNB_URL . 'assets/js/admin.js',
|
WP_BNB_URL . 'assets/js/admin.js',
|
||||||
array( 'jquery' ),
|
$script_deps,
|
||||||
WP_BNB_VERSION,
|
WP_BNB_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -153,12 +215,26 @@ final class Plugin {
|
|||||||
'wp-bnb-admin',
|
'wp-bnb-admin',
|
||||||
'wpBnbAdmin',
|
'wpBnbAdmin',
|
||||||
array(
|
array(
|
||||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||||
'i18n' => array(
|
'postType' => $post_type,
|
||||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
'i18n' => array(
|
||||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||||
|
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||||
|
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||||
|
'addToGallery' => __( 'Add to Gallery', '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' ),
|
||||||
|
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||||
|
'available' => __( 'Available', 'wp-bnb' ),
|
||||||
|
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||||
|
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||||
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
|
'night' => __( 'night', 'wp-bnb' ),
|
||||||
|
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -306,6 +382,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' ); ?>
|
||||||
@@ -315,6 +395,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;
|
||||||
@@ -380,6 +463,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.
|
||||||
*
|
*
|
||||||
@@ -541,6 +761,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;
|
||||||
@@ -567,6 +790,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.
|
||||||
*
|
*
|
||||||
@@ -585,6 +838,43 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for checking room availability.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_check_availability(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||||
|
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||||
|
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||||
|
$exclude = isset( $_POST['exclude_booking'] ) ? absint( $_POST['exclude_booking'] ) : null;
|
||||||
|
|
||||||
|
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( strtotime( $check_out ) <= strtotime( $check_in ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Check-out date must be after check-in date.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Availability::check_availability_with_price( $room_id, $check_in, $check_out, $exclude );
|
||||||
|
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Twig environment.
|
* Get Twig environment.
|
||||||
*
|
*
|
||||||
|
|||||||
1137
src/PostTypes/Booking.php
Normal file
1137
src/PostTypes/Booking.php
Normal file
File diff suppressed because it is too large
Load Diff
562
src/PostTypes/Building.php
Normal file
562
src/PostTypes/Building.php
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Building post type.
|
||||||
|
*
|
||||||
|
* Custom post type for BnB buildings/properties.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\PostTypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\PostTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building post type class.
|
||||||
|
*/
|
||||||
|
final class Building {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post type slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const POST_TYPE = 'bnb_building';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta key prefix.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const META_PREFIX = '_bnb_building_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_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( 'Buildings', 'post type general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Building', 'post type singular name', 'wp-bnb' ),
|
||||||
|
'menu_name' => _x( 'Buildings', 'admin menu', 'wp-bnb' ),
|
||||||
|
'name_admin_bar' => _x( 'Building', 'add new on admin bar', 'wp-bnb' ),
|
||||||
|
'add_new' => _x( 'Add New', 'building', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Building', 'wp-bnb' ),
|
||||||
|
'new_item' => __( 'New Building', 'wp-bnb' ),
|
||||||
|
'edit_item' => __( 'Edit Building', 'wp-bnb' ),
|
||||||
|
'view_item' => __( 'View Building', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'Buildings', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Buildings', 'wp-bnb' ),
|
||||||
|
'parent_item_colon' => __( 'Parent Buildings:', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No buildings found.', 'wp-bnb' ),
|
||||||
|
'not_found_in_trash' => __( 'No buildings found in Trash.', 'wp-bnb' ),
|
||||||
|
'featured_image' => __( 'Building Image', 'wp-bnb' ),
|
||||||
|
'set_featured_image' => __( 'Set building image', 'wp-bnb' ),
|
||||||
|
'remove_featured_image' => __( 'Remove building image', 'wp-bnb' ),
|
||||||
|
'use_featured_image' => __( 'Use as building image', 'wp-bnb' ),
|
||||||
|
'archives' => __( 'Building archives', 'wp-bnb' ),
|
||||||
|
'insert_into_item' => __( 'Insert into building', 'wp-bnb' ),
|
||||||
|
'uploaded_to_this_item' => __( 'Uploaded to this building', 'wp-bnb' ),
|
||||||
|
'filter_items_list' => __( 'Filter buildings list', 'wp-bnb' ),
|
||||||
|
'items_list_navigation' => __( 'Buildings list navigation', 'wp-bnb' ),
|
||||||
|
'items_list' => __( 'Buildings list', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'public' => true,
|
||||||
|
'publicly_queryable' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => 'wp-bnb',
|
||||||
|
'query_var' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'building',
|
||||||
|
'with_front' => false,
|
||||||
|
),
|
||||||
|
'capability_type' => 'post',
|
||||||
|
'has_archive' => true,
|
||||||
|
'hierarchical' => false,
|
||||||
|
'menu_position' => null,
|
||||||
|
'menu_icon' => 'dashicons-building',
|
||||||
|
'supports' => array(
|
||||||
|
'title',
|
||||||
|
'editor',
|
||||||
|
'thumbnail',
|
||||||
|
'excerpt',
|
||||||
|
'revisions',
|
||||||
|
),
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'rest_base' => 'buildings',
|
||||||
|
'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_building_address',
|
||||||
|
__( 'Address', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_address_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_building_contact',
|
||||||
|
__( 'Contact Information', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_contact_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_building_details',
|
||||||
|
__( 'Building Details', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_details_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'side',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render address meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_address_meta_box( \WP_Post $post ): void {
|
||||||
|
wp_nonce_field( 'bnb_building_meta', 'bnb_building_meta_nonce' );
|
||||||
|
|
||||||
|
$street = get_post_meta( $post->ID, self::META_PREFIX . 'street', true );
|
||||||
|
$street2 = get_post_meta( $post->ID, self::META_PREFIX . 'street2', true );
|
||||||
|
$city = get_post_meta( $post->ID, self::META_PREFIX . 'city', true );
|
||||||
|
$state = get_post_meta( $post->ID, self::META_PREFIX . 'state', true );
|
||||||
|
$zip = get_post_meta( $post->ID, self::META_PREFIX . 'zip', true );
|
||||||
|
$country = get_post_meta( $post->ID, self::META_PREFIX . 'country', true );
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_street"><?php esc_html_e( 'Street Address', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_building_street" name="bnb_building_street"
|
||||||
|
value="<?php echo esc_attr( $street ); ?>" class="large-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_street2"><?php esc_html_e( 'Street Address 2', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_building_street2" name="bnb_building_street2"
|
||||||
|
value="<?php echo esc_attr( $street2 ); ?>" class="large-text">
|
||||||
|
<p class="description"><?php esc_html_e( 'Apartment, suite, unit, etc. (optional)', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_city"><?php esc_html_e( 'City', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_building_city" name="bnb_building_city"
|
||||||
|
value="<?php echo esc_attr( $city ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_state"><?php esc_html_e( 'State / Canton', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_building_state" name="bnb_building_state"
|
||||||
|
value="<?php echo esc_attr( $state ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_zip"><?php esc_html_e( 'ZIP / Postal Code', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_building_zip" name="bnb_building_zip"
|
||||||
|
value="<?php echo esc_attr( $zip ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_country"><?php esc_html_e( 'Country', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select id="bnb_building_country" name="bnb_building_country">
|
||||||
|
<option value=""><?php esc_html_e( '— Select Country —', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( self::get_countries() as $code => $name ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $country, $code ); ?>>
|
||||||
|
<?php echo esc_html( $name ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render contact meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_contact_meta_box( \WP_Post $post ): void {
|
||||||
|
$phone = get_post_meta( $post->ID, self::META_PREFIX . 'phone', true );
|
||||||
|
$email = get_post_meta( $post->ID, self::META_PREFIX . 'email', true );
|
||||||
|
$website = get_post_meta( $post->ID, self::META_PREFIX . 'website', true );
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="tel" id="bnb_building_phone" name="bnb_building_phone"
|
||||||
|
value="<?php echo esc_attr( $phone ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="email" id="bnb_building_email" name="bnb_building_email"
|
||||||
|
value="<?php echo esc_attr( $email ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_building_website"><?php esc_html_e( 'Website', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" id="bnb_building_website" name="bnb_building_website"
|
||||||
|
value="<?php echo esc_attr( $website ); ?>" class="regular-text"
|
||||||
|
placeholder="https://">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render details meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_details_meta_box( \WP_Post $post ): void {
|
||||||
|
$total_rooms = get_post_meta( $post->ID, self::META_PREFIX . 'total_rooms', true );
|
||||||
|
$floors = get_post_meta( $post->ID, self::META_PREFIX . 'floors', true );
|
||||||
|
$year_built = get_post_meta( $post->ID, self::META_PREFIX . 'year_built', true );
|
||||||
|
$check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in_time', true );
|
||||||
|
$check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out_time', true );
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_building_total_rooms"><?php esc_html_e( 'Total Rooms', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" id="bnb_building_total_rooms" name="bnb_building_total_rooms"
|
||||||
|
value="<?php echo esc_attr( $total_rooms ); ?>" class="small-text" min="1">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_building_floors"><?php esc_html_e( 'Number of Floors', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" id="bnb_building_floors" name="bnb_building_floors"
|
||||||
|
value="<?php echo esc_attr( $floors ); ?>" class="small-text" min="1">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_building_year_built"><?php esc_html_e( 'Year Built', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" id="bnb_building_year_built" name="bnb_building_year_built"
|
||||||
|
value="<?php echo esc_attr( $year_built ); ?>" class="small-text"
|
||||||
|
min="1800" max="<?php echo esc_attr( gmdate( 'Y' ) ); ?>">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_building_check_in_time"><?php esc_html_e( 'Check-in Time', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="time" id="bnb_building_check_in_time" name="bnb_building_check_in_time"
|
||||||
|
value="<?php echo esc_attr( $check_in ?: '14:00' ); ?>">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_building_check_out_time"><?php esc_html_e( 'Check-out Time', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="time" id="bnb_building_check_out_time" name="bnb_building_check_out_time"
|
||||||
|
value="<?php echo esc_attr( $check_out ?: '11:00' ); ?>">
|
||||||
|
</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_building_meta_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( sanitize_key( $_POST['bnb_building_meta_nonce'] ), 'bnb_building_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosave.
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions.
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address fields.
|
||||||
|
$text_fields = array(
|
||||||
|
'street',
|
||||||
|
'street2',
|
||||||
|
'city',
|
||||||
|
'state',
|
||||||
|
'zip',
|
||||||
|
'country',
|
||||||
|
'phone',
|
||||||
|
'website',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $text_fields as $field ) {
|
||||||
|
$key = 'bnb_building_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . $field,
|
||||||
|
sanitize_text_field( wp_unslash( $_POST[ $key ] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email field (special sanitization).
|
||||||
|
if ( isset( $_POST['bnb_building_email'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . 'email',
|
||||||
|
sanitize_email( wp_unslash( $_POST['bnb_building_email'] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric fields.
|
||||||
|
$numeric_fields = array( 'total_rooms', 'floors', 'year_built' );
|
||||||
|
foreach ( $numeric_fields as $field ) {
|
||||||
|
$key = 'bnb_building_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . $field,
|
||||||
|
absint( $_POST[ $key ] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time fields.
|
||||||
|
$time_fields = array( 'check_in_time', 'check_out_time' );
|
||||||
|
foreach ( $time_fields as $field ) {
|
||||||
|
$key = 'bnb_building_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
$time = sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
|
||||||
|
if ( preg_match( '/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . $field, $time );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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['city'] = __( 'City', 'wp-bnb' );
|
||||||
|
$new_columns['country'] = __( 'Country', 'wp-bnb' );
|
||||||
|
$new_columns['rooms'] = __( 'Rooms', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 'city':
|
||||||
|
$city = get_post_meta( $post_id, self::META_PREFIX . 'city', true );
|
||||||
|
echo esc_html( $city ?: '—' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'country':
|
||||||
|
$country = get_post_meta( $post_id, self::META_PREFIX . 'country', true );
|
||||||
|
if ( $country ) {
|
||||||
|
$countries = self::get_countries();
|
||||||
|
echo esc_html( $countries[ $country ] ?? $country );
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'rooms':
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => 'bnb_room',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => $post_id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$count = count( $rooms );
|
||||||
|
if ( $count > 0 ) {
|
||||||
|
printf(
|
||||||
|
'<a href="%s">%s</a>',
|
||||||
|
esc_url(
|
||||||
|
admin_url(
|
||||||
|
'edit.php?post_type=bnb_room&building_id=' . $post_id
|
||||||
|
)
|
||||||
|
),
|
||||||
|
esc_html(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %d: Number of rooms */
|
||||||
|
_n( '%d room', '%d rooms', $count, 'wp-bnb' ),
|
||||||
|
$count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add sortable columns.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing sortable columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function sortable_columns( array $columns ): array {
|
||||||
|
$columns['city'] = 'city';
|
||||||
|
$columns['country'] = 'country';
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 __( 'Enter building name', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
return $placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of countries.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_countries(): array {
|
||||||
|
return array(
|
||||||
|
'CH' => __( 'Switzerland', 'wp-bnb' ),
|
||||||
|
'DE' => __( 'Germany', 'wp-bnb' ),
|
||||||
|
'AT' => __( 'Austria', 'wp-bnb' ),
|
||||||
|
'FR' => __( 'France', 'wp-bnb' ),
|
||||||
|
'IT' => __( 'Italy', 'wp-bnb' ),
|
||||||
|
'LI' => __( 'Liechtenstein', 'wp-bnb' ),
|
||||||
|
'NL' => __( 'Netherlands', 'wp-bnb' ),
|
||||||
|
'BE' => __( 'Belgium', 'wp-bnb' ),
|
||||||
|
'LU' => __( 'Luxembourg', 'wp-bnb' ),
|
||||||
|
'GB' => __( 'United Kingdom', 'wp-bnb' ),
|
||||||
|
'US' => __( 'United States', 'wp-bnb' ),
|
||||||
|
'CA' => __( 'Canada', 'wp-bnb' ),
|
||||||
|
'ES' => __( 'Spain', 'wp-bnb' ),
|
||||||
|
'PT' => __( 'Portugal', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted address.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_formatted_address( int $post_id ): string {
|
||||||
|
$street = get_post_meta( $post_id, self::META_PREFIX . 'street', true );
|
||||||
|
$street2 = get_post_meta( $post_id, self::META_PREFIX . 'street2', true );
|
||||||
|
$city = get_post_meta( $post_id, self::META_PREFIX . 'city', true );
|
||||||
|
$state = get_post_meta( $post_id, self::META_PREFIX . 'state', true );
|
||||||
|
$zip = get_post_meta( $post_id, self::META_PREFIX . 'zip', true );
|
||||||
|
$country = get_post_meta( $post_id, self::META_PREFIX . 'country', true );
|
||||||
|
|
||||||
|
$parts = array();
|
||||||
|
|
||||||
|
if ( $street ) {
|
||||||
|
$parts[] = $street;
|
||||||
|
}
|
||||||
|
if ( $street2 ) {
|
||||||
|
$parts[] = $street2;
|
||||||
|
}
|
||||||
|
if ( $zip || $city ) {
|
||||||
|
$parts[] = trim( $zip . ' ' . $city );
|
||||||
|
}
|
||||||
|
if ( $state ) {
|
||||||
|
$parts[] = $state;
|
||||||
|
}
|
||||||
|
if ( $country ) {
|
||||||
|
$countries = self::get_countries();
|
||||||
|
$parts[] = $countries[ $country ] ?? $country;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode( "\n", $parts );
|
||||||
|
}
|
||||||
|
}
|
||||||
780
src/PostTypes/Room.php
Normal file
780
src/PostTypes/Room.php
Normal file
@@ -0,0 +1,780 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Room post type.
|
||||||
|
*
|
||||||
|
* Custom post type for BnB rooms.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\PostTypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\PostTypes;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room post type class.
|
||||||
|
*/
|
||||||
|
final class Room {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post type slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const POST_TYPE = 'bnb_room';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta key prefix.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const META_PREFIX = '_bnb_room_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_building_filter' ) );
|
||||||
|
add_action( 'pre_get_posts', array( self::class, 'filter_by_building' ) );
|
||||||
|
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( 'Rooms', 'post type general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Room', 'post type singular name', 'wp-bnb' ),
|
||||||
|
'menu_name' => _x( 'Rooms', 'admin menu', 'wp-bnb' ),
|
||||||
|
'name_admin_bar' => _x( 'Room', 'add new on admin bar', 'wp-bnb' ),
|
||||||
|
'add_new' => _x( 'Add New', 'room', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Room', 'wp-bnb' ),
|
||||||
|
'new_item' => __( 'New Room', 'wp-bnb' ),
|
||||||
|
'edit_item' => __( 'Edit Room', 'wp-bnb' ),
|
||||||
|
'view_item' => __( 'View Room', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'Rooms', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Rooms', 'wp-bnb' ),
|
||||||
|
'parent_item_colon' => __( 'Parent Rooms:', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No rooms found.', 'wp-bnb' ),
|
||||||
|
'not_found_in_trash' => __( 'No rooms found in Trash.', 'wp-bnb' ),
|
||||||
|
'featured_image' => __( 'Room Image', 'wp-bnb' ),
|
||||||
|
'set_featured_image' => __( 'Set room image', 'wp-bnb' ),
|
||||||
|
'remove_featured_image' => __( 'Remove room image', 'wp-bnb' ),
|
||||||
|
'use_featured_image' => __( 'Use as room image', 'wp-bnb' ),
|
||||||
|
'archives' => __( 'Room archives', 'wp-bnb' ),
|
||||||
|
'insert_into_item' => __( 'Insert into room', 'wp-bnb' ),
|
||||||
|
'uploaded_to_this_item' => __( 'Uploaded to this room', 'wp-bnb' ),
|
||||||
|
'filter_items_list' => __( 'Filter rooms list', 'wp-bnb' ),
|
||||||
|
'items_list_navigation' => __( 'Rooms list navigation', 'wp-bnb' ),
|
||||||
|
'items_list' => __( 'Rooms list', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'public' => true,
|
||||||
|
'publicly_queryable' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => 'wp-bnb',
|
||||||
|
'query_var' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'room',
|
||||||
|
'with_front' => false,
|
||||||
|
),
|
||||||
|
'capability_type' => 'post',
|
||||||
|
'has_archive' => true,
|
||||||
|
'hierarchical' => false,
|
||||||
|
'menu_position' => null,
|
||||||
|
'menu_icon' => 'dashicons-admin-home',
|
||||||
|
'supports' => array(
|
||||||
|
'title',
|
||||||
|
'editor',
|
||||||
|
'thumbnail',
|
||||||
|
'excerpt',
|
||||||
|
'revisions',
|
||||||
|
),
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'rest_base' => 'rooms',
|
||||||
|
'rest_controller_class' => 'WP_REST_Posts_Controller',
|
||||||
|
'taxonomies' => array( RoomType::TAXONOMY, Amenity::TAXONOMY ),
|
||||||
|
);
|
||||||
|
|
||||||
|
register_post_type( self::POST_TYPE, $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add meta boxes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_meta_boxes(): void {
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_room_building',
|
||||||
|
__( 'Building', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_building_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'side',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_room_details',
|
||||||
|
__( 'Room Details', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_details_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_room_gallery',
|
||||||
|
__( 'Room Gallery', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_gallery_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_room_pricing',
|
||||||
|
__( 'Room Pricing', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_pricing_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render building selection meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_building_meta_box( \WP_Post $post ): void {
|
||||||
|
wp_nonce_field( 'bnb_room_meta', 'bnb_room_meta_nonce' );
|
||||||
|
|
||||||
|
$building_id = get_post_meta( $post->ID, self::META_PREFIX . 'building_id', true );
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_room_building_id"><?php esc_html_e( 'Select Building', 'wp-bnb' ); ?></label>
|
||||||
|
</p>
|
||||||
|
<select id="bnb_room_building_id" name="bnb_room_building_id" class="widefat" required>
|
||||||
|
<option value=""><?php esc_html_e( '— Select Building —', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $building_id, $building->ID ); ?>>
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php if ( empty( $buildings ) ) : ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Link to add new building */
|
||||||
|
esc_html__( 'No buildings found. %s first.', 'wp-bnb' ),
|
||||||
|
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=' . Building::POST_TYPE ) ) . '">' . esc_html__( 'Add a building', 'wp-bnb' ) . '</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room details meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_details_meta_box( \WP_Post $post ): void {
|
||||||
|
$room_number = get_post_meta( $post->ID, self::META_PREFIX . 'room_number', true );
|
||||||
|
$floor = get_post_meta( $post->ID, self::META_PREFIX . 'floor', true );
|
||||||
|
$capacity = get_post_meta( $post->ID, self::META_PREFIX . 'capacity', true );
|
||||||
|
$max_adults = get_post_meta( $post->ID, self::META_PREFIX . 'max_adults', true );
|
||||||
|
$max_children = get_post_meta( $post->ID, self::META_PREFIX . 'max_children', true );
|
||||||
|
$size = get_post_meta( $post->ID, self::META_PREFIX . 'size', true );
|
||||||
|
$beds = get_post_meta( $post->ID, self::META_PREFIX . 'beds', true );
|
||||||
|
$bathrooms = get_post_meta( $post->ID, self::META_PREFIX . 'bathrooms', true );
|
||||||
|
$status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true );
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_room_number"><?php esc_html_e( 'Room Number', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_room_room_number" name="bnb_room_room_number"
|
||||||
|
value="<?php echo esc_attr( $room_number ); ?>" class="regular-text">
|
||||||
|
<p class="description"><?php esc_html_e( 'Room identifier (e.g., 101, A-12, Suite B)', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_floor"><?php esc_html_e( 'Floor', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" id="bnb_room_floor" name="bnb_room_floor"
|
||||||
|
value="<?php echo esc_attr( $floor ); ?>" class="small-text" min="0">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_size"><?php esc_html_e( 'Size (m²)', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" id="bnb_room_size" name="bnb_room_size"
|
||||||
|
value="<?php echo esc_attr( $size ); ?>" class="small-text" min="1" step="0.1">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<?php esc_html_e( 'Capacity', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<label for="bnb_room_capacity"><?php esc_html_e( 'Total Guests', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" id="bnb_room_capacity" name="bnb_room_capacity"
|
||||||
|
value="<?php echo esc_attr( $capacity ?: '2' ); ?>" class="small-text" min="1" max="20">
|
||||||
|
<br><br>
|
||||||
|
<label for="bnb_room_max_adults"><?php esc_html_e( 'Max Adults', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" id="bnb_room_max_adults" name="bnb_room_max_adults"
|
||||||
|
value="<?php echo esc_attr( $max_adults ?: '2' ); ?>" class="small-text" min="1" max="10">
|
||||||
|
<br><br>
|
||||||
|
<label for="bnb_room_max_children"><?php esc_html_e( 'Max Children', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" id="bnb_room_max_children" name="bnb_room_max_children"
|
||||||
|
value="<?php echo esc_attr( $max_children ?: '0' ); ?>" class="small-text" min="0" max="10">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_beds"><?php esc_html_e( 'Beds', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_room_beds" name="bnb_room_beds"
|
||||||
|
value="<?php echo esc_attr( $beds ); ?>" class="regular-text">
|
||||||
|
<p class="description"><?php esc_html_e( 'Description of beds (e.g., 1 King, 2 Singles)', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_bathrooms"><?php esc_html_e( 'Bathrooms', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" id="bnb_room_bathrooms" name="bnb_room_bathrooms"
|
||||||
|
value="<?php echo esc_attr( $bathrooms ?: '1' ); ?>" class="small-text" min="0" max="5" step="0.5">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_room_status"><?php esc_html_e( 'Room Status', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select id="bnb_room_status" name="bnb_room_status">
|
||||||
|
<?php foreach ( self::get_room_statuses() as $key => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $status ?: 'available', $key ); ?>>
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render gallery meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_gallery_meta_box( \WP_Post $post ): void {
|
||||||
|
$gallery_ids = get_post_meta( $post->ID, self::META_PREFIX . 'gallery', true );
|
||||||
|
$gallery_ids = $gallery_ids ? explode( ',', $gallery_ids ) : array();
|
||||||
|
?>
|
||||||
|
<div id="bnb-room-gallery" class="bnb-gallery-container">
|
||||||
|
<div class="bnb-gallery-images">
|
||||||
|
<?php foreach ( $gallery_ids as $image_id ) : ?>
|
||||||
|
<?php $image = wp_get_attachment_image_src( $image_id, 'thumbnail' ); ?>
|
||||||
|
<?php if ( $image ) : ?>
|
||||||
|
<div class="bnb-gallery-image" data-id="<?php echo esc_attr( $image_id ); ?>">
|
||||||
|
<img src="<?php echo esc_url( $image[0] ); ?>" alt="">
|
||||||
|
<button type="button" class="bnb-remove-image">×</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="bnb_room_gallery" name="bnb_room_gallery"
|
||||||
|
value="<?php echo esc_attr( implode( ',', $gallery_ids ) ); ?>">
|
||||||
|
<button type="button" id="bnb-add-gallery-images" class="button">
|
||||||
|
<?php esc_html_e( 'Add Images', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="description"><?php esc_html_e( 'Add additional images for this room. Drag to reorder.', 'wp-bnb' ); ?></p>
|
||||||
|
<?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.
|
||||||
|
*
|
||||||
|
* @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_room_meta_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( sanitize_key( $_POST['bnb_room_meta_nonce'] ), 'bnb_room_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosave.
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions.
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Building ID.
|
||||||
|
if ( isset( $_POST['bnb_room_building_id'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . 'building_id',
|
||||||
|
absint( $_POST['bnb_room_building_id'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text fields.
|
||||||
|
$text_fields = array( 'room_number', 'beds', 'status' );
|
||||||
|
foreach ( $text_fields as $field ) {
|
||||||
|
$key = 'bnb_room_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . $field,
|
||||||
|
sanitize_text_field( wp_unslash( $_POST[ $key ] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric fields.
|
||||||
|
$numeric_fields = array( 'floor', 'capacity', 'max_adults', 'max_children' );
|
||||||
|
foreach ( $numeric_fields as $field ) {
|
||||||
|
$key = 'bnb_room_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . $field,
|
||||||
|
absint( $_POST[ $key ] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float fields.
|
||||||
|
$float_fields = array( 'size', 'bathrooms' );
|
||||||
|
foreach ( $float_fields as $field ) {
|
||||||
|
$key = 'bnb_room_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . $field,
|
||||||
|
floatval( $_POST[ $key ] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gallery.
|
||||||
|
if ( isset( $_POST['bnb_room_gallery'] ) ) {
|
||||||
|
$gallery_ids = sanitize_text_field( wp_unslash( $_POST['bnb_room_gallery'] ) );
|
||||||
|
// Validate that each ID is numeric.
|
||||||
|
$ids = array_filter( explode( ',', $gallery_ids ), 'is_numeric' );
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . 'gallery',
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ) {
|
||||||
|
if ( 'taxonomy-bnb_room_type' === $key ) {
|
||||||
|
continue; // Will add after title.
|
||||||
|
}
|
||||||
|
if ( 'taxonomy-bnb_amenity' === $key ) {
|
||||||
|
continue; // Will add after room type.
|
||||||
|
}
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
if ( 'title' === $key ) {
|
||||||
|
$new_columns['building'] = __( 'Building', 'wp-bnb' );
|
||||||
|
$new_columns['room_number'] = __( 'Room #', 'wp-bnb' );
|
||||||
|
$new_columns['taxonomy-bnb_room_type'] = __( 'Room Type', 'wp-bnb' );
|
||||||
|
$new_columns['capacity'] = __( 'Capacity', 'wp-bnb' );
|
||||||
|
$new_columns['price'] = __( 'Price/Night', 'wp-bnb' );
|
||||||
|
$new_columns['status'] = __( 'Status', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 'building':
|
||||||
|
$building_id = get_post_meta( $post_id, self::META_PREFIX . 'building_id', true );
|
||||||
|
if ( $building_id ) {
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
if ( $building ) {
|
||||||
|
printf(
|
||||||
|
'<a href="%s">%s</a>',
|
||||||
|
esc_url( get_edit_post_link( $building_id ) ),
|
||||||
|
esc_html( $building->post_title )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'room_number':
|
||||||
|
$room_number = get_post_meta( $post_id, self::META_PREFIX . 'room_number', true );
|
||||||
|
echo esc_html( $room_number ?: '—' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'capacity':
|
||||||
|
$capacity = get_post_meta( $post_id, self::META_PREFIX . 'capacity', true );
|
||||||
|
if ( $capacity ) {
|
||||||
|
printf(
|
||||||
|
'<span class="dashicons dashicons-groups"></span> %s',
|
||||||
|
esc_html( $capacity )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
echo '—';
|
||||||
|
}
|
||||||
|
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':
|
||||||
|
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'available';
|
||||||
|
$statuses = self::get_room_statuses();
|
||||||
|
$colors = self::get_status_colors();
|
||||||
|
?>
|
||||||
|
<span class="bnb-status-badge" style="background-color: <?php echo esc_attr( $colors[ $status ] ?? '#ccc' ); ?>">
|
||||||
|
<?php echo esc_html( $statuses[ $status ] ?? $status ); ?>
|
||||||
|
</span>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add sortable columns.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing sortable columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function sortable_columns( array $columns ): array {
|
||||||
|
$columns['building'] = 'building';
|
||||||
|
$columns['room_number'] = 'room_number';
|
||||||
|
$columns['capacity'] = 'capacity';
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add building filter dropdown to admin list.
|
||||||
|
*
|
||||||
|
* @param string $post_type Current post type.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_building_filter( string $post_type ): void {
|
||||||
|
if ( self::POST_TYPE !== $post_type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( empty( $buildings ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||||||
|
$selected = isset( $_GET['building_id'] ) ? absint( $_GET['building_id'] ) : 0;
|
||||||
|
?>
|
||||||
|
<select name="building_id">
|
||||||
|
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $selected, $building->ID ); ?>>
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rooms by building in admin list.
|
||||||
|
*
|
||||||
|
* @param \WP_Query $query Current query.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function filter_by_building( \WP_Query $query ): void {
|
||||||
|
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
if ( ! empty( $_GET['building_id'] ) ) {
|
||||||
|
$query->set(
|
||||||
|
'meta_query',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'key' => self::META_PREFIX . 'building_id',
|
||||||
|
'value' => absint( $_GET['building_id'] ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 __( 'Enter room name', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
return $placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room status options.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_room_statuses(): array {
|
||||||
|
return array(
|
||||||
|
'available' => __( 'Available', 'wp-bnb' ),
|
||||||
|
'occupied' => __( 'Occupied', 'wp-bnb' ),
|
||||||
|
'maintenance' => __( 'Maintenance', 'wp-bnb' ),
|
||||||
|
'blocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status color codes.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_status_colors(): array {
|
||||||
|
return array(
|
||||||
|
'available' => '#00a32a',
|
||||||
|
'occupied' => '#72aee6',
|
||||||
|
'maintenance' => '#dba617',
|
||||||
|
'blocked' => '#d63638',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get building for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @return \WP_Post|null
|
||||||
|
*/
|
||||||
|
public static function get_building( int $room_id ): ?\WP_Post {
|
||||||
|
$building_id = get_post_meta( $room_id, self::META_PREFIX . 'building_id', true );
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return get_post( $building_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rooms for a building.
|
||||||
|
*
|
||||||
|
* @param int $building_id Building post ID.
|
||||||
|
* @return array<\WP_Post>
|
||||||
|
*/
|
||||||
|
public static function get_rooms_for_building( int $building_id ): array {
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => self::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => self::META_PREFIX . 'building_id',
|
||||||
|
'value' => $building_id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => self::META_PREFIX . 'room_number',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
242
src/Taxonomies/Amenity.php
Normal file
242
src/Taxonomies/Amenity.php
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Amenity taxonomy.
|
||||||
|
*
|
||||||
|
* Non-hierarchical taxonomy for room amenities like WiFi, Parking, etc.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Taxonomies
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Taxonomies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amenity taxonomy class.
|
||||||
|
*/
|
||||||
|
final class Amenity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taxonomy slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const TAXONOMY = 'bnb_amenity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register' ) );
|
||||||
|
add_action( 'bnb_amenity_add_form_fields', array( self::class, 'add_form_fields' ) );
|
||||||
|
add_action( 'bnb_amenity_edit_form_fields', array( self::class, 'edit_form_fields' ), 10, 2 );
|
||||||
|
add_action( 'created_bnb_amenity', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_action( 'edited_bnb_amenity', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_filter( 'manage_edit-bnb_amenity_columns', array( self::class, 'add_columns' ) );
|
||||||
|
add_filter( 'manage_bnb_amenity_custom_column', array( self::class, 'render_column' ), 10, 3 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _x( 'Amenities', 'taxonomy general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Amenity', 'taxonomy singular name', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Amenities', 'wp-bnb' ),
|
||||||
|
'popular_items' => __( 'Popular Amenities', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'All Amenities', 'wp-bnb' ),
|
||||||
|
'parent_item' => null,
|
||||||
|
'parent_item_colon' => null,
|
||||||
|
'edit_item' => __( 'Edit Amenity', 'wp-bnb' ),
|
||||||
|
'update_item' => __( 'Update Amenity', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Amenity', 'wp-bnb' ),
|
||||||
|
'new_item_name' => __( 'New Amenity Name', 'wp-bnb' ),
|
||||||
|
'separate_items_with_commas' => __( 'Separate amenities with commas', 'wp-bnb' ),
|
||||||
|
'add_or_remove_items' => __( 'Add or remove amenities', 'wp-bnb' ),
|
||||||
|
'choose_from_most_used' => __( 'Choose from the most used amenities', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No amenities found.', 'wp-bnb' ),
|
||||||
|
'menu_name' => __( 'Amenities', 'wp-bnb' ),
|
||||||
|
'back_to_items' => __( '← Back to Amenities', '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' => true,
|
||||||
|
'show_in_quick_edit' => true,
|
||||||
|
'show_admin_column' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'amenity',
|
||||||
|
'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_room' ), $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="amenity-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||||
|
<select name="amenity_icon" id="amenity-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 amenity.', '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, 'amenity_icon', true );
|
||||||
|
?>
|
||||||
|
<tr class="form-field term-icon-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="amenity-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="amenity_icon" id="amenity-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 amenity.', '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['amenity_icon'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'amenity_icon',
|
||||||
|
sanitize_text_field( wp_unslash( $_POST['amenity_icon'] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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, 'amenity_icon', true );
|
||||||
|
if ( $icon ) {
|
||||||
|
return '<span class="dashicons dashicons-' . esc_attr( $icon ) . '"></span>';
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available icon options.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_icon_options(): array {
|
||||||
|
return array(
|
||||||
|
'' => __( '— Select Icon —', 'wp-bnb' ),
|
||||||
|
'wifi' => __( 'WiFi', 'wp-bnb' ),
|
||||||
|
'car' => __( 'Parking', 'wp-bnb' ),
|
||||||
|
'food' => __( 'Breakfast', 'wp-bnb' ),
|
||||||
|
'palmtree' => __( 'Garden/Pool', 'wp-bnb' ),
|
||||||
|
'pets' => __( 'Pet Friendly', 'wp-bnb' ),
|
||||||
|
'universal-access' => __( 'Accessibility', 'wp-bnb' ),
|
||||||
|
'tv' => __( 'Television', 'wp-bnb' ),
|
||||||
|
'superhero-alt' => __( 'Air Conditioning', 'wp-bnb' ),
|
||||||
|
'coffee' => __( 'Coffee/Tea', 'wp-bnb' ),
|
||||||
|
'admin-home' => __( 'Kitchen', 'wp-bnb' ),
|
||||||
|
'businessman' => __( 'Business Center', 'wp-bnb' ),
|
||||||
|
'heart' => __( 'Spa/Wellness', 'wp-bnb' ),
|
||||||
|
'groups' => __( 'Family Friendly', 'wp-bnb' ),
|
||||||
|
'location-alt' => __( 'Central Location', 'wp-bnb' ),
|
||||||
|
'building' => __( 'Elevator', 'wp-bnb' ),
|
||||||
|
'store' => __( 'Minibar', 'wp-bnb' ),
|
||||||
|
'admin-appearance' => __( 'Room Service', 'wp-bnb' ),
|
||||||
|
'shield' => __( 'Safe', 'wp-bnb' ),
|
||||||
|
'privacy' => __( 'Non-Smoking', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default amenities to seed on activation.
|
||||||
|
*
|
||||||
|
* @return array<string, array{icon: string}>
|
||||||
|
*/
|
||||||
|
public static function get_default_terms(): array {
|
||||||
|
return array(
|
||||||
|
__( 'WiFi', 'wp-bnb' ) => array( 'icon' => 'wifi' ),
|
||||||
|
__( 'Parking', 'wp-bnb' ) => array( 'icon' => 'car' ),
|
||||||
|
__( 'Breakfast Included', 'wp-bnb' ) => array( 'icon' => 'food' ),
|
||||||
|
__( 'Air Conditioning', 'wp-bnb' ) => array( 'icon' => 'superhero-alt' ),
|
||||||
|
__( 'Television', 'wp-bnb' ) => array( 'icon' => 'tv' ),
|
||||||
|
__( 'Pet Friendly', 'wp-bnb' ) => array( 'icon' => 'pets' ),
|
||||||
|
__( 'Wheelchair Accessible', 'wp-bnb' ) => array( 'icon' => 'universal-access' ),
|
||||||
|
__( 'Non-Smoking', 'wp-bnb' ) => array( 'icon' => 'privacy' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/Taxonomies/RoomType.php
Normal file
224
src/Taxonomies/RoomType.php
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Room Type taxonomy.
|
||||||
|
*
|
||||||
|
* Hierarchical taxonomy for room types like Standard, Suite, Family, etc.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Taxonomies
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Taxonomies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room Type taxonomy class.
|
||||||
|
*/
|
||||||
|
final class RoomType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taxonomy slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const TAXONOMY = 'bnb_room_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register' ) );
|
||||||
|
add_action( 'bnb_room_type_add_form_fields', array( self::class, 'add_form_fields' ) );
|
||||||
|
add_action( 'bnb_room_type_edit_form_fields', array( self::class, 'edit_form_fields' ), 10, 2 );
|
||||||
|
add_action( 'created_bnb_room_type', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_action( 'edited_bnb_room_type', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _x( 'Room Types', 'taxonomy general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Room Type', 'taxonomy singular name', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Room Types', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'All Room Types', 'wp-bnb' ),
|
||||||
|
'parent_item' => __( 'Parent Room Type', 'wp-bnb' ),
|
||||||
|
'parent_item_colon' => __( 'Parent Room Type:', 'wp-bnb' ),
|
||||||
|
'edit_item' => __( 'Edit Room Type', 'wp-bnb' ),
|
||||||
|
'update_item' => __( 'Update Room Type', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Room Type', 'wp-bnb' ),
|
||||||
|
'new_item_name' => __( 'New Room Type Name', 'wp-bnb' ),
|
||||||
|
'menu_name' => __( 'Room Types', 'wp-bnb' ),
|
||||||
|
'back_to_items' => __( '← Back to Room Types', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No room types found.', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'hierarchical' => true, // Hierarchical (like categories).
|
||||||
|
'public' => true,
|
||||||
|
'publicly_queryable' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => true,
|
||||||
|
'show_in_nav_menus' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'show_in_quick_edit' => true,
|
||||||
|
'show_admin_column' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'room-type',
|
||||||
|
'with_front' => false,
|
||||||
|
'hierarchical' => true,
|
||||||
|
),
|
||||||
|
'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_room' ), $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom fields to the add term form.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_form_fields(): void {
|
||||||
|
?>
|
||||||
|
<div class="form-field term-base-capacity-wrap">
|
||||||
|
<label for="room-type-base-capacity"><?php esc_html_e( 'Base Capacity', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" name="room_type_base_capacity" id="room-type-base-capacity" value="2" min="1" max="20">
|
||||||
|
<p><?php esc_html_e( 'Default number of guests this room type typically accommodates.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-field term-sort-order-wrap">
|
||||||
|
<label for="room-type-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" name="room_type_sort_order" id="room-type-sort-order" value="0" min="0">
|
||||||
|
<p><?php esc_html_e( 'Display order for this room type (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 {
|
||||||
|
$base_capacity = get_term_meta( $term->term_id, 'room_type_base_capacity', true );
|
||||||
|
$sort_order = get_term_meta( $term->term_id, 'room_type_sort_order', true );
|
||||||
|
?>
|
||||||
|
<tr class="form-field term-base-capacity-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="room-type-base-capacity"><?php esc_html_e( 'Base Capacity', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="room_type_base_capacity" id="room-type-base-capacity"
|
||||||
|
value="<?php echo esc_attr( $base_capacity ?: '2' ); ?>" min="1" max="20">
|
||||||
|
<p class="description"><?php esc_html_e( 'Default number of guests this room type typically accommodates.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="form-field term-sort-order-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="room-type-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="room_type_sort_order" id="room-type-sort-order"
|
||||||
|
value="<?php echo esc_attr( $sort_order ?: '0' ); ?>" min="0">
|
||||||
|
<p class="description"><?php esc_html_e( 'Display order for this room type (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['room_type_base_capacity'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'room_type_base_capacity',
|
||||||
|
absint( $_POST['room_type_base_capacity'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['room_type_sort_order'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'room_type_sort_order',
|
||||||
|
absint( $_POST['room_type_sort_order'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default room types to seed on activation.
|
||||||
|
*
|
||||||
|
* @return array<string, array{capacity: int, order: int, children?: array<string, array{capacity: int, order: int}>}>
|
||||||
|
*/
|
||||||
|
public static function get_default_terms(): array {
|
||||||
|
return array(
|
||||||
|
__( 'Standard', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 10,
|
||||||
|
'children' => array(
|
||||||
|
__( 'Single', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 1,
|
||||||
|
'order' => 11,
|
||||||
|
),
|
||||||
|
__( 'Double', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 12,
|
||||||
|
),
|
||||||
|
__( 'Twin', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
__( 'Superior', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 20,
|
||||||
|
),
|
||||||
|
__( 'Suite', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 30,
|
||||||
|
'children' => array(
|
||||||
|
__( 'Junior Suite', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 31,
|
||||||
|
),
|
||||||
|
__( 'Executive Suite', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
__( 'Family', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 4,
|
||||||
|
'order' => 40,
|
||||||
|
),
|
||||||
|
__( 'Accessible', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 2,
|
||||||
|
'order' => 50,
|
||||||
|
),
|
||||||
|
__( 'Apartment', 'wp-bnb' ) => array(
|
||||||
|
'capacity' => 4,
|
||||||
|
'order' => 60,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
wp-bnb.php
16
wp-bnb.php
@@ -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.0.1
|
* Version: 0.3.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.0.1' );
|
define( 'WP_BNB_VERSION', '0.3.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
@@ -155,6 +155,18 @@ function wp_bnb_activate(): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load Composer autoloader for activation.
|
||||||
|
$autoloader = WP_BNB_PATH . 'vendor/autoload.php';
|
||||||
|
if ( file_exists( $autoloader ) ) {
|
||||||
|
require_once $autoloader;
|
||||||
|
|
||||||
|
// Register post types and taxonomies before flushing rewrite rules.
|
||||||
|
\Magdev\WpBnb\Taxonomies\Amenity::register();
|
||||||
|
\Magdev\WpBnb\Taxonomies\RoomType::register();
|
||||||
|
\Magdev\WpBnb\PostTypes\Building::register();
|
||||||
|
\Magdev\WpBnb\PostTypes\Room::register();
|
||||||
|
}
|
||||||
|
|
||||||
// Set default options.
|
// Set default options.
|
||||||
add_option( 'wp_bnb_version', WP_BNB_VERSION );
|
add_option( 'wp_bnb_version', WP_BNB_VERSION );
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user