Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a784d92cc9 | |||
| f61dca5f45 | |||
| 28350aabfa | |||
| 3579904bad | |||
| 602549208f | |||
| 45a73e15aa | |||
| 13ba264431 | |||
| c17dd53c5a | |||
| be2735a3bd | |||
| 864b8b2869 |
196
CHANGELOG.md
196
CHANGELOG.md
@@ -5,6 +5,200 @@ 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.7.1] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- CF7 Admin Tag Generator buttons:
|
||||||
|
- Tag generator buttons appear in CF7 form editor for all WP BnB custom tags
|
||||||
|
- BnB Building select with first option label configuration
|
||||||
|
- BnB Room select with building field linking and price display options
|
||||||
|
- BnB Check-in date with min/max advance booking days
|
||||||
|
- BnB Check-out date with check-in field linking and min/max nights
|
||||||
|
- BnB Guests count with room field linking and min/max/default values
|
||||||
|
- All generators support id and class attribute configuration
|
||||||
|
|
||||||
|
## [0.7.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Contact Form 7 Integration:
|
||||||
|
- New `src/Integration/CF7.php` class for CF7 integration
|
||||||
|
- Custom form tags: `[bnb_building_select]`, `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`, `[bnb_guests]`
|
||||||
|
- Server-side validation for all custom tags
|
||||||
|
- Availability checking before form submission
|
||||||
|
- Automatic booking creation on form submission with 'pending' status
|
||||||
|
- Guest record creation/linking using existing `find_or_create_guest` pattern
|
||||||
|
- Price calculation using existing Calculator class
|
||||||
|
- Email notifications via existing EmailNotifier
|
||||||
|
- CF7 Frontend Assets:
|
||||||
|
- `assets/js/cf7-integration.js` for dynamic form behavior
|
||||||
|
- Building-based room filtering
|
||||||
|
- Date linking (checkout min = checkin + 1)
|
||||||
|
- Capacity validation against selected room
|
||||||
|
- AJAX availability checking with status display
|
||||||
|
- Dynamic price calculation display
|
||||||
|
- `assets/css/cf7-integration.css` for form styling
|
||||||
|
- Availability status indicators (checking/available/unavailable)
|
||||||
|
- Price display formatting
|
||||||
|
- Capacity warning styling
|
||||||
|
- Responsive design with dark mode support
|
||||||
|
- Custom CF7 Mail Tags:
|
||||||
|
- `[_bnb_booking_reference]` - Generated booking reference
|
||||||
|
- `[_bnb_booking_id]` - Booking post ID
|
||||||
|
- `[_bnb_room_name]` - Selected room title
|
||||||
|
- `[_bnb_calculated_price]` - Formatted price
|
||||||
|
- `[_bnb_nights]` - Number of nights
|
||||||
|
- Form Type Detection:
|
||||||
|
- Auto-detects booking forms by presence of `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`
|
||||||
|
- CSS class `wp-bnb-booking-form` for explicit form type declaration
|
||||||
|
- Inquiry forms use default CF7 email handling without booking creation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to conditionally initialize CF7 integration when CF7 is active
|
||||||
|
- Frontend assets now include CF7-specific CSS and JavaScript when CF7 is detected
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Contact Form 7 plugin required for CF7 integration features (optional)
|
||||||
|
|
||||||
|
## [0.6.1] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Auto-Update System:
|
||||||
|
- New `src/License/Updater.php` class for WordPress update integration
|
||||||
|
- Hooks into `pre_set_site_transient_update_plugins` for update detection
|
||||||
|
- Plugin info modal via `plugins_api` filter
|
||||||
|
- Configurable update check frequency (1-168 hours)
|
||||||
|
- Option to enable/disable update notifications
|
||||||
|
- Option to enable/disable automatic updates
|
||||||
|
- AJAX endpoint for manual update check
|
||||||
|
- Automatic cache clearing when license settings change
|
||||||
|
- Updates Tab in Settings:
|
||||||
|
- Enable/disable update notifications toggle
|
||||||
|
- Enable/disable automatic updates toggle
|
||||||
|
- Update check frequency setting
|
||||||
|
- Manual "Check for Updates" button
|
||||||
|
- Display of last check timestamp and current version
|
||||||
|
- Localhost Development Mode:
|
||||||
|
- License bypass for local development environments
|
||||||
|
- Detects: localhost, 127.0.0.1, ::1, .local/.test/.localhost/.dev/.ddev.site domains
|
||||||
|
- Private IP range detection (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
|
||||||
|
- "Development Mode" notice on Dashboard and License settings page
|
||||||
|
- Extended General Settings:
|
||||||
|
- Business address fields (street, city, postal code, country)
|
||||||
|
- Contact fields (email, phone, website)
|
||||||
|
- Social media fields (Facebook, Instagram, X/Twitter, LinkedIn, TripAdvisor)
|
||||||
|
- Pricing Settings Subtabs:
|
||||||
|
- Split into three subtabs: Pricing Tiers, Weekend Days, Seasons
|
||||||
|
- Each subtab has its own save button
|
||||||
|
- Seasons subtab shows priority column and link to Seasons Manager
|
||||||
|
- Guest Data Encryption:
|
||||||
|
- AES-256-CBC encryption for sensitive data (ID/passport numbers)
|
||||||
|
- Uses WordPress AUTH_KEY for encryption key derivation
|
||||||
|
- `encrypt()` and `decrypt()` methods in Guest class
|
||||||
|
- Backward compatible with legacy unencrypted data
|
||||||
|
- Security notice displayed in Identification meta box
|
||||||
|
- Guest Auto-Creation from Booking:
|
||||||
|
- When new guest data is entered in booking form, guest record is automatically created
|
||||||
|
- Links booking to the new guest via guest_id meta
|
||||||
|
- Prevents duplicate guest entries
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Admin submenu reordered for better organization:
|
||||||
|
- Dashboard at top, Settings at bottom
|
||||||
|
- Logical grouping: Buildings, Rooms, Bookings, Guests, Services, Calendar, Seasons
|
||||||
|
- Booking title auto-generates with guest name and dates (room number removed)
|
||||||
|
- Disabled Gutenberg block editor for form-based post types:
|
||||||
|
- Service, Guest, and Booking now use classic editor
|
||||||
|
- Meta boxes display properly instead of being hidden at bottom
|
||||||
|
- Form-based interfaces more appropriate than block editor for data entry
|
||||||
|
- Settings tabs now flush with tab content (no gap)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed Booking admin issues with auto-draft status causing type errors
|
||||||
|
- Fixed guest dropdown to always load existing guests
|
||||||
|
- Fixed booking history display on Guest edit page
|
||||||
|
- Fixed service pricing meta box not displaying radio buttons (Gutenberg hiding meta boxes)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Guest ID/passport numbers encrypted at rest using AES-256-CBC
|
||||||
|
- Random IV generation for each encryption operation
|
||||||
|
- Secure key derivation from WordPress AUTH_KEY
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-02-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Frontend Features System:
|
||||||
|
- Room search with multiple filters (availability, capacity, room type, amenities, price range, building)
|
||||||
|
- AJAX-powered search with pagination and "Load More" functionality
|
||||||
|
- Date validation (check-out after check-in, minimum today)
|
||||||
|
- Real-time availability checking on single room pages
|
||||||
|
- Price calculator with breakdown display
|
||||||
|
- Shortcodes:
|
||||||
|
- `[bnb_buildings]` - Display buildings list/grid with filtering and sorting
|
||||||
|
- `[bnb_rooms]` - Display rooms list/grid with multiple filter options
|
||||||
|
- `[bnb_room_search]` - Interactive room search form with results
|
||||||
|
- `[bnb_building id="X"]` - Display single building details
|
||||||
|
- `[bnb_room id="X"]` - Display single room details with availability form
|
||||||
|
- WordPress Widgets:
|
||||||
|
- Similar Rooms widget (shows rooms from same building/type)
|
||||||
|
- Building Rooms widget (lists all rooms in a building)
|
||||||
|
- Availability Calendar widget (mini calendar with booking status)
|
||||||
|
- Gutenberg Blocks:
|
||||||
|
- Building block with ID selector
|
||||||
|
- Room block with ID selector
|
||||||
|
- Room Search block with filter presets
|
||||||
|
- Buildings List block with layout options
|
||||||
|
- Rooms List block with filter options
|
||||||
|
- Server-side rendered blocks for consistent output
|
||||||
|
- Frontend Search Class (`src/Frontend/Search.php`):
|
||||||
|
- Core search functionality with availability filtering
|
||||||
|
- Price range filtering with Calculator integration
|
||||||
|
- Pagination support
|
||||||
|
- AJAX endpoints: search_rooms, get_availability, get_calendar, calculate_price
|
||||||
|
- Room data formatting for JSON responses
|
||||||
|
- Frontend Shortcodes Class (`src/Frontend/Shortcodes.php`):
|
||||||
|
- All shortcode registration and handlers
|
||||||
|
- Grid/list layout support
|
||||||
|
- Column configuration (1-4 columns)
|
||||||
|
- Sorting options (title, date, price, capacity)
|
||||||
|
- Limit and offset support
|
||||||
|
- Block Registrar Class (`src/Blocks/BlockRegistrar.php`):
|
||||||
|
- Gutenberg block registration
|
||||||
|
- Block editor assets (CSS/JS)
|
||||||
|
- Server-side render callbacks
|
||||||
|
- Block data localization for editor
|
||||||
|
- Frontend Assets:
|
||||||
|
- Comprehensive CSS with CSS custom properties for theming
|
||||||
|
- Building and room card styles
|
||||||
|
- Search form and results styling
|
||||||
|
- Calendar widget styling with availability states
|
||||||
|
- Responsive design (breakpoints: 480px, 768px, 1024px)
|
||||||
|
- JavaScript with SearchForm, CalendarWidget, AvailabilityForm, PriceCalculator classes
|
||||||
|
- AJAX integration with proper error handling
|
||||||
|
- XSS-safe DOM construction (no innerHTML with user data)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated with frontend component initialization
|
||||||
|
- Frontend assets now include localized script data with AJAX URL, nonce, and i18n strings
|
||||||
|
- Widget registration added to init_frontend() method
|
||||||
|
- Search, Shortcodes, and BlockRegistrar initialized when license is valid
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- AJAX nonce verification on all frontend requests
|
||||||
|
- Input sanitization on all search parameters
|
||||||
|
- Output escaping in shortcode and widget templates
|
||||||
|
- XSS prevention in JavaScript (textContent instead of innerHTML)
|
||||||
|
|
||||||
## [0.5.0] - 2026-01-31
|
## [0.5.0] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -290,6 +484,8 @@ 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.6.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.1
|
||||||
|
[0.6.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.0
|
||||||
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
||||||
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
||||||
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
||||||
|
|||||||
256
CLAUDE.md
256
CLAUDE.md
@@ -1,4 +1,4 @@
|
|||||||
# WordPress BnB Management
|
# WordPress BnB Manager
|
||||||
|
|
||||||
**Author:** Marco Graetsch
|
**Author:** Marco Graetsch
|
||||||
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
|
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
|
||||||
@@ -38,6 +38,10 @@ 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.
|
||||||
|
|
||||||
|
### Known Bugs
|
||||||
|
|
||||||
|
(none)
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **Language:** PHP 8.3.x
|
||||||
@@ -234,9 +238,18 @@ wp-bnb/
|
|||||||
│ ├── Admin/ # Admin pages
|
│ ├── Admin/ # Admin pages
|
||||||
│ │ ├── Calendar.php # Availability calendar page
|
│ │ ├── Calendar.php # Availability calendar page
|
||||||
│ │ └── Seasons.php # Seasons management page
|
│ │ └── Seasons.php # Seasons management page
|
||||||
|
│ ├── Blocks/ # Gutenberg blocks
|
||||||
|
│ │ └── BlockRegistrar.php # Block registration and rendering
|
||||||
│ ├── Booking/ # Booking system
|
│ ├── Booking/ # Booking system
|
||||||
│ │ ├── Availability.php # Availability checking
|
│ │ ├── Availability.php # Availability checking
|
||||||
│ │ └── EmailNotifier.php # Email notifications
|
│ │ └── EmailNotifier.php # Email notifications
|
||||||
|
│ ├── Frontend/ # Frontend components
|
||||||
|
│ │ ├── Search.php # Room search and AJAX handlers
|
||||||
|
│ │ ├── Shortcodes.php # All shortcode handlers
|
||||||
|
│ │ └── Widgets/ # WordPress widgets
|
||||||
|
│ │ ├── AvailabilityCalendar.php
|
||||||
|
│ │ ├── BuildingRooms.php
|
||||||
|
│ │ └── SimilarRooms.php
|
||||||
│ ├── License/
|
│ ├── License/
|
||||||
│ │ └── Manager.php # License management
|
│ │ └── Manager.php # License management
|
||||||
│ ├── PostTypes/ # Custom post types
|
│ ├── PostTypes/ # Custom post types
|
||||||
@@ -247,6 +260,8 @@ wp-bnb/
|
|||||||
│ │ ├── Calculator.php # Price calculation
|
│ │ ├── Calculator.php # Price calculation
|
||||||
│ │ ├── PricingTier.php # Pricing tier enum
|
│ │ ├── PricingTier.php # Pricing tier enum
|
||||||
│ │ └── Season.php # Seasonal pricing
|
│ │ └── Season.php # Seasonal pricing
|
||||||
|
│ ├── Integration/ # Third-party integrations
|
||||||
|
│ │ └── CF7.php # Contact Form 7 integration
|
||||||
│ └── Taxonomies/ # Custom taxonomies
|
│ └── Taxonomies/ # Custom taxonomies
|
||||||
│ ├── Amenity.php # Amenity taxonomy (tags)
|
│ ├── Amenity.php # Amenity taxonomy (tags)
|
||||||
│ └── RoomType.php # Room type taxonomy (categories)
|
│ └── RoomType.php # Room type taxonomy (categories)
|
||||||
@@ -256,10 +271,14 @@ wp-bnb/
|
|||||||
├── assets/
|
├── assets/
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
│ │ ├── admin.css # Admin styles
|
│ │ ├── admin.css # Admin styles
|
||||||
│ │ └── frontend.css # Frontend styles
|
│ │ ├── blocks-editor.css # Gutenberg editor styles
|
||||||
|
│ │ ├── cf7-integration.css # CF7 form styles
|
||||||
|
│ │ └── frontend.css # Frontend styles (~1250 lines)
|
||||||
│ └── js/
|
│ └── js/
|
||||||
│ ├── admin.js # Admin scripts
|
│ ├── admin.js # Admin scripts
|
||||||
│ └── frontend.js # Frontend scripts
|
│ ├── blocks-editor.js # Gutenberg editor scripts
|
||||||
|
│ ├── cf7-integration.js # CF7 form scripts
|
||||||
|
│ └── frontend.js # Frontend scripts (~825 lines)
|
||||||
├── templates/ # Twig templates (future)
|
├── templates/ # Twig templates (future)
|
||||||
├── languages/ # Translation files (future)
|
├── languages/ # Translation files (future)
|
||||||
└── releases/ # Release packages (git-ignored)
|
└── releases/ # Release packages (git-ignored)
|
||||||
@@ -323,7 +342,7 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Implemented license settings page with validation/activation buttons
|
- Implemented license settings page with validation/activation buttons
|
||||||
- Created admin CSS and JavaScript for license management
|
- Created admin CSS and JavaScript for license management
|
||||||
- Created Gitea CI/CD pipeline at `.gitea/workflows/release.yml`
|
- Created Gitea CI/CD pipeline at `.gitea/workflows/release.yml`
|
||||||
- Created `PLAN.md` with full implementation roadmap (8 phases)
|
- Created `PLAN.md` with full implementation roadmap (10 phases)
|
||||||
- Created `README.md` with user documentation
|
- Created `README.md` with user documentation
|
||||||
- Created `CHANGELOG.md` following Keep a Changelog format
|
- Created `CHANGELOG.md` following Keep a Changelog format
|
||||||
- Updated `CLAUDE.md` with architecture details
|
- Updated `CLAUDE.md` with architecture details
|
||||||
@@ -560,3 +579,232 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Same namespace classes can reference each other directly without use statements
|
- Same namespace classes can reference each other directly without use statements
|
||||||
- Services meta box renders before pricing meta box so services total is available
|
- Services meta box renders before pricing meta box so services total is available
|
||||||
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
|
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
|
||||||
|
|
||||||
|
### 2026-02-02 - Version 0.6.0 (Frontend Features)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Frontend/Search.php` class
|
||||||
|
- Room search with multiple filters: availability, capacity, room type, amenities, price range, building
|
||||||
|
- AJAX endpoints: `wp_bnb_search_rooms`, `wp_bnb_get_availability`, `wp_bnb_get_calendar`, `wp_bnb_calculate_price`
|
||||||
|
- Pagination support with configurable per_page
|
||||||
|
- Room data formatting for JSON responses with thumbnails, pricing, amenities
|
||||||
|
- Price range filtering using Calculator integration
|
||||||
|
- Availability filtering using Availability class
|
||||||
|
- Created `src/Frontend/Shortcodes.php` class
|
||||||
|
- `[bnb_buildings]` - Buildings list/grid with layout, columns, limit, orderby options
|
||||||
|
- `[bnb_rooms]` - Rooms list/grid with building, room_type, amenities filters
|
||||||
|
- `[bnb_room_search]` - Interactive search form with results container
|
||||||
|
- `[bnb_building id="X"]` - Single building display with rooms
|
||||||
|
- `[bnb_room id="X"]` - Single room display with availability form
|
||||||
|
- Grid system with 1-4 column support
|
||||||
|
- Sorting options: title, date, price, capacity
|
||||||
|
- Created `src/Frontend/Widgets/` directory with three widgets
|
||||||
|
- `SimilarRooms.php` - Shows rooms from same building/room type
|
||||||
|
- `BuildingRooms.php` - Lists all rooms in a building
|
||||||
|
- `AvailabilityCalendar.php` - Mini calendar with booking status
|
||||||
|
- All widgets extend `WP_Widget` with form/update/widget methods
|
||||||
|
- Auto-detection of current building/room from page context
|
||||||
|
- Created `src/Blocks/BlockRegistrar.php` class
|
||||||
|
- Five Gutenberg blocks: Building, Room, Room Search, Buildings List, Rooms List
|
||||||
|
- Server-side rendering using shortcode system
|
||||||
|
- Block editor assets (CSS/JS) enqueuing
|
||||||
|
- Block data localization with buildings, rooms, room types, amenities
|
||||||
|
- `render_callback` functions for each block type
|
||||||
|
- Created `assets/js/blocks-editor.js`
|
||||||
|
- Block registration using `wp.blocks.registerBlockType`
|
||||||
|
- InspectorControls for sidebar settings panels
|
||||||
|
- ServerSideRender for live preview in editor
|
||||||
|
- Attribute definitions matching shortcode parameters
|
||||||
|
- Created `assets/css/blocks-editor.css`
|
||||||
|
- Minimal editor styling for block placeholders
|
||||||
|
- Preview container styling
|
||||||
|
- Updated `assets/css/frontend.css` (~1250 lines)
|
||||||
|
- CSS custom properties for theming (colors, spacing, border-radius)
|
||||||
|
- Building and room card components
|
||||||
|
- Search form with field groups
|
||||||
|
- Results grid with responsive columns
|
||||||
|
- Calendar widget with availability states (available, booked, past, today)
|
||||||
|
- Legend styling
|
||||||
|
- Responsive breakpoints: 480px, 768px, 1024px
|
||||||
|
- Updated `assets/js/frontend.js` (~825 lines)
|
||||||
|
- `WpBnb` namespace with utility methods (ajax, formatDate, parseDate, debounce)
|
||||||
|
- `SearchForm` class: form submission, date validation, results rendering, load more
|
||||||
|
- `CalendarWidget` class: month navigation, AJAX calendar loading
|
||||||
|
- `AvailabilityForm` class: availability checking on single room pages
|
||||||
|
- `PriceCalculator` class: real-time price calculation with breakdown
|
||||||
|
- XSS-safe DOM construction using textContent instead of innerHTML
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added use statements for new frontend classes
|
||||||
|
- `init_frontend()` initializes Search, Shortcodes, BlockRegistrar
|
||||||
|
- `register_widgets()` method for widget registration
|
||||||
|
- `wp_localize_script()` adds AJAX URL, nonce, i18n strings to frontend
|
||||||
|
- Updated version to 0.6.0 in both plugin header and constant
|
||||||
|
- Updated CHANGELOG.md with comprehensive v0.6.0 release notes
|
||||||
|
- Updated PLAN.md to mark Phase 6 complete
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Server-side rendered Gutenberg blocks avoid complex build processes and ensure PHP/JS output consistency
|
||||||
|
- Shortcode system works well as render backend for blocks via `render_callback`
|
||||||
|
- Widget auto-detection from page context (`is_singular()`, `get_the_ID()`) reduces configuration
|
||||||
|
- CSS custom properties enable easy theming without modifying core styles
|
||||||
|
- AJAX nonce verification requires `wp_ajax_nopriv_` for non-logged-in users in frontend search
|
||||||
|
- Calendar data from `Availability::get_calendar_data()` provides consistent format for PHP and JS rendering
|
||||||
|
- XSS prevention in JS: use `textContent` for user data, `createElement` for structure
|
||||||
|
- Frontend components require license check (`LicenseManager::is_license_valid()`) before initialization
|
||||||
|
- Block editor requires separate script handle from frontend to avoid conflicts
|
||||||
|
|
||||||
|
**Released:**
|
||||||
|
|
||||||
|
- Committed: `864b8b2` on dev branch
|
||||||
|
- Merged to main (fast-forward)
|
||||||
|
- Tagged: `v0.6.0`
|
||||||
|
- Pushed to origin: dev, main, v0.6.0
|
||||||
|
|
||||||
|
### 2026-02-03 - Bug Fixes and Enhancements
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Fixed gap between settings page tabs and tab content
|
||||||
|
- Changed `.nav-tab-wrapper` margin-bottom from 20px to 0
|
||||||
|
- Added explicit border-bottom to create seamless connection with tab content
|
||||||
|
- Added license bypass for localhost development environments
|
||||||
|
- Created `LicenseManager::is_localhost()` method
|
||||||
|
- Detects: localhost, 127.0.0.1, ::1, .local/.test/.localhost/.dev/.ddev.site domains, private IP ranges
|
||||||
|
- `is_license_valid()` now returns true for localhost environments
|
||||||
|
- Added "Development Mode" notice on license settings page and dashboard when localhost detected
|
||||||
|
- Expanded General Settings with business owner fields
|
||||||
|
- Added Address section: street, city, postal code, country
|
||||||
|
- Added Contact section: email, phone, website
|
||||||
|
- Added Social Media section: Facebook, Instagram, X (Twitter), LinkedIn, TripAdvisor
|
||||||
|
- Updated `save_general_settings()` with proper sanitization for all new fields
|
||||||
|
- Created subtabs on Pricing settings tab
|
||||||
|
- Three subtabs: Pricing Tiers, Weekend Days, Seasons
|
||||||
|
- Each subtab has its own save button and focused content
|
||||||
|
- Added CSS for subtab navigation styling
|
||||||
|
- Seasons subtab now shows priority column and direct link to Seasons Manager
|
||||||
|
- Implemented auto-updates system
|
||||||
|
- Created `src/License/Updater.php` class
|
||||||
|
- Integrates with WordPress plugin update system via `pre_set_site_transient_update_plugins`
|
||||||
|
- Provides plugin info for "View details" modal via `plugins_api` filter
|
||||||
|
- Uses license client's `checkForUpdates()` method
|
||||||
|
- Configurable check frequency (1-168 hours)
|
||||||
|
- Options for notifications enabled and auto-install enabled
|
||||||
|
- Automatic cache clearing when license settings change or after updates
|
||||||
|
- Added Updates tab to settings page
|
||||||
|
- Enable/disable update notifications
|
||||||
|
- Enable/disable automatic updates
|
||||||
|
- Configurable update check frequency
|
||||||
|
- Manual "Check for Updates" button with AJAX
|
||||||
|
- Display of last check timestamp and current version
|
||||||
|
- Reordered admin submenu for better organization
|
||||||
|
- Dashboard at top, Settings at bottom
|
||||||
|
- Logical grouping: Buildings, Rooms, Bookings, Guests, Services, Calendar, Seasons
|
||||||
|
- Fixed Booking admin issues
|
||||||
|
- Fixed auto-draft status causing type errors (check for WP_Post object)
|
||||||
|
- Fixed guest dropdown to always load existing guests
|
||||||
|
- Booking title now auto-generates with guest name and dates (room removed per user request)
|
||||||
|
- Fixed booking history display on Guest edit page
|
||||||
|
- Implemented guest auto-creation from booking form
|
||||||
|
- When new guest data is entered in booking, guest record is automatically created
|
||||||
|
- Links booking to the new guest via guest_id meta
|
||||||
|
- Added encryption for sensitive guest data
|
||||||
|
- ID/passport numbers encrypted using AES-256-CBC
|
||||||
|
- Uses WordPress AUTH_KEY for encryption key derivation
|
||||||
|
- `encrypt()` and `decrypt()` methods in Guest class
|
||||||
|
- Backward compatible with legacy unencrypted data
|
||||||
|
- Security notice displayed in Identification meta box
|
||||||
|
- Disabled Gutenberg block editor for form-based post types
|
||||||
|
- Service, Guest, and Booking post types now use classic editor
|
||||||
|
- Added `disable_block_editor()` filter to each post type class
|
||||||
|
- Meta boxes now appear properly instead of being hidden at bottom
|
||||||
|
- Form-based interfaces are more appropriate than block editor for data entry
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `assets/css/admin.css` - Fixed tab gap, added subtab styles, booking form styles
|
||||||
|
- `assets/js/admin.js` - AJAX update check, booking form improvements, guest auto-creation
|
||||||
|
- `src/License/Manager.php` - Added `is_localhost()` method, updated `is_license_valid()`
|
||||||
|
- `src/License/Updater.php` - New file for auto-updates with configurable settings
|
||||||
|
- `src/Plugin.php` - Business owner settings, pricing subtabs, updates tab, menu reordering
|
||||||
|
- `src/PostTypes/Booking.php` - Auto-draft fixes, title generation, guest creation, disable Gutenberg
|
||||||
|
- `src/PostTypes/Guest.php` - AES-256-CBC encryption for ID numbers, disable Gutenberg
|
||||||
|
- `src/PostTypes/Service.php` - Disable Gutenberg for classic editor UI
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- WordPress nav-tab styling expects tabs and content to be flush (no margin/gap)
|
||||||
|
- Localhost detection should cover common development TLDs (.local, .test, .dev, .ddev.site)
|
||||||
|
- Private IP ranges can be detected using `FILTER_FLAG_NO_PRIV_RANGE`
|
||||||
|
- WordPress plugin updates require hooking into `pre_set_site_transient_update_plugins` and `plugins_api`
|
||||||
|
- Subtabs can be implemented with query parameters and conditional rendering within a single settings callback
|
||||||
|
- URL fields should use `esc_url_raw()` for sanitization, email fields use `sanitize_email()`
|
||||||
|
- Always check if post object is valid (`$post instanceof \WP_Post`) before accessing properties - auto-draft causes issues
|
||||||
|
- AES-256-CBC encryption with random IV provides secure storage for sensitive data
|
||||||
|
- Store IV concatenated with encrypted data (IV is not secret, just needs to be unique)
|
||||||
|
- `use_block_editor_for_post_type` filter disables Gutenberg per post type
|
||||||
|
- Post types with `show_in_rest => true` get Gutenberg by default, which hides traditional meta boxes
|
||||||
|
- Form-based admin interfaces (data entry) should use classic editor, not block editor
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.7.0 (Contact Form 7 Integration)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Integration/CF7.php` (~750 lines)
|
||||||
|
- Custom form tags: `[bnb_building_select]`, `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`, `[bnb_guests]`
|
||||||
|
- Server-side validation for all custom tags
|
||||||
|
- Availability validation in `wpcf7_before_send_mail` hook
|
||||||
|
- Automatic booking creation on form submission via `wpcf7_mail_sent`
|
||||||
|
- Guest record creation/linking using `find_or_create_guest()` pattern
|
||||||
|
- Custom mail tags: `[_bnb_room_name]`, `[_bnb_building_name]`, `[_bnb_calculated_price]`, `[_bnb_nights]`, `[_bnb_booking_reference]`
|
||||||
|
- Form type detection via CSS class `wp-bnb-booking-form`
|
||||||
|
- Created `assets/js/cf7-integration.js` (~230 lines)
|
||||||
|
- Building-based room filtering (rooms dropdown updates when building selected)
|
||||||
|
- Date validation (check-out after check-in, no past dates)
|
||||||
|
- Guest capacity validation against room limits
|
||||||
|
- AJAX availability checking with status display
|
||||||
|
- AJAX price calculation with formatted display
|
||||||
|
- Debounced updates to prevent excessive requests
|
||||||
|
- Created `assets/css/cf7-integration.css` (~200 lines)
|
||||||
|
- Two-column responsive form layout
|
||||||
|
- Availability status indicators (checking spinner, available checkmark, unavailable X)
|
||||||
|
- Price display formatting
|
||||||
|
- Capacity warning styling
|
||||||
|
- Dark mode support via `prefers-color-scheme`
|
||||||
|
- Print styles (hide interactive elements)
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added `use Magdev\WpBnb\Integration\CF7` import
|
||||||
|
- CF7 initialization in `init_frontend()` when WPCF7 class exists
|
||||||
|
- CF7 assets enqueuing with localized i18n strings
|
||||||
|
- Updated `README.md` with comprehensive CF7 documentation
|
||||||
|
- Custom form tags reference with options
|
||||||
|
- Example booking form template
|
||||||
|
- Example inquiry form template
|
||||||
|
- Custom mail tags documentation
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
|
||||||
|
- `src/Integration/CF7.php` - Main CF7 integration class
|
||||||
|
- `assets/js/cf7-integration.js` - Frontend JavaScript
|
||||||
|
- `assets/css/cf7-integration.css` - Form styling
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- CF7 custom tags registered via `wpcf7_add_form_tag()` with callback functions
|
||||||
|
- Validation filters follow pattern `wpcf7_validate_{tag_name}`
|
||||||
|
- `wpcf7_before_send_mail` can abort submission by setting `$abort` to true and adding validation error
|
||||||
|
- `wpcf7_mail_sent` fires after successful email, ideal for booking creation
|
||||||
|
- Custom mail tags via `wpcf7_special_mail_tags` filter receive submission data
|
||||||
|
- Form type detection by CSS class more reliable than checking for specific tags
|
||||||
|
- Room dropdown with `data-building` attributes enables client-side filtering
|
||||||
|
- AJAX endpoints reuse existing `wp_bnb_get_availability` and `wp_bnb_calculate_price` actions
|
||||||
|
- CF7 assets should depend on `contact-form-7` script/style handles
|
||||||
|
- Guest linking uses email as unique identifier for find-or-create pattern
|
||||||
|
|
||||||
|
**Released:**
|
||||||
|
|
||||||
|
- Committed: `28350aa` on dev branch
|
||||||
|
- Merged to main (fast-forward)
|
||||||
|
- Tagged: `v0.7.0`
|
||||||
|
- Pushed to origin: dev, main, v0.7.0
|
||||||
|
|||||||
80
PLAN.md
80
PLAN.md
@@ -116,11 +116,11 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Automatic price calculation
|
- [x] Automatic price calculation
|
||||||
- [x] Service summary display
|
- [x] Service summary display
|
||||||
|
|
||||||
## Phase 6: Frontend Features (v0.6.0)
|
## Phase 6: Frontend Features (v0.6.0) - Complete
|
||||||
|
|
||||||
### Search & Filtering
|
### Search & Filtering
|
||||||
|
|
||||||
- [ ] Room search with filters
|
- [x] Room search with filters
|
||||||
- Date range (availability)
|
- Date range (availability)
|
||||||
- Capacity
|
- Capacity
|
||||||
- Room type
|
- Room type
|
||||||
@@ -130,38 +130,39 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
|
|
||||||
### Display Components
|
### Display Components
|
||||||
|
|
||||||
- [ ] Building list/grid shortcode
|
- [x] Building list/grid shortcode
|
||||||
- [ ] Room list/grid shortcode
|
- [x] Room list/grid shortcode
|
||||||
- [ ] Room detail template
|
- [x] Room detail template
|
||||||
- [ ] Availability widget
|
- [x] Availability widget
|
||||||
|
|
||||||
### Gutenberg Blocks
|
### Gutenberg Blocks
|
||||||
|
|
||||||
- [ ] Building block
|
- [x] Building block
|
||||||
- [ ] Room block
|
- [x] Room block
|
||||||
- [ ] Room search block
|
- [x] Room search block
|
||||||
- [ ] Booking form block
|
- [x] Buildings list block
|
||||||
|
- [x] Rooms list block
|
||||||
|
|
||||||
### Widgets
|
### Widgets
|
||||||
|
|
||||||
- [ ] Similar rooms widget
|
- [x] Similar rooms widget
|
||||||
- [ ] Building rooms widget
|
- [x] Building rooms widget
|
||||||
- [ ] Availability calendar widget
|
- [x] Availability calendar widget
|
||||||
|
|
||||||
## Phase 7: Contact Form 7 Integration (v0.7.0)
|
## Phase 7: Contact Form 7 Integration (v0.7.0) - Complete
|
||||||
|
|
||||||
### Booking Request Form
|
### Booking Request Form
|
||||||
|
|
||||||
- [ ] Custom CF7 tags for rooms/dates
|
- [x] Custom CF7 tags for rooms/dates
|
||||||
- [ ] Form validation
|
- [x] Form validation
|
||||||
- [ ] Booking creation on submission
|
- [x] Booking creation on submission
|
||||||
- [ ] Email notifications
|
- [x] Email notifications
|
||||||
|
|
||||||
### Inquiry Form
|
### Inquiry Form
|
||||||
|
|
||||||
- [ ] General inquiry handling
|
- [x] General inquiry handling
|
||||||
- [ ] Room-specific inquiries
|
- [x] Room-specific inquiries
|
||||||
- [ ] Auto-response templates
|
- [x] Auto-response templates (uses default CF7 mail templates)
|
||||||
|
|
||||||
## Phase 8: Dashboard & Reports (v0.8.0)
|
## Phase 8: Dashboard & Reports (v0.8.0)
|
||||||
|
|
||||||
@@ -179,6 +180,17 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [ ] Guest statistics
|
- [ ] Guest statistics
|
||||||
- [ ] Export functionality (CSV, PDF)
|
- [ ] Export functionality (CSV, PDF)
|
||||||
|
|
||||||
|
## Phase 9: Prometheus Metrics (v0.9.0)
|
||||||
|
|
||||||
|
- [ ] Meanigful Metrics for this Plugin, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
|
||||||
|
- [ ] Example Grafana-Dashboard, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
|
||||||
|
- [ ] Update settings page to enable/disable metrics
|
||||||
|
|
||||||
|
## Phase 10: Security Audit (v0.10.0)
|
||||||
|
|
||||||
|
- [ ] Check for Wordpress best-practises
|
||||||
|
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
|
||||||
|
|
||||||
## Future Considerations (v1.0.0+)
|
## Future Considerations (v1.0.0+)
|
||||||
|
|
||||||
### WooCommerce Integration (Optional)
|
### WooCommerce Integration (Optional)
|
||||||
@@ -285,15 +297,17 @@ 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 | Complete |
|
| 0.1.0 | Data structures | Complete |
|
||||||
| 0.2.0 | Pricing | Complete |
|
| 0.2.0 | Pricing | Complete |
|
||||||
| 0.3.0 | Bookings | Complete |
|
| 0.3.0 | Bookings | Complete |
|
||||||
| 0.4.0 | Guests | Complete |
|
| 0.4.0 | Guests | Complete |
|
||||||
| 0.5.0 | Services | Complete |
|
| 0.5.0 | Services | Complete |
|
||||||
| 0.6.0 | Frontend | TBD |
|
| 0.6.0 | Frontend | Complete |
|
||||||
| 0.7.0 | CF7 Integration | TBD |
|
| 0.7.0 | CF7 Integration | Complete |
|
||||||
| 0.8.0 | Dashboard | TBD |
|
| 0.8.0 | Dashboard | TBD |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 0.9.0 | Prometheus Metrics | TBD |
|
||||||
|
| 0.10.0 | Security Audit | TBD |
|
||||||
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
212
README.md
212
README.md
@@ -10,17 +10,22 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
|
|
||||||
- **Multi-Property Support**: Manage multiple buildings, each with multiple rooms
|
- **Multi-Property Support**: Manage multiple buildings, each with multiple rooms
|
||||||
- **Flexible Pricing**: Configure short-term (nights), mid-term (weeks), and long-term (months) pricing
|
- **Flexible Pricing**: Configure short-term (nights), mid-term (weeks), and long-term (months) pricing
|
||||||
|
- **Seasonal Pricing**: Set price modifiers for high/low seasons
|
||||||
- **Booking Management**: Track reservations from inquiry to checkout
|
- **Booking Management**: Track reservations from inquiry to checkout
|
||||||
- **Guest Management**: Store guest information securely with GDPR compliance
|
- **Guest Management**: Store guest information securely with GDPR compliance
|
||||||
|
- **Data Encryption**: Sensitive guest data (ID/passport) encrypted at rest
|
||||||
- **Additional Services**: Offer extras like breakfast, parking, or tours
|
- **Additional Services**: Offer extras like breakfast, parking, or tours
|
||||||
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
|
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
|
||||||
- **Contact Form 7 Integration**: Accept booking requests through forms
|
- **Auto-Updates**: Automatic update checks and installation from license server
|
||||||
|
- **Development Mode**: License bypass for local development environments
|
||||||
|
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- WordPress 6.0 or higher
|
- WordPress 6.0 or higher
|
||||||
- PHP 8.3 or higher
|
- PHP 8.3 or higher
|
||||||
- Valid license key
|
- Valid license key
|
||||||
|
- Contact Form 7 (optional, for booking forms)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -44,6 +49,23 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
|
|
||||||
- **Business Name**: Your B&B business name
|
- **Business Name**: Your B&B business name
|
||||||
- **Currency**: Select your preferred currency (CHF, EUR, USD, GBP)
|
- **Currency**: Select your preferred currency (CHF, EUR, USD, GBP)
|
||||||
|
- **Business Address**: Street, city, postal code, country
|
||||||
|
- **Contact Information**: Email, phone, website
|
||||||
|
- **Social Media**: Facebook, Instagram, X (Twitter), LinkedIn, TripAdvisor
|
||||||
|
|
||||||
|
### Update Settings
|
||||||
|
|
||||||
|
- **Update Notifications**: Enable/disable update notifications in WordPress
|
||||||
|
- **Automatic Updates**: Enable/disable automatic plugin updates
|
||||||
|
- **Check Frequency**: How often to check for updates (1-168 hours)
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
The plugin automatically detects local development environments and bypasses license validation. Supported environments:
|
||||||
|
|
||||||
|
- localhost, 127.0.0.1, ::1
|
||||||
|
- Domains ending in .local, .test, .localhost, .dev, .ddev.site
|
||||||
|
- Private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -81,26 +103,192 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
Display buildings and rooms on your site using shortcodes:
|
Display buildings and rooms on your site using shortcodes:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
[wp_bnb_buildings]
|
[bnb_buildings] - List all buildings (grid/list layout)
|
||||||
[wp_bnb_rooms building="123"]
|
[bnb_rooms building="123"] - List rooms, optionally filtered by building
|
||||||
[wp_bnb_room_search]
|
[bnb_room_search] - Interactive room search form
|
||||||
|
[bnb_building id="123"] - Display a single building
|
||||||
|
[bnb_room id="456"] - Display a single room with availability
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Shortcode Attributes
|
||||||
|
|
||||||
|
**`[bnb_buildings]`** and **`[bnb_rooms]`**:
|
||||||
|
|
||||||
|
- `layout` - "grid" or "list" (default: grid)
|
||||||
|
- `columns` - 1-4 columns (default: 3)
|
||||||
|
- `limit` - Number of items (default: 12)
|
||||||
|
- `orderby` - title, date, price, capacity (default: title)
|
||||||
|
- `order` - ASC or DESC (default: ASC)
|
||||||
|
|
||||||
|
**`[bnb_rooms]`** additional attributes:
|
||||||
|
|
||||||
|
- `building` - Building ID to filter by
|
||||||
|
- `room_type` - Room type slug to filter by
|
||||||
|
- `amenities` - Comma-separated amenity slugs
|
||||||
|
|
||||||
## Gutenberg Blocks
|
## Gutenberg Blocks
|
||||||
|
|
||||||
The following blocks are available in the block editor:
|
The following blocks are available in the block editor:
|
||||||
|
|
||||||
- **Building** - Display a single building
|
- **Building** - Display a single building with details
|
||||||
- **Room** - Display a single room
|
- **Room** - Display a single room with availability form
|
||||||
- **Room Search** - Search and filter rooms
|
- **Room Search** - Interactive search form with filters
|
||||||
- **Booking Form** - Accept booking requests
|
- **Buildings List** - Display buildings grid/list
|
||||||
|
- **Rooms List** - Display rooms grid/list with filters
|
||||||
|
|
||||||
## Widgets
|
## Widgets
|
||||||
|
|
||||||
Available sidebar widgets:
|
Available sidebar widgets:
|
||||||
|
|
||||||
- **Similar Rooms** - Show rooms similar to the current one
|
- **Similar Rooms** - Show rooms from same building or room type
|
||||||
- **Building Rooms** - List all rooms in a building
|
- **Building Rooms** - List all rooms in a building
|
||||||
|
- **Availability Calendar** - Mini calendar showing booking status
|
||||||
|
|
||||||
|
## Contact Form 7 Integration
|
||||||
|
|
||||||
|
The plugin integrates with Contact Form 7 to accept booking requests and inquiries. Custom form tags are provided for room selection, date pickers, and guest counts.
|
||||||
|
|
||||||
|
### Custom Form Tags
|
||||||
|
|
||||||
|
Use these tags in your CF7 forms:
|
||||||
|
|
||||||
|
- `[bnb_building_select name]` - Building dropdown (optional filter for rooms)
|
||||||
|
- `[bnb_room_select* name]` - Room dropdown with capacity data
|
||||||
|
- `[bnb_date_checkin* name]` - Check-in date picker
|
||||||
|
- `[bnb_date_checkout* name]` - Check-out date picker
|
||||||
|
- `[bnb_guests* name]` - Guest count input
|
||||||
|
|
||||||
|
### Tag Options
|
||||||
|
|
||||||
|
**`[bnb_building_select]`**:
|
||||||
|
|
||||||
|
- `first_as_label:"text"` - Placeholder text (default: "All Locations")
|
||||||
|
|
||||||
|
**`[bnb_room_select]`**:
|
||||||
|
|
||||||
|
- `building_field:"name"` - Link to building field for filtering
|
||||||
|
- `first_as_label:"text"` - Placeholder text (default: "Select Room")
|
||||||
|
|
||||||
|
**`[bnb_guests]`**:
|
||||||
|
|
||||||
|
- `min:N` - Minimum guests (default: 1)
|
||||||
|
- `max:N` - Maximum guests (default: 10)
|
||||||
|
- `default:N` - Default value (default: 1)
|
||||||
|
|
||||||
|
### Example Booking Form
|
||||||
|
|
||||||
|
```txt
|
||||||
|
<div class="wp-bnb-booking-form">
|
||||||
|
<h3>Book Your Stay</h3>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
[bnb_building_select building first_as_label:"All Locations"]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
[bnb_room_select* room building_field:"building" first_as_label:"Select a Room"]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row-2col">
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>Check-in</label>
|
||||||
|
[bnb_date_checkin* check_in]
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>Check-out</label>
|
||||||
|
[bnb_date_checkout* check_out]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-availability-status"></div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Number of Guests</label>
|
||||||
|
[bnb_guests* guests min:1 max:10 default:2]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-price-display"></div>
|
||||||
|
|
||||||
|
<h4>Your Information</h4>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row-2col">
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>First Name</label>
|
||||||
|
[text* first_name]
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>Last Name</label>
|
||||||
|
[text* last_name]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Email</label>
|
||||||
|
[email* your_email]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Phone</label>
|
||||||
|
[tel your_phone]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Message</label>
|
||||||
|
[textarea your_message]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[submit "Request Booking"]
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Inquiry Form
|
||||||
|
|
||||||
|
For room-specific inquiries, add the `wp-bnb-inquiry-form` class:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
<div class="wp-bnb-inquiry-form">
|
||||||
|
<h3>Inquire About This Room</h3>
|
||||||
|
|
||||||
|
[hidden room default:123]
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Your Name</label>
|
||||||
|
[text* your_name]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Email</label>
|
||||||
|
[email* your_email]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Your Question</label>
|
||||||
|
[textarea* your_message]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[submit "Send Inquiry"]
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Features
|
||||||
|
|
||||||
|
- **Availability Checking**: Real-time AJAX validation shows room availability
|
||||||
|
- **Price Display**: Estimated total calculated and displayed automatically
|
||||||
|
- **Room Filtering**: Rooms filter by building selection
|
||||||
|
- **Date Validation**: Check-out must be after check-in, no past dates
|
||||||
|
- **Capacity Validation**: Guest count validated against room capacity
|
||||||
|
- **Automatic Booking**: Booking record created with "pending" status on submission
|
||||||
|
- **Guest Linking**: Guest records created or linked by email address
|
||||||
|
|
||||||
|
### Custom Mail Tags
|
||||||
|
|
||||||
|
Use these in your CF7 mail templates:
|
||||||
|
|
||||||
|
- `[_bnb_room_name]` - Room title
|
||||||
|
- `[_bnb_building_name]` - Building name
|
||||||
|
- `[_bnb_calculated_price]` - Formatted price
|
||||||
|
- `[_bnb_nights]` - Number of nights
|
||||||
|
- `[_bnb_booking_reference]` - Booking reference (after creation)
|
||||||
|
|
||||||
## Hooks and Filters
|
## Hooks and Filters
|
||||||
|
|
||||||
@@ -123,7 +311,7 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) {
|
|||||||
|
|
||||||
### Do I need a license to use this plugin?
|
### Do I need a license to use this plugin?
|
||||||
|
|
||||||
Yes, a valid license is required to use the frontend features. The admin functionality works without a license for evaluation purposes.
|
Yes, a valid license is required to use the frontend features in production. The admin functionality works without a license for evaluation purposes. Local development environments (localhost, .local, .test, .dev domains) automatically bypass license validation.
|
||||||
|
|
||||||
### Can I manage multiple properties?
|
### Can I manage multiple properties?
|
||||||
|
|
||||||
@@ -137,6 +325,10 @@ Yes, guest data can be exported and deleted on request, and consent is tracked a
|
|||||||
|
|
||||||
WooCommerce integration for payments is planned for a future release.
|
WooCommerce integration for payments is planned for a future release.
|
||||||
|
|
||||||
|
### How is guest data secured?
|
||||||
|
|
||||||
|
Sensitive guest data like passport/ID numbers are encrypted using AES-256-CBC encryption before storage. The encryption key is derived from your WordPress AUTH_KEY, ensuring data is secure at rest.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes.
|
See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes.
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
|
|
||||||
/* Settings Tabs */
|
/* Settings Tabs */
|
||||||
.nav-tab-wrapper {
|
.nav-tab-wrapper {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 0;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
@@ -64,6 +65,57 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings Subtabs */
|
||||||
|
.wp-bnb-subtabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #50575e;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab:last-child {
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #1d2327;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Form Tables */
|
/* Form Tables */
|
||||||
.form-table th {
|
.form-table th {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
86
assets/css/blocks-editor.css
Normal file
86
assets/css/blocks-editor.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* WP BnB Block Editor Styles
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Block placeholder styling */
|
||||||
|
.wp-bnb-block-placeholder {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server-side render container */
|
||||||
|
.wp-block-wp-bnb-building,
|
||||||
|
.wp-block-wp-bnb-room,
|
||||||
|
.wp-block-wp-bnb-room-search,
|
||||||
|
.wp-block-wp-bnb-buildings,
|
||||||
|
.wp-block-wp-bnb-rooms {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder in editor */
|
||||||
|
.wp-block-wp-bnb-building .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-room .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-room-search .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-buildings .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-rooms .components-placeholder {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner container */
|
||||||
|
.wp-block-wp-bnb-building .components-spinner,
|
||||||
|
.wp-block-wp-bnb-room .components-spinner,
|
||||||
|
.wp-block-wp-bnb-room-search .components-spinner,
|
||||||
|
.wp-block-wp-bnb-buildings .components-spinner,
|
||||||
|
.wp-block-wp-bnb-rooms .components-spinner {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inspector control sections */
|
||||||
|
.wp-block-wp-bnb-building .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-room .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-room-search .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-buildings .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-rooms .components-panel__body {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select control styling */
|
||||||
|
.wp-block-wp-bnb-building .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-room .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-room-search .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-buildings .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-rooms .components-select-control__input {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview container in editor */
|
||||||
|
.wp-bnb-editor-preview {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable interactive elements in preview */
|
||||||
|
.wp-bnb-editor-preview a,
|
||||||
|
.wp-bnb-editor-preview button,
|
||||||
|
.wp-bnb-editor-preview input,
|
||||||
|
.wp-bnb-editor-preview select {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add visual indicator that this is a preview */
|
||||||
|
.wp-bnb-editor-preview::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
344
assets/css/cf7-integration.css
Normal file
344
assets/css/cf7-integration.css
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
/**
|
||||||
|
* WP BnB Contact Form 7 Integration Styles
|
||||||
|
*
|
||||||
|
* Styling for CF7 booking forms.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Custom Properties */
|
||||||
|
:root {
|
||||||
|
--wp-bnb-cf7-primary: #2271b1;
|
||||||
|
--wp-bnb-cf7-success: #00a32a;
|
||||||
|
--wp-bnb-cf7-warning: #dba617;
|
||||||
|
--wp-bnb-cf7-error: #d63638;
|
||||||
|
--wp-bnb-cf7-text: #1d2327;
|
||||||
|
--wp-bnb-cf7-text-light: #646970;
|
||||||
|
--wp-bnb-cf7-border: #c3c4c7;
|
||||||
|
--wp-bnb-cf7-bg: #f0f0f1;
|
||||||
|
--wp-bnb-cf7-radius: 4px;
|
||||||
|
--wp-bnb-cf7-spacing: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Layout */
|
||||||
|
.wp-bnb-booking-form,
|
||||||
|
.wp-bnb-inquiry-form {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-booking-form h3,
|
||||||
|
.wp-bnb-booking-form h4,
|
||||||
|
.wp-bnb-inquiry-form h3,
|
||||||
|
.wp-bnb-inquiry-form h4 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
border-bottom: 1px solid var(--wp-bnb-cf7-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-booking-form h3:first-child,
|
||||||
|
.wp-bnb-inquiry-form h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Rows */
|
||||||
|
.wp-bnb-form-row {
|
||||||
|
margin-bottom: var(--wp-bnb-cf7-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-form-row-2col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--wp-bnb-cf7-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.wp-bnb-form-row-2col {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
.wp-bnb-form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-form-field label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--wp-bnb-cf7-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom CF7 Tags Styling */
|
||||||
|
.wp-bnb-building-select,
|
||||||
|
.wp-bnb-room-select,
|
||||||
|
.wp-bnb-date-checkin,
|
||||||
|
.wp-bnb-date-checkout,
|
||||||
|
.wp-bnb-guests {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 1px solid var(--wp-bnb-cf7-border);
|
||||||
|
border-radius: var(--wp-bnb-cf7-radius);
|
||||||
|
background-color: #fff;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-building-select:focus,
|
||||||
|
.wp-bnb-room-select:focus,
|
||||||
|
.wp-bnb-date-checkin:focus,
|
||||||
|
.wp-bnb-date-checkout:focus,
|
||||||
|
.wp-bnb-guests:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--wp-bnb-cf7-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select dropdown */
|
||||||
|
.wp-bnb-room-select optgroup {
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--wp-bnb-cf7-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date inputs */
|
||||||
|
.wp-bnb-date-checkin,
|
||||||
|
.wp-bnb-date-checkout {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Number input */
|
||||||
|
.wp-bnb-guests {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Availability Status */
|
||||||
|
.wp-bnb-availability-status {
|
||||||
|
padding: var(--wp-bnb-cf7-spacing);
|
||||||
|
margin: var(--wp-bnb-cf7-spacing) 0;
|
||||||
|
background-color: var(--wp-bnb-cf7-bg);
|
||||||
|
border-radius: var(--wp-bnb-cf7-radius);
|
||||||
|
text-align: center;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-availability-status:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-checking {
|
||||||
|
color: var(--wp-bnb-cf7-text-light);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-checking::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: 2px solid var(--wp-bnb-cf7-border);
|
||||||
|
border-top-color: var(--wp-bnb-cf7-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: wp-bnb-spin 0.8s linear infinite;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wp-bnb-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-available {
|
||||||
|
color: var(--wp-bnb-cf7-success);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-available::before {
|
||||||
|
content: "\2713";
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-unavailable {
|
||||||
|
color: var(--wp-bnb-cf7-error);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-unavailable::before {
|
||||||
|
content: "\2717";
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Display */
|
||||||
|
.wp-bnb-price-display {
|
||||||
|
padding: var(--wp-bnb-cf7-spacing);
|
||||||
|
margin: var(--wp-bnb-cf7-spacing) 0;
|
||||||
|
background-color: #e7f5ea;
|
||||||
|
border: 1px solid var(--wp-bnb-cf7-success);
|
||||||
|
border-radius: var(--wp-bnb-cf7-radius);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-price-display:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-price-label {
|
||||||
|
color: var(--wp-bnb-cf7-text-light);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-price-amount {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--wp-bnb-cf7-success);
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-nights {
|
||||||
|
color: var(--wp-bnb-cf7-text-light);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Capacity Warning */
|
||||||
|
.wp-bnb-capacity-warning {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--wp-bnb-cf7-error);
|
||||||
|
background-color: #fcf0f1;
|
||||||
|
border-radius: var(--wp-bnb-cf7-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation Errors */
|
||||||
|
.wpcf7-form-control-wrap .wpcf7-not-valid-tip {
|
||||||
|
color: var(--wp-bnb-cf7-error);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpcf7-form-control.wpcf7-not-valid {
|
||||||
|
border-color: var(--wp-bnb-cf7-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Response Messages */
|
||||||
|
.wpcf7 form.sent .wpcf7-response-output {
|
||||||
|
border-color: var(--wp-bnb-cf7-success);
|
||||||
|
background-color: #e7f5ea;
|
||||||
|
color: var(--wp-bnb-cf7-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpcf7 form.failed .wpcf7-response-output,
|
||||||
|
.wpcf7 form.aborted .wpcf7-response-output,
|
||||||
|
.wpcf7 form.spam .wpcf7-response-output,
|
||||||
|
.wpcf7 form.invalid .wpcf7-response-output {
|
||||||
|
border-color: var(--wp-bnb-cf7-error);
|
||||||
|
background-color: #fcf0f1;
|
||||||
|
color: var(--wp-bnb-cf7-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit Button */
|
||||||
|
.wp-bnb-booking-form .wpcf7-submit,
|
||||||
|
.wp-bnb-inquiry-form .wpcf7-submit {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--wp-bnb-cf7-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--wp-bnb-cf7-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-booking-form .wpcf7-submit:hover,
|
||||||
|
.wp-bnb-inquiry-form .wpcf7-submit:hover {
|
||||||
|
background-color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-booking-form .wpcf7-submit:disabled,
|
||||||
|
.wp-bnb-inquiry-form .wpcf7-submit:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.wpcf7 .wpcf7-spinner {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hidden Room Field (for inquiry forms) */
|
||||||
|
.wp-bnb-inquiry-form input[type="hidden"] + .wpcf7-form-control-wrap {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Section Headers */
|
||||||
|
.wp-bnb-booking-form hr,
|
||||||
|
.wp-bnb-inquiry-form hr {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--wp-bnb-cf7-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--wp-bnb-cf7-text: #f0f0f1;
|
||||||
|
--wp-bnb-cf7-text-light: #a7aaad;
|
||||||
|
--wp-bnb-cf7-border: #50575e;
|
||||||
|
--wp-bnb-cf7-bg: #2c3338;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-building-select,
|
||||||
|
.wp-bnb-room-select,
|
||||||
|
.wp-bnb-date-checkin,
|
||||||
|
.wp-bnb-date-checkout,
|
||||||
|
.wp-bnb-guests {
|
||||||
|
background-color: #3c434a;
|
||||||
|
color: var(--wp-bnb-cf7-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-price-display {
|
||||||
|
background-color: #1a3320;
|
||||||
|
border-color: var(--wp-bnb-cf7-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-capacity-warning {
|
||||||
|
background-color: #3c1618;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpcf7 form.sent .wpcf7-response-output {
|
||||||
|
background-color: #1a3320;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpcf7 form.failed .wpcf7-response-output,
|
||||||
|
.wpcf7 form.aborted .wpcf7-response-output,
|
||||||
|
.wpcf7 form.spam .wpcf7-response-output,
|
||||||
|
.wpcf7 form.invalid .wpcf7-response-output {
|
||||||
|
background-color: #3c1618;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.wp-bnb-availability-status,
|
||||||
|
.wp-bnb-price-display,
|
||||||
|
.wpcf7-submit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -91,6 +91,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize update check functionality.
|
||||||
|
*/
|
||||||
|
function initUpdateCheck() {
|
||||||
|
var $checkBtn = $('#wp-bnb-check-updates');
|
||||||
|
var $spinner = $('#wp-bnb-update-spinner');
|
||||||
|
var $message = $('#wp-bnb-update-message');
|
||||||
|
var $latestVersion = $('#wp-bnb-latest-version');
|
||||||
|
var $lastCheck = $('#wp-bnb-update-last-check');
|
||||||
|
|
||||||
|
if (!$checkBtn.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkBtn.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Disable button and show spinner.
|
||||||
|
$checkBtn.prop('disabled', true);
|
||||||
|
$spinner.addClass('is-active');
|
||||||
|
$message.hide();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: wpBnbAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_check_updates',
|
||||||
|
nonce: wpBnbAdmin.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
$checkBtn.prop('disabled', false);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
|
||||||
|
// Update last check time.
|
||||||
|
$lastCheck.text(wpBnbAdmin.i18n.justNow || 'Just now');
|
||||||
|
|
||||||
|
// Update version display.
|
||||||
|
if (data.update_available) {
|
||||||
|
$latestVersion.html(
|
||||||
|
'<span style="color: #00a32a; font-weight: 600;">' +
|
||||||
|
data.latest_version +
|
||||||
|
'</span> ' +
|
||||||
|
'<span class="dashicons dashicons-yes" style="color: #00a32a;"></span> ' +
|
||||||
|
'<em>' + (wpBnbAdmin.i18n.updateAvailable || 'Update available!') + '</em>'
|
||||||
|
);
|
||||||
|
showUpdateMessage('success', data.message);
|
||||||
|
} else {
|
||||||
|
$latestVersion.html(
|
||||||
|
data.latest_version +
|
||||||
|
' <span style="color: #646970;">' +
|
||||||
|
(wpBnbAdmin.i18n.upToDate || '(You are up to date)') +
|
||||||
|
'</span>'
|
||||||
|
);
|
||||||
|
showUpdateMessage('success', data.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showUpdateMessage('error', response.data.message || wpBnbAdmin.i18n.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
$checkBtn.prop('disabled', false);
|
||||||
|
showUpdateMessage('error', wpBnbAdmin.i18n.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an update message.
|
||||||
|
*
|
||||||
|
* @param {string} type Message type (success or error).
|
||||||
|
* @param {string} message Message text.
|
||||||
|
*/
|
||||||
|
function showUpdateMessage(type, message) {
|
||||||
|
$message
|
||||||
|
.removeClass('success error')
|
||||||
|
.addClass(type)
|
||||||
|
.text(message)
|
||||||
|
.fadeIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize room gallery functionality.
|
* Initialize room gallery functionality.
|
||||||
*/
|
*/
|
||||||
@@ -768,7 +853,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pricingTypeInputs.on('change', function() {
|
function updatePriceRowVisibility() {
|
||||||
var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val();
|
var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val();
|
||||||
|
|
||||||
if (pricingType === 'included') {
|
if (pricingType === 'included') {
|
||||||
@@ -784,7 +869,12 @@
|
|||||||
$priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.');
|
$priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
$pricingTypeInputs.on('change', updatePriceRowVisibility);
|
||||||
|
|
||||||
|
// Set initial visibility state on page load.
|
||||||
|
updatePriceRowVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -937,6 +1027,7 @@
|
|||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
|
initUpdateCheck();
|
||||||
initRoomGallery();
|
initRoomGallery();
|
||||||
initPricingSettings();
|
initPricingSettings();
|
||||||
initSeasonForm();
|
initSeasonForm();
|
||||||
|
|||||||
489
assets/js/blocks-editor.js
Normal file
489
assets/js/blocks-editor.js
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
/**
|
||||||
|
* WP BnB Gutenberg Blocks
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function(wp) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { registerBlockType } = wp.blocks;
|
||||||
|
const { createElement, Fragment } = wp.element;
|
||||||
|
const { InspectorControls, useBlockProps } = wp.blockEditor;
|
||||||
|
const { PanelBody, SelectControl, ToggleControl, RangeControl, Placeholder, Spinner } = wp.components;
|
||||||
|
const { ServerSideRender } = wp.editor || wp.serverSideRender;
|
||||||
|
const { __ } = wp.i18n;
|
||||||
|
const el = createElement;
|
||||||
|
|
||||||
|
// Get localized data
|
||||||
|
const { buildings, rooms, roomTypes, i18n } = wpBnbBlocks;
|
||||||
|
|
||||||
|
// Building options for select
|
||||||
|
const buildingOptions = [
|
||||||
|
{ value: 0, label: i18n.selectBuilding },
|
||||||
|
...buildings
|
||||||
|
];
|
||||||
|
|
||||||
|
// Room options for select
|
||||||
|
const roomOptions = [
|
||||||
|
{ value: 0, label: i18n.selectRoom },
|
||||||
|
...rooms.map(r => ({
|
||||||
|
value: r.value,
|
||||||
|
label: r.building ? `${r.label} (${r.building})` : r.label
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
// Room type options
|
||||||
|
const roomTypeOptions = [
|
||||||
|
{ value: '', label: i18n.allTypes },
|
||||||
|
...roomTypes.map(t => ({
|
||||||
|
value: t.slug,
|
||||||
|
label: t.name
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
// Building filter options for rooms block
|
||||||
|
const buildingFilterOptions = [
|
||||||
|
{ value: 0, label: i18n.allBuildings },
|
||||||
|
...buildings
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/building', {
|
||||||
|
title: i18n.buildingBlock,
|
||||||
|
icon: 'building',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
buildingId: { type: 'number', default: 0 },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showAddress: { type: 'boolean', default: true },
|
||||||
|
showRooms: { type: 'boolean', default: true },
|
||||||
|
showContact: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.buildingBlock,
|
||||||
|
value: attributes.buildingId,
|
||||||
|
options: buildingOptions,
|
||||||
|
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAddress,
|
||||||
|
checked: attributes.showAddress,
|
||||||
|
onChange: (value) => setAttributes({ showAddress: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showRooms,
|
||||||
|
checked: attributes.showRooms,
|
||||||
|
onChange: (value) => setAttributes({ showRooms: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showContact,
|
||||||
|
checked: attributes.showContact,
|
||||||
|
onChange: (value) => setAttributes({ showContact: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
attributes.buildingId ?
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/building',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'building', label: i18n.buildingBlock }, el(Spinner))
|
||||||
|
}) :
|
||||||
|
el(Placeholder, { icon: 'building', label: i18n.buildingBlock },
|
||||||
|
buildings.length === 0 ?
|
||||||
|
el('p', {}, i18n.noBuildings) :
|
||||||
|
el(SelectControl, {
|
||||||
|
value: attributes.buildingId,
|
||||||
|
options: buildingOptions,
|
||||||
|
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null; // Server-side rendered
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/room', {
|
||||||
|
title: i18n.roomBlock,
|
||||||
|
icon: 'admin-home',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
roomId: { type: 'number', default: 0 },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showGallery: { type: 'boolean', default: true },
|
||||||
|
showPrice: { type: 'boolean', default: true },
|
||||||
|
showAmenities: { type: 'boolean', default: true },
|
||||||
|
showAvailability: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.roomBlock,
|
||||||
|
value: attributes.roomId,
|
||||||
|
options: roomOptions,
|
||||||
|
onChange: (value) => setAttributes({ roomId: parseInt(value, 10) })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showGallery,
|
||||||
|
checked: attributes.showGallery,
|
||||||
|
onChange: (value) => setAttributes({ showGallery: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showPrice,
|
||||||
|
checked: attributes.showPrice,
|
||||||
|
onChange: (value) => setAttributes({ showPrice: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAmenities,
|
||||||
|
checked: attributes.showAmenities,
|
||||||
|
onChange: (value) => setAttributes({ showAmenities: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAvailability,
|
||||||
|
checked: attributes.showAvailability,
|
||||||
|
onChange: (value) => setAttributes({ showAvailability: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
attributes.roomId ?
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/room',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'admin-home', label: i18n.roomBlock }, el(Spinner))
|
||||||
|
}) :
|
||||||
|
el(Placeholder, { icon: 'admin-home', label: i18n.roomBlock },
|
||||||
|
rooms.length === 0 ?
|
||||||
|
el('p', {}, i18n.noRooms) :
|
||||||
|
el(SelectControl, {
|
||||||
|
value: attributes.roomId,
|
||||||
|
options: roomOptions,
|
||||||
|
onChange: (value) => setAttributes({ roomId: parseInt(value, 10) })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room Search Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/room-search', {
|
||||||
|
title: i18n.roomSearchBlock,
|
||||||
|
icon: 'search',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
layout: { type: 'string', default: 'grid' },
|
||||||
|
columns: { type: 'number', default: 3 },
|
||||||
|
showDates: { type: 'boolean', default: true },
|
||||||
|
showGuests: { type: 'boolean', default: true },
|
||||||
|
showRoomType: { type: 'boolean', default: true },
|
||||||
|
showAmenities: { type: 'boolean', default: true },
|
||||||
|
showPriceRange: { type: 'boolean', default: true },
|
||||||
|
showBuilding: { type: 'boolean', default: true },
|
||||||
|
resultsPerPage: { type: 'number', default: 12 }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.layout,
|
||||||
|
value: attributes.layout,
|
||||||
|
options: [
|
||||||
|
{ value: 'grid', label: i18n.grid },
|
||||||
|
{ value: 'list', label: i18n.list }
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value })
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.columns,
|
||||||
|
value: attributes.columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 4
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.resultsPerPage,
|
||||||
|
value: attributes.resultsPerPage,
|
||||||
|
onChange: (value) => setAttributes({ resultsPerPage: value }),
|
||||||
|
min: 4,
|
||||||
|
max: 48
|
||||||
|
})
|
||||||
|
),
|
||||||
|
el(PanelBody, { title: i18n.filterSettings, initialOpen: false },
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showDates,
|
||||||
|
checked: attributes.showDates,
|
||||||
|
onChange: (value) => setAttributes({ showDates: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showGuests,
|
||||||
|
checked: attributes.showGuests,
|
||||||
|
onChange: (value) => setAttributes({ showGuests: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showRoomType,
|
||||||
|
checked: attributes.showRoomType,
|
||||||
|
onChange: (value) => setAttributes({ showRoomType: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAmenities,
|
||||||
|
checked: attributes.showAmenities,
|
||||||
|
onChange: (value) => setAttributes({ showAmenities: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showPriceRange,
|
||||||
|
checked: attributes.showPriceRange,
|
||||||
|
onChange: (value) => setAttributes({ showPriceRange: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showBuilding,
|
||||||
|
checked: attributes.showBuilding,
|
||||||
|
onChange: (value) => setAttributes({ showBuilding: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/room-search',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'search', label: i18n.roomSearchBlock }, el(Spinner))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buildings List Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/buildings', {
|
||||||
|
title: i18n.buildingsBlock,
|
||||||
|
icon: 'building',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
layout: { type: 'string', default: 'grid' },
|
||||||
|
columns: { type: 'number', default: 3 },
|
||||||
|
limit: { type: 'number', default: -1 },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showAddress: { type: 'boolean', default: true },
|
||||||
|
showRoomsCount: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.layout,
|
||||||
|
value: attributes.layout,
|
||||||
|
options: [
|
||||||
|
{ value: 'grid', label: i18n.grid },
|
||||||
|
{ value: 'list', label: i18n.list }
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value })
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.columns,
|
||||||
|
value: attributes.columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 4
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.limit,
|
||||||
|
value: attributes.limit,
|
||||||
|
onChange: (value) => setAttributes({ limit: value }),
|
||||||
|
min: -1,
|
||||||
|
max: 20
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAddress,
|
||||||
|
checked: attributes.showAddress,
|
||||||
|
onChange: (value) => setAttributes({ showAddress: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showRoomsCount,
|
||||||
|
checked: attributes.showRoomsCount,
|
||||||
|
onChange: (value) => setAttributes({ showRoomsCount: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/buildings',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'building', label: i18n.buildingsBlock }, el(Spinner))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms List Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/rooms', {
|
||||||
|
title: i18n.roomsBlock,
|
||||||
|
icon: 'admin-home',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
layout: { type: 'string', default: 'grid' },
|
||||||
|
columns: { type: 'number', default: 3 },
|
||||||
|
limit: { type: 'number', default: 12 },
|
||||||
|
buildingId: { type: 'number', default: 0 },
|
||||||
|
roomType: { type: 'string', default: '' },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showPrice: { type: 'boolean', default: true },
|
||||||
|
showCapacity: { type: 'boolean', default: true },
|
||||||
|
showAmenities: { type: 'boolean', default: true },
|
||||||
|
showBuilding: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.layout,
|
||||||
|
value: attributes.layout,
|
||||||
|
options: [
|
||||||
|
{ value: 'grid', label: i18n.grid },
|
||||||
|
{ value: 'list', label: i18n.list }
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value })
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.columns,
|
||||||
|
value: attributes.columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 4
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.limit,
|
||||||
|
value: attributes.limit,
|
||||||
|
onChange: (value) => setAttributes({ limit: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 48
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showPrice,
|
||||||
|
checked: attributes.showPrice,
|
||||||
|
onChange: (value) => setAttributes({ showPrice: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showCapacity,
|
||||||
|
checked: attributes.showCapacity,
|
||||||
|
onChange: (value) => setAttributes({ showCapacity: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAmenities,
|
||||||
|
checked: attributes.showAmenities,
|
||||||
|
onChange: (value) => setAttributes({ showAmenities: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showBuilding,
|
||||||
|
checked: attributes.showBuilding,
|
||||||
|
onChange: (value) => setAttributes({ showBuilding: value })
|
||||||
|
})
|
||||||
|
),
|
||||||
|
el(PanelBody, { title: i18n.filterSettings, initialOpen: false },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.buildingBlock,
|
||||||
|
value: attributes.buildingId,
|
||||||
|
options: buildingFilterOptions,
|
||||||
|
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
|
||||||
|
}),
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.roomType,
|
||||||
|
value: attributes.roomType,
|
||||||
|
options: roomTypeOptions,
|
||||||
|
onChange: (value) => setAttributes({ roomType: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/rooms',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'admin-home', label: i18n.roomsBlock }, el(Spinner))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window.wp);
|
||||||
375
assets/js/cf7-integration.js
Normal file
375
assets/js/cf7-integration.js
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
/**
|
||||||
|
* WP BnB Contact Form 7 Integration
|
||||||
|
*
|
||||||
|
* Handles dynamic form behavior for booking forms.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WpBnbCF7 = {
|
||||||
|
config: window.wpBnbCF7 || {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all CF7 integration features.
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.initBuildingRoomFilter();
|
||||||
|
this.initDateValidation();
|
||||||
|
this.initCapacityValidation();
|
||||||
|
this.initAvailabilityCheck();
|
||||||
|
this.initPriceDisplay();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rooms dropdown when building is selected.
|
||||||
|
*/
|
||||||
|
initBuildingRoomFilter: function() {
|
||||||
|
document.querySelectorAll('[data-bnb-building-select]').forEach(function(buildingSelect) {
|
||||||
|
const form = buildingSelect.closest('form');
|
||||||
|
const roomSelect = form ? form.querySelector('[data-bnb-room-select]') : null;
|
||||||
|
|
||||||
|
if (!roomSelect) return;
|
||||||
|
|
||||||
|
// Store all options for filtering
|
||||||
|
const allOptions = Array.from(roomSelect.querySelectorAll('option, optgroup'));
|
||||||
|
const originalHTML = roomSelect.innerHTML;
|
||||||
|
|
||||||
|
buildingSelect.addEventListener('change', function() {
|
||||||
|
const selectedBuilding = buildingSelect.value;
|
||||||
|
|
||||||
|
// Show all options if no building selected
|
||||||
|
if (!selectedBuilding) {
|
||||||
|
roomSelect.innerHTML = originalHTML;
|
||||||
|
roomSelect.dispatchEvent(new Event('change'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter options by building
|
||||||
|
roomSelect.innerHTML = '';
|
||||||
|
|
||||||
|
// Add placeholder option
|
||||||
|
const placeholder = document.createElement('option');
|
||||||
|
placeholder.value = '';
|
||||||
|
placeholder.textContent = WpBnbCF7.config.i18n?.selectRoom || '-- Select Room --';
|
||||||
|
roomSelect.appendChild(placeholder);
|
||||||
|
|
||||||
|
allOptions.forEach(function(el) {
|
||||||
|
if (el.tagName === 'OPTGROUP') {
|
||||||
|
// Check if any options in this optgroup match
|
||||||
|
const matchingOptions = Array.from(el.querySelectorAll('option')).filter(function(opt) {
|
||||||
|
return opt.dataset.building === selectedBuilding;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingOptions.length > 0) {
|
||||||
|
const clonedGroup = el.cloneNode(false);
|
||||||
|
matchingOptions.forEach(function(opt) {
|
||||||
|
clonedGroup.appendChild(opt.cloneNode(true));
|
||||||
|
});
|
||||||
|
roomSelect.appendChild(clonedGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger change to update dependent fields
|
||||||
|
roomSelect.dispatchEvent(new Event('change'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and link check-in/check-out dates.
|
||||||
|
*/
|
||||||
|
initDateValidation: function() {
|
||||||
|
document.querySelectorAll('[data-bnb-checkin]').forEach(function(checkinInput) {
|
||||||
|
const form = checkinInput.closest('form');
|
||||||
|
const checkoutInput = form ? form.querySelector('[data-bnb-checkout]') : null;
|
||||||
|
|
||||||
|
if (!checkoutInput) return;
|
||||||
|
|
||||||
|
// Set minimum check-in to today
|
||||||
|
const today = WpBnbCF7.formatDate(new Date());
|
||||||
|
if (!checkinInput.getAttribute('min') || checkinInput.getAttribute('min') < today) {
|
||||||
|
checkinInput.setAttribute('min', today);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkinInput.addEventListener('change', function() {
|
||||||
|
if (checkinInput.value) {
|
||||||
|
// Set checkout minimum to checkin + 1 day
|
||||||
|
const minCheckout = new Date(checkinInput.value);
|
||||||
|
minCheckout.setDate(minCheckout.getDate() + 1);
|
||||||
|
checkoutInput.setAttribute('min', WpBnbCF7.formatDate(minCheckout));
|
||||||
|
|
||||||
|
// Clear checkout if it's now invalid
|
||||||
|
if (checkoutInput.value && checkoutInput.value <= checkinInput.value) {
|
||||||
|
checkoutInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger availability check
|
||||||
|
WpBnbCF7.triggerAvailabilityCheck(form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkoutInput.addEventListener('change', function() {
|
||||||
|
if (checkoutInput.value && checkinInput.value) {
|
||||||
|
if (checkoutInput.value <= checkinInput.value) {
|
||||||
|
alert(WpBnbCF7.config.i18n?.invalidDateRange || 'Check-out must be after check-in');
|
||||||
|
checkoutInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger availability check
|
||||||
|
WpBnbCF7.triggerAvailabilityCheck(form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate guest count against room capacity.
|
||||||
|
*/
|
||||||
|
initCapacityValidation: function() {
|
||||||
|
document.querySelectorAll('[data-bnb-guests]').forEach(function(guestsInput) {
|
||||||
|
const form = guestsInput.closest('form');
|
||||||
|
const roomSelect = form ? form.querySelector('[data-bnb-room-select]') : null;
|
||||||
|
|
||||||
|
if (!roomSelect) return;
|
||||||
|
|
||||||
|
const validateCapacity = function() {
|
||||||
|
const selectedOption = roomSelect.selectedOptions[0];
|
||||||
|
const capacity = parseInt(selectedOption?.dataset.capacity || 99, 10);
|
||||||
|
const guests = parseInt(guestsInput.value || 0, 10);
|
||||||
|
|
||||||
|
// Update max attribute
|
||||||
|
guestsInput.setAttribute('max', capacity);
|
||||||
|
|
||||||
|
// Show warning if over capacity
|
||||||
|
const wrapper = guestsInput.closest('.wpcf7-form-control-wrap');
|
||||||
|
let warning = wrapper ? wrapper.querySelector('.wp-bnb-capacity-warning') : null;
|
||||||
|
|
||||||
|
if (guests > capacity) {
|
||||||
|
if (!warning && wrapper) {
|
||||||
|
warning = document.createElement('span');
|
||||||
|
warning.className = 'wp-bnb-capacity-warning';
|
||||||
|
wrapper.appendChild(warning);
|
||||||
|
}
|
||||||
|
if (warning) {
|
||||||
|
warning.textContent = (WpBnbCF7.config.i18n?.capacityExceeded || 'Maximum %d guests for this room').replace('%d', capacity);
|
||||||
|
}
|
||||||
|
} else if (warning) {
|
||||||
|
warning.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
roomSelect.addEventListener('change', validateCapacity);
|
||||||
|
guestsInput.addEventListener('change', validateCapacity);
|
||||||
|
guestsInput.addEventListener('input', validateCapacity);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize AJAX availability checking.
|
||||||
|
*/
|
||||||
|
initAvailabilityCheck: function() {
|
||||||
|
// Find forms with availability display
|
||||||
|
document.querySelectorAll('.wp-bnb-availability-status').forEach(function(statusEl) {
|
||||||
|
const form = statusEl.closest('form');
|
||||||
|
if (form) {
|
||||||
|
form._availabilityStatus = statusEl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger availability check for a form.
|
||||||
|
*
|
||||||
|
* @param {HTMLFormElement} form Form element.
|
||||||
|
*/
|
||||||
|
triggerAvailabilityCheck: function(form) {
|
||||||
|
const roomSelect = form.querySelector('[data-bnb-room-select]');
|
||||||
|
const checkinInput = form.querySelector('[data-bnb-checkin]');
|
||||||
|
const checkoutInput = form.querySelector('[data-bnb-checkout]');
|
||||||
|
const statusEl = form._availabilityStatus || form.querySelector('.wp-bnb-availability-status');
|
||||||
|
const priceEl = form.querySelector('.wp-bnb-price-display');
|
||||||
|
|
||||||
|
if (!roomSelect || !checkinInput || !checkoutInput) return;
|
||||||
|
|
||||||
|
const roomId = roomSelect.value;
|
||||||
|
const checkIn = checkinInput.value;
|
||||||
|
const checkOut = checkoutInput.value;
|
||||||
|
|
||||||
|
if (!roomId || !checkIn || !checkOut) {
|
||||||
|
if (statusEl) statusEl.innerHTML = '';
|
||||||
|
if (priceEl) priceEl.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = '<span class="wp-bnb-checking">' + (WpBnbCF7.config.i18n?.checking || 'Checking availability...') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make AJAX request
|
||||||
|
WpBnbCF7.ajax('wp_bnb_get_availability', {
|
||||||
|
room_id: roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (statusEl) {
|
||||||
|
if (response.available) {
|
||||||
|
let html = '<span class="wp-bnb-available">' + (WpBnbCF7.config.i18n?.available || 'Room is available!') + '</span>';
|
||||||
|
statusEl.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
statusEl.innerHTML = '<span class="wp-bnb-unavailable">' + (WpBnbCF7.config.i18n?.unavailable || 'Room is not available for these dates') + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update price display
|
||||||
|
if (priceEl && response.available && response.price_formatted) {
|
||||||
|
priceEl.innerHTML = '<span class="wp-bnb-price-label">' + (WpBnbCF7.config.i18n?.estimatedTotal || 'Estimated Total') + ':</span> ' +
|
||||||
|
'<span class="wp-bnb-price-amount">' + response.price_formatted + '</span> ' +
|
||||||
|
'<span class="wp-bnb-nights">(' + response.nights + ' ' + (WpBnbCF7.config.i18n?.nights || 'nights') + ')</span>';
|
||||||
|
} else if (priceEl) {
|
||||||
|
priceEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Availability check failed:', error);
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize price display updates.
|
||||||
|
*/
|
||||||
|
initPriceDisplay: function() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
document.querySelectorAll('.wp-bnb-price-display').forEach(function(priceEl) {
|
||||||
|
const form = priceEl.closest('form');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
const updatePrice = self.debounce(function() {
|
||||||
|
const roomSelect = form.querySelector('[data-bnb-room-select]');
|
||||||
|
const checkinInput = form.querySelector('[data-bnb-checkin]');
|
||||||
|
const checkoutInput = form.querySelector('[data-bnb-checkout]');
|
||||||
|
|
||||||
|
if (!roomSelect?.value || !checkinInput?.value || !checkoutInput?.value) {
|
||||||
|
priceEl.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ajax('wp_bnb_calculate_price', {
|
||||||
|
room_id: roomSelect.value,
|
||||||
|
check_in: checkinInput.value,
|
||||||
|
check_out: checkoutInput.value
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
priceEl.innerHTML = '<span class="wp-bnb-price-label">' + (self.config.i18n?.estimatedTotal || 'Estimated Total') + ':</span> ' +
|
||||||
|
'<span class="wp-bnb-price-amount">' + response.price_formatted + '</span> ' +
|
||||||
|
'<span class="wp-bnb-nights">(' + response.nights + ' ' + (self.config.i18n?.nights || 'nights') + ')</span>';
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
priceEl.innerHTML = '';
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Bind to relevant field changes
|
||||||
|
form.querySelectorAll('[data-bnb-room-select], [data-bnb-checkin], [data-bnb-checkout]')
|
||||||
|
.forEach(function(input) {
|
||||||
|
input.addEventListener('change', updatePrice);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make AJAX request.
|
||||||
|
*
|
||||||
|
* @param {string} action AJAX action name.
|
||||||
|
* @param {object} data Request data.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
ajax: function(action, data) {
|
||||||
|
data = data || {};
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', action);
|
||||||
|
formData.append('nonce', this.config.nonce || '');
|
||||||
|
|
||||||
|
Object.keys(data).forEach(function(key) {
|
||||||
|
if (data[key] !== null && data[key] !== undefined) {
|
||||||
|
formData.append(key, data[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetch(this.config.ajaxUrl || '/wp-admin/admin-ajax.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'same-origin'
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(responseData) {
|
||||||
|
if (!responseData.success) {
|
||||||
|
throw new Error(responseData.data?.message || 'Request failed');
|
||||||
|
}
|
||||||
|
return responseData.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date as YYYY-MM-DD.
|
||||||
|
*
|
||||||
|
* @param {Date} date Date object.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
formatDate: function(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return year + '-' + month + '-' + day;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function.
|
||||||
|
*
|
||||||
|
* @param {function} func Function to debounce.
|
||||||
|
* @param {number} wait Milliseconds to wait.
|
||||||
|
* @return {function}
|
||||||
|
*/
|
||||||
|
debounce: function(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function() {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(function() {
|
||||||
|
func.apply(context, args);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize on DOM ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
WpBnbCF7.init();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
WpBnbCF7.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-initialize on CF7 form reset
|
||||||
|
document.addEventListener('wpcf7reset', function(event) {
|
||||||
|
setTimeout(function() {
|
||||||
|
WpBnbCF7.init();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export to window for external access
|
||||||
|
window.WpBnbCF7 = WpBnbCF7;
|
||||||
|
})();
|
||||||
@@ -1,12 +1,825 @@
|
|||||||
/**
|
/**
|
||||||
* WP BnB Frontend JavaScript
|
* WP BnB Frontend JavaScript
|
||||||
*
|
*
|
||||||
|
* Handles search forms, calendar widgets, and interactive elements.
|
||||||
|
*
|
||||||
* @package Magdev\WpBnb
|
* @package Magdev\WpBnb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Placeholder - Frontend scripts will be added as features are implemented
|
/**
|
||||||
|
* WP BnB Frontend namespace.
|
||||||
|
*/
|
||||||
|
const WpBnb = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration from localized script.
|
||||||
|
*/
|
||||||
|
config: window.wpBnbFrontend || {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all frontend components.
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.initSearchForms();
|
||||||
|
this.initCalendarWidgets();
|
||||||
|
this.initAvailabilityForms();
|
||||||
|
this.initPriceCalculators();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize room search forms.
|
||||||
|
*/
|
||||||
|
initSearchForms: function() {
|
||||||
|
const forms = document.querySelectorAll('.wp-bnb-search-form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
new SearchForm(form);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize calendar widgets.
|
||||||
|
*/
|
||||||
|
initCalendarWidgets: function() {
|
||||||
|
const calendars = document.querySelectorAll('.wp-bnb-availability-calendar-widget');
|
||||||
|
calendars.forEach(calendar => {
|
||||||
|
new CalendarWidget(calendar);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize availability check forms on single room pages.
|
||||||
|
*/
|
||||||
|
initAvailabilityForms: function() {
|
||||||
|
const forms = document.querySelectorAll('.wp-bnb-availability-check');
|
||||||
|
forms.forEach(form => {
|
||||||
|
new AvailabilityForm(form);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize price calculator forms.
|
||||||
|
*/
|
||||||
|
initPriceCalculators: function() {
|
||||||
|
const calculators = document.querySelectorAll('.wp-bnb-price-calculator');
|
||||||
|
calculators.forEach(calculator => {
|
||||||
|
new PriceCalculator(calculator);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an AJAX request.
|
||||||
|
*
|
||||||
|
* @param {string} action The AJAX action.
|
||||||
|
* @param {Object} data The request data.
|
||||||
|
* @return {Promise} Promise resolving to response data.
|
||||||
|
*/
|
||||||
|
ajax: function(action, data = {}) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', action);
|
||||||
|
formData.append('nonce', this.config.nonce || '');
|
||||||
|
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (data[key] !== null && data[key] !== undefined) {
|
||||||
|
formData.append(key, data[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetch(this.config.ajaxUrl || '/wp-admin/admin-ajax.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'same-origin'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.data?.message || 'Request failed');
|
||||||
|
}
|
||||||
|
return data.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as YYYY-MM-DD.
|
||||||
|
*
|
||||||
|
* @param {Date} date The date object.
|
||||||
|
* @return {string} Formatted date string.
|
||||||
|
*/
|
||||||
|
formatDate: function(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date string.
|
||||||
|
*
|
||||||
|
* @param {string} dateStr Date string in YYYY-MM-DD format.
|
||||||
|
* @return {Date|null} Date object or null if invalid.
|
||||||
|
*/
|
||||||
|
parseDate: function(dateStr) {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
const parts = dateStr.split('-');
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate nights between two dates.
|
||||||
|
*
|
||||||
|
* @param {Date} checkIn Check-in date.
|
||||||
|
* @param {Date} checkOut Check-out date.
|
||||||
|
* @return {number} Number of nights.
|
||||||
|
*/
|
||||||
|
calculateNights: function(checkIn, checkOut) {
|
||||||
|
if (!checkIn || !checkOut) return 0;
|
||||||
|
const diffTime = checkOut.getTime() - checkIn.getTime();
|
||||||
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce a function.
|
||||||
|
*
|
||||||
|
* @param {Function} func The function to debounce.
|
||||||
|
* @param {number} wait Wait time in milliseconds.
|
||||||
|
* @return {Function} Debounced function.
|
||||||
|
*/
|
||||||
|
debounce: function(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Form handler class.
|
||||||
|
*/
|
||||||
|
class SearchForm {
|
||||||
|
constructor(element) {
|
||||||
|
this.form = element;
|
||||||
|
this.resultsContainer = document.querySelector(
|
||||||
|
this.form.dataset.results || '.wp-bnb-search-results'
|
||||||
|
);
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Form submission.
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.search();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Date validation.
|
||||||
|
const checkIn = this.form.querySelector('[name="check_in"]');
|
||||||
|
const checkOut = this.form.querySelector('[name="check_out"]');
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
// Set min date to today.
|
||||||
|
const today = WpBnb.formatDate(new Date());
|
||||||
|
checkIn.setAttribute('min', today);
|
||||||
|
|
||||||
|
checkIn.addEventListener('change', () => {
|
||||||
|
if (checkIn.value) {
|
||||||
|
// Set check-out min to day after check-in.
|
||||||
|
const minCheckOut = WpBnb.parseDate(checkIn.value);
|
||||||
|
if (minCheckOut) {
|
||||||
|
minCheckOut.setDate(minCheckOut.getDate() + 1);
|
||||||
|
checkOut.setAttribute('min', WpBnb.formatDate(minCheckOut));
|
||||||
|
|
||||||
|
// Clear check-out if it's before new minimum.
|
||||||
|
if (checkOut.value && checkOut.value <= checkIn.value) {
|
||||||
|
checkOut.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkOut.addEventListener('change', () => {
|
||||||
|
if (checkOut.value && checkIn.value && checkOut.value <= checkIn.value) {
|
||||||
|
alert(WpBnb.config.i18n?.invalidDateRange || 'Check-out must be after check-in');
|
||||||
|
checkOut.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button.
|
||||||
|
const resetBtn = this.form.querySelector('[type="reset"]');
|
||||||
|
if (resetBtn) {
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.clearResults();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load more button.
|
||||||
|
if (this.resultsContainer) {
|
||||||
|
this.resultsContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('wp-bnb-load-more')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormData() {
|
||||||
|
const formData = new FormData(this.form);
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (value) {
|
||||||
|
// Handle array fields (amenities[]).
|
||||||
|
if (key.endsWith('[]')) {
|
||||||
|
const cleanKey = key.slice(0, -2);
|
||||||
|
if (!data[cleanKey]) {
|
||||||
|
data[cleanKey] = [];
|
||||||
|
}
|
||||||
|
data[cleanKey].push(value);
|
||||||
|
} else {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert arrays to comma-separated strings for AJAX.
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (Array.isArray(data[key])) {
|
||||||
|
data[key] = data[key].join(',');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
search() {
|
||||||
|
if (this.isLoading) return;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.showLoading();
|
||||||
|
|
||||||
|
const data = this.getFormData();
|
||||||
|
data.page = this.currentPage;
|
||||||
|
data.per_page = this.form.dataset.perPage || 12;
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_search_rooms', data)
|
||||||
|
.then(response => {
|
||||||
|
this.renderResults(response, this.currentPage === 1);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.showError(error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.hideLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore() {
|
||||||
|
this.currentPage++;
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResults(response, replace = true) {
|
||||||
|
if (!this.resultsContainer) return;
|
||||||
|
|
||||||
|
const { rooms, total, page, total_pages } = response;
|
||||||
|
|
||||||
|
if (replace) {
|
||||||
|
this.resultsContainer.innerHTML = '';
|
||||||
|
} else {
|
||||||
|
// Remove existing load more button.
|
||||||
|
const existingLoadMore = this.resultsContainer.querySelector('.wp-bnb-load-more-wrapper');
|
||||||
|
if (existingLoadMore) {
|
||||||
|
existingLoadMore.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rooms.length === 0 && replace) {
|
||||||
|
this.resultsContainer.innerHTML = `
|
||||||
|
<div class="wp-bnb-no-results">
|
||||||
|
<p>${WpBnb.config.i18n?.noResults || 'No rooms found matching your criteria.'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create results count.
|
||||||
|
if (replace) {
|
||||||
|
const countEl = document.createElement('div');
|
||||||
|
countEl.className = 'wp-bnb-results-count';
|
||||||
|
countEl.innerHTML = `<p>${WpBnb.config.i18n?.resultsFound?.replace('%d', total) || `${total} rooms found`}</p>`;
|
||||||
|
this.resultsContainer.appendChild(countEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create grid container.
|
||||||
|
let grid = this.resultsContainer.querySelector('.wp-bnb-rooms-grid');
|
||||||
|
if (!grid) {
|
||||||
|
grid = document.createElement('div');
|
||||||
|
grid.className = 'wp-bnb-rooms-grid wp-bnb-grid wp-bnb-grid-3';
|
||||||
|
this.resultsContainer.appendChild(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render room cards.
|
||||||
|
rooms.forEach(room => {
|
||||||
|
const card = this.createRoomCard(room);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add load more button if there are more pages.
|
||||||
|
if (page < total_pages) {
|
||||||
|
const loadMoreWrapper = document.createElement('div');
|
||||||
|
loadMoreWrapper.className = 'wp-bnb-load-more-wrapper';
|
||||||
|
loadMoreWrapper.innerHTML = `
|
||||||
|
<button type="button" class="wp-bnb-load-more wp-bnb-button">
|
||||||
|
${WpBnb.config.i18n?.loadMore || 'Load More'}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
this.resultsContainer.appendChild(loadMoreWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoomCard(room) {
|
||||||
|
const card = document.createElement('article');
|
||||||
|
card.className = 'wp-bnb-room-card';
|
||||||
|
|
||||||
|
let imageHtml = '';
|
||||||
|
if (room.thumbnail) {
|
||||||
|
imageHtml = `
|
||||||
|
<div class="wp-bnb-room-card-image">
|
||||||
|
<a href="${this.escapeHtml(room.permalink)}">
|
||||||
|
<img src="${this.escapeHtml(room.thumbnail)}" alt="${this.escapeHtml(room.title)}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amenitiesHtml = '';
|
||||||
|
if (room.amenities && room.amenities.length > 0) {
|
||||||
|
const amenityItems = room.amenities.slice(0, 4).map(a =>
|
||||||
|
`<span class="wp-bnb-amenity-tag">${this.escapeHtml(a.name)}</span>`
|
||||||
|
).join('');
|
||||||
|
amenitiesHtml = `<div class="wp-bnb-room-card-amenities">${amenityItems}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let priceHtml = '';
|
||||||
|
if (room.price_display) {
|
||||||
|
priceHtml = `
|
||||||
|
<div class="wp-bnb-room-card-price">
|
||||||
|
<span class="wp-bnb-price">${this.escapeHtml(room.price_display)}</span>
|
||||||
|
<span class="wp-bnb-price-unit">/ ${WpBnb.config.i18n?.perNight || 'night'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
${imageHtml}
|
||||||
|
<div class="wp-bnb-room-card-content">
|
||||||
|
<h3 class="wp-bnb-room-card-title">
|
||||||
|
<a href="${this.escapeHtml(room.permalink)}">${this.escapeHtml(room.title)}</a>
|
||||||
|
</h3>
|
||||||
|
${room.building_name ? `<p class="wp-bnb-room-card-building">${this.escapeHtml(room.building_name)}</p>` : ''}
|
||||||
|
<div class="wp-bnb-room-card-meta">
|
||||||
|
${room.capacity ? `<span class="wp-bnb-capacity">${room.capacity} ${WpBnb.config.i18n?.guests || 'guests'}</span>` : ''}
|
||||||
|
${room.room_type ? `<span class="wp-bnb-room-type">${this.escapeHtml(room.room_type)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
${amenitiesHtml}
|
||||||
|
${priceHtml}
|
||||||
|
<a href="${this.escapeHtml(room.permalink)}" class="wp-bnb-room-card-link wp-bnb-button wp-bnb-button-small">
|
||||||
|
${WpBnb.config.i18n?.viewDetails || 'View Details'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
this.form.classList.add('wp-bnb-loading');
|
||||||
|
const submitBtn = this.form.querySelector('[type="submit"]');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.dataset.originalText = submitBtn.textContent;
|
||||||
|
submitBtn.textContent = WpBnb.config.i18n?.searching || 'Searching...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading() {
|
||||||
|
this.form.classList.remove('wp-bnb-loading');
|
||||||
|
const submitBtn = this.form.querySelector('[type="submit"]');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
if (submitBtn.dataset.originalText) {
|
||||||
|
submitBtn.textContent = submitBtn.dataset.originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
if (!this.resultsContainer) return;
|
||||||
|
this.resultsContainer.innerHTML = `
|
||||||
|
<div class="wp-bnb-error">
|
||||||
|
<p>${this.escapeHtml(message)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearResults() {
|
||||||
|
if (this.resultsContainer) {
|
||||||
|
this.resultsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar Widget handler class.
|
||||||
|
*/
|
||||||
|
class CalendarWidget {
|
||||||
|
constructor(element) {
|
||||||
|
this.container = element;
|
||||||
|
this.roomId = element.dataset.roomId;
|
||||||
|
this.currentYear = parseInt(element.querySelector('[data-year]')?.dataset.year) || new Date().getFullYear();
|
||||||
|
this.currentMonth = parseInt(element.querySelector('[data-month]')?.dataset.month) || (new Date().getMonth() + 1);
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Navigation buttons.
|
||||||
|
this.container.addEventListener('click', (e) => {
|
||||||
|
const navBtn = e.target.closest('.wp-bnb-calendar-nav');
|
||||||
|
if (navBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
const direction = navBtn.dataset.direction;
|
||||||
|
if (direction === 'prev') {
|
||||||
|
this.navigatePrev();
|
||||||
|
} else if (direction === 'next') {
|
||||||
|
this.navigateNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigatePrev() {
|
||||||
|
this.currentMonth--;
|
||||||
|
if (this.currentMonth < 1) {
|
||||||
|
this.currentMonth = 12;
|
||||||
|
this.currentYear--;
|
||||||
|
}
|
||||||
|
this.loadCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateNext() {
|
||||||
|
this.currentMonth++;
|
||||||
|
if (this.currentMonth > 12) {
|
||||||
|
this.currentMonth = 1;
|
||||||
|
this.currentYear++;
|
||||||
|
}
|
||||||
|
this.loadCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCalendar() {
|
||||||
|
this.container.classList.add('wp-bnb-loading');
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_get_calendar', {
|
||||||
|
room_id: this.roomId,
|
||||||
|
year: this.currentYear,
|
||||||
|
month: this.currentMonth
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
this.renderCalendar(response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Calendar load error:', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.container.classList.remove('wp-bnb-loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCalendar(data) {
|
||||||
|
const monthContainer = this.container.querySelector('.wp-bnb-calendar-month');
|
||||||
|
if (!monthContainer) return;
|
||||||
|
|
||||||
|
// Update month/year attributes.
|
||||||
|
monthContainer.dataset.year = this.currentYear;
|
||||||
|
monthContainer.dataset.month = this.currentMonth;
|
||||||
|
|
||||||
|
// Update month name.
|
||||||
|
const monthNameEl = monthContainer.querySelector('.wp-bnb-calendar-month-name');
|
||||||
|
if (monthNameEl) {
|
||||||
|
monthNameEl.textContent = `${data.month_name} ${this.currentYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild calendar grid.
|
||||||
|
const tbody = monthContainer.querySelector('.wp-bnb-calendar-grid tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
let day = 1;
|
||||||
|
const totalDays = data.days_in_month;
|
||||||
|
const firstDay = data.first_day_of_week;
|
||||||
|
const weeks = Math.ceil((firstDay + totalDays) / 7);
|
||||||
|
|
||||||
|
for (let week = 0; week < weeks; week++) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
for (let dow = 0; dow < 7; dow++) {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
const cellIndex = week * 7 + dow;
|
||||||
|
|
||||||
|
if (cellIndex < firstDay || day > totalDays) {
|
||||||
|
td.className = 'wp-bnb-calendar-empty';
|
||||||
|
} else {
|
||||||
|
const dayData = data.days[day];
|
||||||
|
const classes = ['wp-bnb-calendar-day'];
|
||||||
|
|
||||||
|
if (dayData) {
|
||||||
|
if (dayData.is_booked) {
|
||||||
|
classes.push('wp-bnb-booked');
|
||||||
|
} else {
|
||||||
|
classes.push('wp-bnb-available');
|
||||||
|
}
|
||||||
|
if (dayData.is_past) {
|
||||||
|
classes.push('wp-bnb-past');
|
||||||
|
}
|
||||||
|
if (dayData.is_today) {
|
||||||
|
classes.push('wp-bnb-today');
|
||||||
|
}
|
||||||
|
td.dataset.date = dayData.date || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
td.className = classes.join(' ');
|
||||||
|
td.textContent = day;
|
||||||
|
day++;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.appendChild(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability Form handler class.
|
||||||
|
* For checking availability on single room pages.
|
||||||
|
*/
|
||||||
|
class AvailabilityForm {
|
||||||
|
constructor(element) {
|
||||||
|
this.form = element;
|
||||||
|
this.roomId = element.dataset.roomId;
|
||||||
|
this.resultContainer = element.querySelector('.wp-bnb-availability-result');
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.checkAvailability();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Date validation.
|
||||||
|
const checkIn = this.form.querySelector('[name="check_in"]');
|
||||||
|
const checkOut = this.form.querySelector('[name="check_out"]');
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
const today = WpBnb.formatDate(new Date());
|
||||||
|
checkIn.setAttribute('min', today);
|
||||||
|
|
||||||
|
checkIn.addEventListener('change', () => {
|
||||||
|
if (checkIn.value) {
|
||||||
|
const minCheckOut = WpBnb.parseDate(checkIn.value);
|
||||||
|
if (minCheckOut) {
|
||||||
|
minCheckOut.setDate(minCheckOut.getDate() + 1);
|
||||||
|
checkOut.setAttribute('min', WpBnb.formatDate(minCheckOut));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clearResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkOut.addEventListener('change', () => {
|
||||||
|
this.clearResult();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAvailability() {
|
||||||
|
const checkIn = this.form.querySelector('[name="check_in"]')?.value;
|
||||||
|
const checkOut = this.form.querySelector('[name="check_out"]')?.value;
|
||||||
|
|
||||||
|
if (!checkIn || !checkOut) {
|
||||||
|
this.showResult('error', WpBnb.config.i18n?.selectDates || 'Please select check-in and check-out dates.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkOut <= checkIn) {
|
||||||
|
this.showResult('error', WpBnb.config.i18n?.invalidDateRange || 'Check-out must be after check-in.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.classList.add('wp-bnb-loading');
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_get_availability', {
|
||||||
|
room_id: this.roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.available) {
|
||||||
|
let message = WpBnb.config.i18n?.available || 'Room is available!';
|
||||||
|
if (response.price_display) {
|
||||||
|
message += ` ${WpBnb.config.i18n?.totalPrice || 'Total'}: ${response.price_display}`;
|
||||||
|
}
|
||||||
|
this.showResult('success', message, response);
|
||||||
|
} else {
|
||||||
|
this.showResult('error', WpBnb.config.i18n?.notAvailable || 'Sorry, the room is not available for these dates.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.showResult('error', error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.form.classList.remove('wp-bnb-loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult(type, message, data = null) {
|
||||||
|
if (!this.resultContainer) return;
|
||||||
|
|
||||||
|
let html = `<div class="wp-bnb-availability-${type}">${this.escapeHtml(message)}</div>`;
|
||||||
|
|
||||||
|
if (type === 'success' && data && data.booking_url) {
|
||||||
|
html += `
|
||||||
|
<a href="${this.escapeHtml(data.booking_url)}" class="wp-bnb-button wp-bnb-book-now">
|
||||||
|
${WpBnb.config.i18n?.bookNow || 'Book Now'}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resultContainer.innerHTML = html;
|
||||||
|
this.resultContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
clearResult() {
|
||||||
|
if (this.resultContainer) {
|
||||||
|
this.resultContainer.innerHTML = '';
|
||||||
|
this.resultContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price Calculator handler class.
|
||||||
|
*/
|
||||||
|
class PriceCalculator {
|
||||||
|
constructor(element) {
|
||||||
|
this.container = element;
|
||||||
|
this.roomId = element.dataset.roomId;
|
||||||
|
this.priceDisplay = element.querySelector('.wp-bnb-calculated-price');
|
||||||
|
this.breakdownDisplay = element.querySelector('.wp-bnb-price-breakdown');
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const checkIn = this.container.querySelector('[name="check_in"]');
|
||||||
|
const checkOut = this.container.querySelector('[name="check_out"]');
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
const debouncedCalculate = WpBnb.debounce(() => this.calculate(), 300);
|
||||||
|
|
||||||
|
checkIn.addEventListener('change', debouncedCalculate);
|
||||||
|
checkOut.addEventListener('change', debouncedCalculate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
const checkIn = this.container.querySelector('[name="check_in"]')?.value;
|
||||||
|
const checkOut = this.container.querySelector('[name="check_out"]')?.value;
|
||||||
|
|
||||||
|
if (!checkIn || !checkOut || checkOut <= checkIn) {
|
||||||
|
this.clearDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.classList.add('wp-bnb-loading');
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_calculate_price', {
|
||||||
|
room_id: this.roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
this.displayPrice(response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Price calculation error:', error);
|
||||||
|
this.clearDisplay();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.container.classList.remove('wp-bnb-loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displayPrice(data) {
|
||||||
|
if (this.priceDisplay) {
|
||||||
|
this.priceDisplay.innerHTML = `
|
||||||
|
<span class="wp-bnb-price-label">${WpBnb.config.i18n?.total || 'Total'}:</span>
|
||||||
|
<span class="wp-bnb-price-amount">${this.escapeHtml(data.formatted_total)}</span>
|
||||||
|
`;
|
||||||
|
this.priceDisplay.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.breakdownDisplay && data.breakdown) {
|
||||||
|
let breakdownHtml = '<ul class="wp-bnb-breakdown-list">';
|
||||||
|
|
||||||
|
if (data.breakdown.nights) {
|
||||||
|
breakdownHtml += `<li>${data.breakdown.nights} ${WpBnb.config.i18n?.nights || 'nights'}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.tier) {
|
||||||
|
breakdownHtml += `<li>${this.escapeHtml(data.breakdown.tier)}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.base_total) {
|
||||||
|
breakdownHtml += `<li>${WpBnb.config.i18n?.basePrice || 'Base'}: ${this.escapeHtml(data.breakdown.base_total)}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.weekend_total && parseFloat(data.breakdown.weekend_total) > 0) {
|
||||||
|
breakdownHtml += `<li>${WpBnb.config.i18n?.weekendSurcharge || 'Weekend surcharge'}: ${this.escapeHtml(data.breakdown.weekend_total)}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.season_name) {
|
||||||
|
breakdownHtml += `<li>${WpBnb.config.i18n?.season || 'Season'}: ${this.escapeHtml(data.breakdown.season_name)}</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakdownHtml += '</ul>';
|
||||||
|
|
||||||
|
this.breakdownDisplay.innerHTML = breakdownHtml;
|
||||||
|
this.breakdownDisplay.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDisplay() {
|
||||||
|
if (this.priceDisplay) {
|
||||||
|
this.priceDisplay.innerHTML = '';
|
||||||
|
this.priceDisplay.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (this.breakdownDisplay) {
|
||||||
|
this.breakdownDisplay.innerHTML = '';
|
||||||
|
this.breakdownDisplay.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on DOM ready.
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => WpBnb.init());
|
||||||
|
} else {
|
||||||
|
WpBnb.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose to global scope for potential external use.
|
||||||
|
window.WpBnb = WpBnb;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
465
src/Blocks/BlockRegistrar.php
Normal file
465
src/Blocks/BlockRegistrar.php
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Block registrar.
|
||||||
|
*
|
||||||
|
* Handles registration of all Gutenberg blocks.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Blocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Blocks;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block registrar class.
|
||||||
|
*/
|
||||||
|
final class BlockRegistrar {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize block registration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register_blocks' ) );
|
||||||
|
add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all blocks.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_blocks(): void {
|
||||||
|
// Building block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/building',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'buildingId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0,
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAddress' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showRooms' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showContact' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_building_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Room block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/room',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'roomId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0,
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showGallery' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showPrice' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAmenities' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAvailability' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_room_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Room Search block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/room-search',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'layout' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'grid',
|
||||||
|
),
|
||||||
|
'columns' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 3,
|
||||||
|
),
|
||||||
|
'showDates' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showGuests' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showRoomType' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAmenities' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showPriceRange' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showBuilding' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'resultsPerPage' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_room_search_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buildings List block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/buildings',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'layout' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'grid',
|
||||||
|
),
|
||||||
|
'columns' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 3,
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => -1,
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAddress' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showRoomsCount' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_buildings_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rooms List block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/rooms',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'layout' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'grid',
|
||||||
|
),
|
||||||
|
'columns' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 3,
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 12,
|
||||||
|
),
|
||||||
|
'buildingId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0,
|
||||||
|
),
|
||||||
|
'roomType' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '',
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showPrice' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showCapacity' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAmenities' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showBuilding' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_rooms_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue editor assets.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function enqueue_editor_assets(): void {
|
||||||
|
// Register the editor script.
|
||||||
|
wp_register_script(
|
||||||
|
'wp-bnb-blocks-editor',
|
||||||
|
WP_BNB_URL . 'assets/js/blocks-editor.js',
|
||||||
|
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data' ),
|
||||||
|
WP_BNB_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get buildings and rooms for selectors.
|
||||||
|
$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',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$form_data = Search::get_search_form_data();
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
'wp-bnb-blocks-editor',
|
||||||
|
'wpBnbBlocks',
|
||||||
|
array(
|
||||||
|
'buildings' => array_map(
|
||||||
|
function ( $building ) {
|
||||||
|
return array(
|
||||||
|
'value' => $building->ID,
|
||||||
|
'label' => $building->post_title,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$buildings
|
||||||
|
),
|
||||||
|
'rooms' => array_map(
|
||||||
|
function ( $room ) {
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
return array(
|
||||||
|
'value' => $room->ID,
|
||||||
|
'label' => $room->post_title,
|
||||||
|
'building' => $building ? $building->post_title : '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$rooms
|
||||||
|
),
|
||||||
|
'roomTypes' => $form_data['room_types'],
|
||||||
|
'amenities' => $form_data['amenities'],
|
||||||
|
'i18n' => array(
|
||||||
|
'selectBuilding' => __( 'Select a building', 'wp-bnb' ),
|
||||||
|
'selectRoom' => __( 'Select a room', 'wp-bnb' ),
|
||||||
|
'noBuildings' => __( 'No buildings found. Create a building first.', 'wp-bnb' ),
|
||||||
|
'noRooms' => __( 'No rooms found. Create a room first.', 'wp-bnb' ),
|
||||||
|
'buildingBlock' => __( 'Building', 'wp-bnb' ),
|
||||||
|
'roomBlock' => __( 'Room', 'wp-bnb' ),
|
||||||
|
'roomSearchBlock' => __( 'Room Search', 'wp-bnb' ),
|
||||||
|
'buildingsBlock' => __( 'Buildings List', 'wp-bnb' ),
|
||||||
|
'roomsBlock' => __( 'Rooms List', 'wp-bnb' ),
|
||||||
|
'displaySettings' => __( 'Display Settings', 'wp-bnb' ),
|
||||||
|
'filterSettings' => __( 'Filter Settings', 'wp-bnb' ),
|
||||||
|
'layout' => __( 'Layout', 'wp-bnb' ),
|
||||||
|
'grid' => __( 'Grid', 'wp-bnb' ),
|
||||||
|
'list' => __( 'List', 'wp-bnb' ),
|
||||||
|
'columns' => __( 'Columns', 'wp-bnb' ),
|
||||||
|
'limit' => __( 'Limit', 'wp-bnb' ),
|
||||||
|
'showImage' => __( 'Show image', 'wp-bnb' ),
|
||||||
|
'showAddress' => __( 'Show address', 'wp-bnb' ),
|
||||||
|
'showRooms' => __( 'Show rooms', 'wp-bnb' ),
|
||||||
|
'showRoomsCount' => __( 'Show rooms count', 'wp-bnb' ),
|
||||||
|
'showContact' => __( 'Show contact', 'wp-bnb' ),
|
||||||
|
'showGallery' => __( 'Show gallery', 'wp-bnb' ),
|
||||||
|
'showPrice' => __( 'Show price', 'wp-bnb' ),
|
||||||
|
'showAmenities' => __( 'Show amenities', 'wp-bnb' ),
|
||||||
|
'showAvailability' => __( 'Show availability', 'wp-bnb' ),
|
||||||
|
'showCapacity' => __( 'Show capacity', 'wp-bnb' ),
|
||||||
|
'showBuilding' => __( 'Show building', 'wp-bnb' ),
|
||||||
|
'showDates' => __( 'Show date filter', 'wp-bnb' ),
|
||||||
|
'showGuests' => __( 'Show guests filter', 'wp-bnb' ),
|
||||||
|
'showRoomType' => __( 'Show room type filter', 'wp-bnb' ),
|
||||||
|
'showPriceRange' => __( 'Show price range filter', 'wp-bnb' ),
|
||||||
|
'resultsPerPage' => __( 'Results per page', 'wp-bnb' ),
|
||||||
|
'roomType' => __( 'Room Type', 'wp-bnb' ),
|
||||||
|
'allTypes' => __( 'All Types', 'wp-bnb' ),
|
||||||
|
'allBuildings' => __( 'All Buildings', 'wp-bnb' ),
|
||||||
|
'previewPlaceholder' => __( 'Preview will appear here', 'wp-bnb' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Editor styles.
|
||||||
|
wp_enqueue_style(
|
||||||
|
'wp-bnb-blocks-editor',
|
||||||
|
WP_BNB_URL . 'assets/css/blocks-editor.css',
|
||||||
|
array(),
|
||||||
|
WP_BNB_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render building block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_building_block( array $attributes ): string {
|
||||||
|
$building_id = $attributes['buildingId'] ?? 0;
|
||||||
|
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return '<p class="wp-bnb-block-placeholder">' . esc_html__( 'Please select a building.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shortcodes::render_single_building(
|
||||||
|
array(
|
||||||
|
'id' => $building_id,
|
||||||
|
'show_rooms' => ( $attributes['showRooms'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_address' => ( $attributes['showAddress'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_contact' => ( $attributes['showContact'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_room_block( array $attributes ): string {
|
||||||
|
$room_id = $attributes['roomId'] ?? 0;
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return '<p class="wp-bnb-block-placeholder">' . esc_html__( 'Please select a room.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shortcodes::render_single_room(
|
||||||
|
array(
|
||||||
|
'id' => $room_id,
|
||||||
|
'show_gallery' => ( $attributes['showGallery'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_pricing' => ( $attributes['showPrice'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_availability' => ( $attributes['showAvailability'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room search block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_room_search_block( array $attributes ): string {
|
||||||
|
return Shortcodes::render_room_search(
|
||||||
|
array(
|
||||||
|
'layout' => $attributes['layout'] ?? 'grid',
|
||||||
|
'columns' => $attributes['columns'] ?? 3,
|
||||||
|
'show_dates' => ( $attributes['showDates'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_guests' => ( $attributes['showGuests'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_room_type' => ( $attributes['showRoomType'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_price_range' => ( $attributes['showPriceRange'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_building' => ( $attributes['showBuilding'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'results_per_page' => $attributes['resultsPerPage'] ?? 12,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render buildings list block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_buildings_block( array $attributes ): string {
|
||||||
|
return Shortcodes::render_buildings(
|
||||||
|
array(
|
||||||
|
'layout' => $attributes['layout'] ?? 'grid',
|
||||||
|
'columns' => $attributes['columns'] ?? 3,
|
||||||
|
'limit' => $attributes['limit'] ?? -1,
|
||||||
|
'show_image' => ( $attributes['showImage'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_address' => ( $attributes['showAddress'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_rooms_count' => ( $attributes['showRoomsCount'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render rooms list block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_rooms_block( array $attributes ): string {
|
||||||
|
return Shortcodes::render_rooms(
|
||||||
|
array(
|
||||||
|
'layout' => $attributes['layout'] ?? 'grid',
|
||||||
|
'columns' => $attributes['columns'] ?? 3,
|
||||||
|
'limit' => $attributes['limit'] ?? 12,
|
||||||
|
'building_id' => $attributes['buildingId'] ?? 0,
|
||||||
|
'room_type' => $attributes['roomType'] ?? '',
|
||||||
|
'show_image' => ( $attributes['showImage'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_price' => ( $attributes['showPrice'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_capacity' => ( $attributes['showCapacity'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_building' => ( $attributes['showBuilding'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
677
src/Frontend/Search.php
Normal file
677
src/Frontend/Search.php
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Frontend room search.
|
||||||
|
*
|
||||||
|
* Handles room search with availability checking and filtering.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search class for frontend room searches.
|
||||||
|
*/
|
||||||
|
final class Search {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the search system.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Public AJAX handlers (no login required).
|
||||||
|
add_action( 'wp_ajax_wp_bnb_search_rooms', array( self::class, 'ajax_search_rooms' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_search_rooms', array( self::class, 'ajax_search_rooms' ) );
|
||||||
|
|
||||||
|
add_action( 'wp_ajax_wp_bnb_get_availability', array( self::class, 'ajax_get_availability' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_get_availability', array( self::class, 'ajax_get_availability' ) );
|
||||||
|
|
||||||
|
add_action( 'wp_ajax_wp_bnb_get_calendar', array( self::class, 'ajax_get_calendar' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_get_calendar', array( self::class, 'ajax_get_calendar' ) );
|
||||||
|
|
||||||
|
add_action( 'wp_ajax_wp_bnb_calculate_price', array( self::class, 'ajax_calculate_price' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_calculate_price', array( self::class, 'ajax_calculate_price' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for rooms with filters.
|
||||||
|
*
|
||||||
|
* @param array $args Search arguments.
|
||||||
|
* @return array Array of room data.
|
||||||
|
*/
|
||||||
|
public static function search( array $args = array() ): array {
|
||||||
|
$defaults = array(
|
||||||
|
'check_in' => '',
|
||||||
|
'check_out' => '',
|
||||||
|
'guests' => 0,
|
||||||
|
'room_type' => '',
|
||||||
|
'amenities' => array(),
|
||||||
|
'price_min' => 0,
|
||||||
|
'price_max' => 0,
|
||||||
|
'building_id' => 0,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'limit' => -1,
|
||||||
|
'offset' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
|
// Build base query.
|
||||||
|
$query_args = array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => (int) $args['limit'],
|
||||||
|
'offset' => (int) $args['offset'],
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
),
|
||||||
|
'tax_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by building.
|
||||||
|
if ( ! empty( $args['building_id'] ) ) {
|
||||||
|
$query_args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => (int) $args['building_id'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by capacity.
|
||||||
|
if ( ! empty( $args['guests'] ) && (int) $args['guests'] > 0 ) {
|
||||||
|
$query_args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_capacity',
|
||||||
|
'value' => (int) $args['guests'],
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'NUMERIC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by room status (only available rooms).
|
||||||
|
$query_args['meta_query'][] = array(
|
||||||
|
'relation' => 'OR',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'value' => 'available',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'compare' => 'NOT EXISTS',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by room type.
|
||||||
|
if ( ! empty( $args['room_type'] ) ) {
|
||||||
|
$query_args['tax_query'][] = array(
|
||||||
|
'taxonomy' => RoomType::TAXONOMY,
|
||||||
|
'field' => is_numeric( $args['room_type'] ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $args['room_type'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by amenities (all must match).
|
||||||
|
if ( ! empty( $args['amenities'] ) ) {
|
||||||
|
$amenities = is_array( $args['amenities'] ) ? $args['amenities'] : explode( ',', $args['amenities'] );
|
||||||
|
$amenities = array_map( 'trim', $amenities );
|
||||||
|
$amenities = array_filter( $amenities );
|
||||||
|
|
||||||
|
if ( ! empty( $amenities ) ) {
|
||||||
|
$query_args['tax_query'][] = array(
|
||||||
|
'taxonomy' => Amenity::TAXONOMY,
|
||||||
|
'field' => is_numeric( $amenities[0] ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $amenities,
|
||||||
|
'operator' => 'AND',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ordering.
|
||||||
|
switch ( $args['orderby'] ) {
|
||||||
|
case 'price':
|
||||||
|
$query_args['meta_key'] = '_bnb_room_price_' . PricingTier::SHORT_TERM->value;
|
||||||
|
$query_args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
case 'capacity':
|
||||||
|
$query_args['meta_key'] = '_bnb_room_capacity';
|
||||||
|
$query_args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
$query_args['orderby'] = 'date';
|
||||||
|
break;
|
||||||
|
case 'random':
|
||||||
|
$query_args['orderby'] = 'rand';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$query_args['orderby'] = 'title';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query_args['order'] = strtoupper( $args['order'] ) === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
|
// Execute query.
|
||||||
|
$rooms = get_posts( $query_args );
|
||||||
|
|
||||||
|
// Filter by availability if dates provided.
|
||||||
|
if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||||
|
$rooms = self::filter_by_availability( $rooms, $args['check_in'], $args['check_out'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by price range.
|
||||||
|
if ( ( ! empty( $args['price_min'] ) || ! empty( $args['price_max'] ) ) && ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||||
|
$rooms = self::filter_by_price_range(
|
||||||
|
$rooms,
|
||||||
|
(float) $args['price_min'],
|
||||||
|
(float) $args['price_max'],
|
||||||
|
$args['check_in'],
|
||||||
|
$args['check_out']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result array with room data.
|
||||||
|
$results = array();
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$results[] = self::get_room_data( $room, $args['check_in'], $args['check_out'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rooms by availability.
|
||||||
|
*
|
||||||
|
* @param array $rooms Array of WP_Post objects.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @return array Filtered rooms.
|
||||||
|
*/
|
||||||
|
public static function filter_by_availability( array $rooms, string $check_in, string $check_out ): array {
|
||||||
|
return array_filter(
|
||||||
|
$rooms,
|
||||||
|
function ( $room ) use ( $check_in, $check_out ) {
|
||||||
|
return Availability::is_available( $room->ID, $check_in, $check_out );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rooms by price range.
|
||||||
|
*
|
||||||
|
* @param array $rooms Array of WP_Post objects.
|
||||||
|
* @param float $min Minimum price.
|
||||||
|
* @param float $max Maximum price.
|
||||||
|
* @param string $check_in Check-in date.
|
||||||
|
* @param string $check_out Check-out date.
|
||||||
|
* @return array Filtered rooms.
|
||||||
|
*/
|
||||||
|
public static function filter_by_price_range( array $rooms, float $min, float $max, string $check_in, string $check_out ): array {
|
||||||
|
return array_filter(
|
||||||
|
$rooms,
|
||||||
|
function ( $room ) use ( $min, $max, $check_in, $check_out ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room->ID, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
|
||||||
|
if ( $min > 0 && $price < $min ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( $max > 0 && $price > $max ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete room data for display.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @param string $check_in Optional check-in date.
|
||||||
|
* @param string $check_out Optional check-out date.
|
||||||
|
* @return array Room data array.
|
||||||
|
*/
|
||||||
|
public static function get_room_data( \WP_Post $room, string $check_in = '', string $check_out = '' ): array {
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
|
||||||
|
// Get room types.
|
||||||
|
$room_types = wp_get_post_terms( $room->ID, RoomType::TAXONOMY, array( 'fields' => 'names' ) );
|
||||||
|
|
||||||
|
// Get amenities with icons.
|
||||||
|
$amenities = wp_get_post_terms( $room->ID, Amenity::TAXONOMY );
|
||||||
|
$amenity_list = array();
|
||||||
|
foreach ( $amenities as $amenity ) {
|
||||||
|
$amenity_list[] = array(
|
||||||
|
'id' => $amenity->term_id,
|
||||||
|
'name' => $amenity->name,
|
||||||
|
'slug' => $amenity->slug,
|
||||||
|
'icon' => get_term_meta( $amenity->term_id, 'amenity_icon', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gallery images.
|
||||||
|
$gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true );
|
||||||
|
$gallery = array();
|
||||||
|
if ( $gallery_ids ) {
|
||||||
|
$ids = explode( ',', $gallery_ids );
|
||||||
|
foreach ( $ids as $id ) {
|
||||||
|
$image = wp_get_attachment_image_src( (int) $id, 'large' );
|
||||||
|
if ( $image ) {
|
||||||
|
$gallery[] = array(
|
||||||
|
'id' => (int) $id,
|
||||||
|
'url' => $image[0],
|
||||||
|
'width' => $image[1],
|
||||||
|
'height' => $image[2],
|
||||||
|
'thumb' => wp_get_attachment_image_src( (int) $id, 'thumbnail' )[0] ?? $image[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get pricing.
|
||||||
|
$pricing = Calculator::getRoomPricing( $room->ID );
|
||||||
|
$nightly_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? null;
|
||||||
|
|
||||||
|
// Calculate stay price if dates provided.
|
||||||
|
$stay_price = null;
|
||||||
|
$nights = 0;
|
||||||
|
if ( ! empty( $check_in ) && ! empty( $check_out ) ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room->ID, $check_in, $check_out );
|
||||||
|
$stay_price = $calculator->calculate();
|
||||||
|
$nights = $calculator->getNights();
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$stay_price = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => $room->post_title,
|
||||||
|
'slug' => $room->post_name,
|
||||||
|
'excerpt' => get_the_excerpt( $room ),
|
||||||
|
'content' => apply_filters( 'the_content', $room->post_content ),
|
||||||
|
'permalink' => get_permalink( $room->ID ),
|
||||||
|
'featured_image' => get_the_post_thumbnail_url( $room->ID, 'large' ),
|
||||||
|
'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'medium' ),
|
||||||
|
'gallery' => $gallery,
|
||||||
|
'building' => $building ? array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => $building->post_title,
|
||||||
|
'permalink' => get_permalink( $building->ID ),
|
||||||
|
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||||
|
) : null,
|
||||||
|
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
|
||||||
|
'floor' => (int) get_post_meta( $room->ID, '_bnb_room_floor', true ),
|
||||||
|
'capacity' => (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ),
|
||||||
|
'max_adults' => (int) get_post_meta( $room->ID, '_bnb_room_max_adults', true ),
|
||||||
|
'max_children' => (int) get_post_meta( $room->ID, '_bnb_room_max_children', true ),
|
||||||
|
'size' => (float) get_post_meta( $room->ID, '_bnb_room_size', true ),
|
||||||
|
'beds' => get_post_meta( $room->ID, '_bnb_room_beds', true ),
|
||||||
|
'bathrooms' => (float) get_post_meta( $room->ID, '_bnb_room_bathrooms', true ),
|
||||||
|
'room_types' => $room_types,
|
||||||
|
'amenities' => $amenity_list,
|
||||||
|
'nightly_price' => $nightly_price,
|
||||||
|
'price_formatted' => $nightly_price ? Calculator::formatPrice( $nightly_price ) : null,
|
||||||
|
'stay_price' => $stay_price,
|
||||||
|
'stay_price_formatted' => $stay_price ? Calculator::formatPrice( $stay_price ) : null,
|
||||||
|
'nights' => $nights,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data for search form (room types, amenities, buildings).
|
||||||
|
*
|
||||||
|
* @return array Form data.
|
||||||
|
*/
|
||||||
|
public static function get_search_form_data(): array {
|
||||||
|
// Get all room types.
|
||||||
|
$room_types = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => RoomType::TAXONOMY,
|
||||||
|
'hide_empty' => true,
|
||||||
|
'orderby' => 'meta_value_num',
|
||||||
|
'meta_key' => 'room_type_sort_order',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all amenities.
|
||||||
|
$amenities = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => Amenity::TAXONOMY,
|
||||||
|
'hide_empty' => true,
|
||||||
|
'orderby' => 'name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all buildings with rooms.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter buildings to only those with rooms.
|
||||||
|
$buildings_with_rooms = array();
|
||||||
|
foreach ( $buildings as $building ) {
|
||||||
|
$rooms = Room::get_rooms_for_building( $building->ID );
|
||||||
|
if ( ! empty( $rooms ) ) {
|
||||||
|
$buildings_with_rooms[] = array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => $building->post_title,
|
||||||
|
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get price range from all rooms.
|
||||||
|
$price_range = self::get_price_range();
|
||||||
|
|
||||||
|
// Get capacity range.
|
||||||
|
$capacity_range = self::get_capacity_range();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'room_types' => array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'parent' => $term->parent,
|
||||||
|
'count' => $term->count,
|
||||||
|
'capacity' => (int) get_term_meta( $term->term_id, 'room_type_base_capacity', true ),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$room_types
|
||||||
|
),
|
||||||
|
'amenities' => array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'icon' => get_term_meta( $term->term_id, 'amenity_icon', true ),
|
||||||
|
'count' => $term->count,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$amenities
|
||||||
|
),
|
||||||
|
'buildings' => $buildings_with_rooms,
|
||||||
|
'price_range' => $price_range,
|
||||||
|
'capacity_range' => $capacity_range,
|
||||||
|
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price range from all rooms.
|
||||||
|
*
|
||||||
|
* @return array Min and max prices.
|
||||||
|
*/
|
||||||
|
public static function get_price_range(): array {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$meta_key = '_bnb_room_price_' . PricingTier::SHORT_TERM->value;
|
||||||
|
|
||||||
|
$result = $wpdb->get_row(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT MIN(CAST(meta_value AS DECIMAL(10,2))) as min_price,
|
||||||
|
MAX(CAST(meta_value AS DECIMAL(10,2))) as max_price
|
||||||
|
FROM {$wpdb->postmeta} pm
|
||||||
|
JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||||
|
WHERE pm.meta_key = %s
|
||||||
|
AND pm.meta_value != ''
|
||||||
|
AND pm.meta_value > 0
|
||||||
|
AND p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'",
|
||||||
|
$meta_key,
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'min' => $result ? (float) $result->min_price : 0,
|
||||||
|
'max' => $result ? (float) $result->max_price : 500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get capacity range from all rooms.
|
||||||
|
*
|
||||||
|
* @return array Min and max capacity.
|
||||||
|
*/
|
||||||
|
public static function get_capacity_range(): array {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_row(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT MIN(CAST(meta_value AS UNSIGNED)) as min_capacity,
|
||||||
|
MAX(CAST(meta_value AS UNSIGNED)) as max_capacity
|
||||||
|
FROM {$wpdb->postmeta} pm
|
||||||
|
JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||||
|
WHERE pm.meta_key = '_bnb_room_capacity'
|
||||||
|
AND pm.meta_value != ''
|
||||||
|
AND p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'",
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'min' => $result && $result->min_capacity ? (int) $result->min_capacity : 1,
|
||||||
|
'max' => $result && $result->max_capacity ? (int) $result->max_capacity : 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for room search.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_search_rooms(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$args = array(
|
||||||
|
'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'] ) ) : '',
|
||||||
|
'guests' => isset( $_POST['guests'] ) ? absint( $_POST['guests'] ) : 0,
|
||||||
|
'room_type' => isset( $_POST['room_type'] ) ? sanitize_text_field( wp_unslash( $_POST['room_type'] ) ) : '',
|
||||||
|
'amenities' => isset( $_POST['amenities'] ) ? array_map( 'sanitize_text_field', (array) $_POST['amenities'] ) : array(),
|
||||||
|
'price_min' => isset( $_POST['price_min'] ) ? (float) $_POST['price_min'] : 0,
|
||||||
|
'price_max' => isset( $_POST['price_max'] ) ? (float) $_POST['price_max'] : 0,
|
||||||
|
'building_id' => isset( $_POST['building_id'] ) ? absint( $_POST['building_id'] ) : 0,
|
||||||
|
'orderby' => isset( $_POST['orderby'] ) ? sanitize_text_field( wp_unslash( $_POST['orderby'] ) ) : 'title',
|
||||||
|
'order' => isset( $_POST['order'] ) ? sanitize_text_field( wp_unslash( $_POST['order'] ) ) : 'ASC',
|
||||||
|
'limit' => isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 12,
|
||||||
|
'offset' => isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0,
|
||||||
|
);
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
// Validate dates if provided.
|
||||||
|
if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||||
|
$check_in = strtotime( $args['check_in'] );
|
||||||
|
$check_out = strtotime( $args['check_out'] );
|
||||||
|
|
||||||
|
if ( ! $check_in || ! $check_out || $check_out <= $check_in ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Invalid date range.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $check_in < strtotime( 'today' ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Check-in date cannot be in the past.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = self::search( $args );
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'rooms' => $results,
|
||||||
|
'count' => count( $results ),
|
||||||
|
'args' => $args,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for availability check.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_get_availability(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$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'] ) ) : '';
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = Availability::is_available( $room_id, $check_in, $check_out );
|
||||||
|
|
||||||
|
$result = array(
|
||||||
|
'available' => $available,
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $available ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
|
||||||
|
$result['price'] = $price;
|
||||||
|
$result['price_formatted'] = Calculator::formatPrice( $price );
|
||||||
|
$result['nights'] = $calculator->getNights();
|
||||||
|
$result['breakdown'] = $calculator->getBreakdown();
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$result['price_error'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for calendar data.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_get_calendar(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||||
|
$year = isset( $_POST['year'] ) ? absint( $_POST['year'] ) : (int) gmdate( 'Y' );
|
||||||
|
$month = isset( $_POST['month'] ) ? absint( $_POST['month'] ) : (int) gmdate( 'n' );
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Room ID is required.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate month.
|
||||||
|
$month = max( 1, min( 12, $month ) );
|
||||||
|
|
||||||
|
// Get calendar data.
|
||||||
|
$calendar = Availability::get_calendar_data( $room_id, $year, $month );
|
||||||
|
|
||||||
|
// Simplify for frontend (remove booking details, just show availability).
|
||||||
|
$days = array();
|
||||||
|
foreach ( $calendar['days'] as $day_num => $day_data ) {
|
||||||
|
$days[ $day_num ] = array(
|
||||||
|
'date' => $day_data['date'],
|
||||||
|
'day' => $day_data['day'],
|
||||||
|
'available' => ! $day_data['is_booked'],
|
||||||
|
'is_past' => $day_data['is_past'],
|
||||||
|
'is_today' => $day_data['is_today'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'month_name' => $calendar['month_name'],
|
||||||
|
'days_in_month' => $calendar['days_in_month'],
|
||||||
|
'first_day_of_week' => $calendar['first_day_of_week'],
|
||||||
|
'days' => $days,
|
||||||
|
'prev_month' => $calendar['prev_month'],
|
||||||
|
'next_month' => $calendar['next_month'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for price calculation.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_calculate_price(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$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'] ) ) : '';
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
$breakdown = $calculator->getBreakdown();
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $calculator->getNights(),
|
||||||
|
'price' => $price,
|
||||||
|
'price_formatted' => Calculator::formatPrice( $price ),
|
||||||
|
'tier' => $breakdown['tier'] ?? null,
|
||||||
|
'breakdown' => $breakdown,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => $e->getMessage() )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
867
src/Frontend/Shortcodes.php
Normal file
867
src/Frontend/Shortcodes.php
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Frontend shortcodes.
|
||||||
|
*
|
||||||
|
* Handles all shortcode registration and rendering.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcodes class.
|
||||||
|
*/
|
||||||
|
final class Shortcodes {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize shortcodes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_shortcode( 'bnb_buildings', array( self::class, 'render_buildings' ) );
|
||||||
|
add_shortcode( 'bnb_rooms', array( self::class, 'render_rooms' ) );
|
||||||
|
add_shortcode( 'bnb_room_search', array( self::class, 'render_room_search' ) );
|
||||||
|
add_shortcode( 'bnb_building', array( self::class, 'render_single_building' ) );
|
||||||
|
add_shortcode( 'bnb_room', array( self::class, 'render_single_room' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render buildings list/grid shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_buildings( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'limit' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'show_image' => 'yes',
|
||||||
|
'show_address' => 'yes',
|
||||||
|
'show_rooms_count' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_buildings'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query buildings.
|
||||||
|
$query_args = array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => (int) $atts['limit'],
|
||||||
|
'orderby' => sanitize_text_field( $atts['orderby'] ),
|
||||||
|
'order' => strtoupper( $atts['order'] ) === 'DESC' ? 'DESC' : 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$buildings = get_posts( $query_args );
|
||||||
|
|
||||||
|
if ( empty( $buildings ) ) {
|
||||||
|
return '<p class="wp-bnb-no-results">' . esc_html__( 'No buildings found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$layout = sanitize_text_field( $atts['layout'] );
|
||||||
|
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||||
|
|
||||||
|
$classes = array(
|
||||||
|
'wp-bnb-buildings',
|
||||||
|
'wp-bnb-buildings-' . $layout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( 'grid' === $layout ) {
|
||||||
|
$classes[] = 'wp-bnb-columns-' . $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<?php echo self::render_building_card( $building, $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single building card.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $building Building post.
|
||||||
|
* @param array $atts Display attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
private static function render_building_card( \WP_Post $building, array $atts ): string {
|
||||||
|
$show_image = 'yes' === $atts['show_image'];
|
||||||
|
$show_address = 'yes' === $atts['show_address'];
|
||||||
|
$show_rooms_count = 'yes' === $atts['show_rooms_count'];
|
||||||
|
|
||||||
|
// Get room count.
|
||||||
|
$rooms = Room::get_rooms_for_building( $building->ID );
|
||||||
|
$room_count = count( $rooms );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-building-card">
|
||||||
|
<?php if ( $show_image && has_post_thumbnail( $building->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-image">
|
||||||
|
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>">
|
||||||
|
<?php echo get_the_post_thumbnail( $building->ID, 'medium_large' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-building-content">
|
||||||
|
<h3 class="wp-bnb-building-title">
|
||||||
|
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>">
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<?php if ( $show_address ) : ?>
|
||||||
|
<?php
|
||||||
|
$city = get_post_meta( $building->ID, '_bnb_building_city', true );
|
||||||
|
$country = get_post_meta( $building->ID, '_bnb_building_country', true );
|
||||||
|
if ( $city || $country ) :
|
||||||
|
$countries = Building::get_countries();
|
||||||
|
$country_name = $countries[ $country ] ?? $country;
|
||||||
|
?>
|
||||||
|
<p class="wp-bnb-building-address">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<?php echo esc_html( implode( ', ', array_filter( array( $city, $country_name ) ) ) ); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_rooms_count && $room_count > 0 ) : ?>
|
||||||
|
<p class="wp-bnb-building-rooms">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of rooms */
|
||||||
|
esc_html( _n( '%d room', '%d rooms', $room_count, 'wp-bnb' ) ),
|
||||||
|
(int) $room_count
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( has_excerpt( $building->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-excerpt">
|
||||||
|
<?php echo wp_kses_post( get_the_excerpt( $building->ID ) ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>" class="wp-bnb-button">
|
||||||
|
<?php esc_html_e( 'View Details', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render rooms list/grid shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_rooms( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'limit' => 12,
|
||||||
|
'building_id' => 0,
|
||||||
|
'room_type' => '',
|
||||||
|
'amenities' => '',
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'show_image' => 'yes',
|
||||||
|
'show_price' => 'yes',
|
||||||
|
'show_capacity' => 'yes',
|
||||||
|
'show_amenities' => 'yes',
|
||||||
|
'show_building' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_rooms'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use search function for filtering.
|
||||||
|
$search_args = array(
|
||||||
|
'building_id' => (int) $atts['building_id'],
|
||||||
|
'room_type' => sanitize_text_field( $atts['room_type'] ),
|
||||||
|
'amenities' => $atts['amenities'] ? explode( ',', $atts['amenities'] ) : array(),
|
||||||
|
'orderby' => sanitize_text_field( $atts['orderby'] ),
|
||||||
|
'order' => $atts['order'],
|
||||||
|
'limit' => (int) $atts['limit'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = Search::search( $search_args );
|
||||||
|
|
||||||
|
if ( empty( $rooms ) ) {
|
||||||
|
return '<p class="wp-bnb-no-results">' . esc_html__( 'No rooms found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$layout = sanitize_text_field( $atts['layout'] );
|
||||||
|
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||||
|
|
||||||
|
$classes = array(
|
||||||
|
'wp-bnb-rooms',
|
||||||
|
'wp-bnb-rooms-' . $layout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( 'grid' === $layout ) {
|
||||||
|
$classes[] = 'wp-bnb-columns-' . $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||||
|
<?php foreach ( $rooms as $room_data ) : ?>
|
||||||
|
<?php echo self::render_room_card( $room_data, $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single room card.
|
||||||
|
*
|
||||||
|
* @param array $room Room data array.
|
||||||
|
* @param array $atts Display attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
private static function render_room_card( array $room, array $atts ): string {
|
||||||
|
$show_image = 'yes' === $atts['show_image'];
|
||||||
|
$show_price = 'yes' === $atts['show_price'];
|
||||||
|
$show_capacity = 'yes' === $atts['show_capacity'];
|
||||||
|
$show_amenities = 'yes' === $atts['show_amenities'];
|
||||||
|
$show_building = 'yes' === $atts['show_building'];
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-room-card" data-room-id="<?php echo esc_attr( $room['id'] ); ?>">
|
||||||
|
<?php if ( $show_image && ! empty( $room['featured_image'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-image">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<img src="<?php echo esc_url( $room['featured_image'] ); ?>" alt="<?php echo esc_attr( $room['title'] ); ?>">
|
||||||
|
</a>
|
||||||
|
<?php if ( ! empty( $room['room_types'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-type-badge"><?php echo esc_html( $room['room_types'][0] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-content">
|
||||||
|
<h3 class="wp-bnb-room-title">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<?php if ( $show_building && ! empty( $room['building'] ) ) : ?>
|
||||||
|
<p class="wp-bnb-room-building">
|
||||||
|
<span class="dashicons dashicons-building"></span>
|
||||||
|
<a href="<?php echo esc_url( $room['building']['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room['building']['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( ! empty( $room['building']['city'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-city">, <?php echo esc_html( $room['building']['city'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-meta">
|
||||||
|
<?php if ( $show_capacity && ! empty( $room['capacity'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-capacity">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of guests */
|
||||||
|
esc_html( _n( '%d guest', '%d guests', $room['capacity'], 'wp-bnb' ) ),
|
||||||
|
(int) $room['capacity']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['size'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-size">
|
||||||
|
<span class="dashicons dashicons-editor-expand"></span>
|
||||||
|
<?php echo esc_html( $room['size'] ); ?> m²
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['beds'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-beds">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
<?php echo esc_html( $room['beds'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( $show_amenities && ! empty( $room['amenities'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-amenities">
|
||||||
|
<?php foreach ( array_slice( $room['amenities'], 0, 4 ) as $amenity ) : ?>
|
||||||
|
<span class="wp-bnb-amenity" title="<?php echo esc_attr( $amenity['name'] ); ?>">
|
||||||
|
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="wp-bnb-amenity-name"><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if ( count( $room['amenities'] ) > 4 ) : ?>
|
||||||
|
<span class="wp-bnb-amenity-more">
|
||||||
|
+<?php echo (int) ( count( $room['amenities'] ) - 4 ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-footer">
|
||||||
|
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-price">
|
||||||
|
<span class="wp-bnb-price-amount"><?php echo esc_html( $room['price_formatted'] ); ?></span>
|
||||||
|
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>" class="wp-bnb-button">
|
||||||
|
<?php esc_html_e( 'View Details', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room search form with results.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_room_search( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'show_dates' => 'yes',
|
||||||
|
'show_guests' => 'yes',
|
||||||
|
'show_room_type' => 'yes',
|
||||||
|
'show_amenities' => 'yes',
|
||||||
|
'show_price_range' => 'yes',
|
||||||
|
'show_building' => 'yes',
|
||||||
|
'results_per_page' => 12,
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_room_search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get search form data.
|
||||||
|
$form_data = Search::get_search_form_data();
|
||||||
|
|
||||||
|
$layout = sanitize_text_field( $atts['layout'] );
|
||||||
|
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-room-search" data-layout="<?php echo esc_attr( $layout ); ?>" data-columns="<?php echo esc_attr( $columns ); ?>" data-per-page="<?php echo esc_attr( $atts['results_per_page'] ); ?>">
|
||||||
|
<form class="wp-bnb-search-form" id="wp-bnb-search-form">
|
||||||
|
<div class="wp-bnb-search-fields">
|
||||||
|
<?php if ( 'yes' === $atts['show_dates'] ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-dates">
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-check-in"><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-check-in" name="check_in" min="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-check-out"><?php esc_html_e( 'Check-out', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-check-out" name="check_out" min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_guests'] ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-guests">
|
||||||
|
<label for="wp-bnb-guests"><?php esc_html_e( 'Guests', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-guests" name="guests">
|
||||||
|
<option value=""><?php esc_html_e( 'Any', 'wp-bnb' ); ?></option>
|
||||||
|
<?php for ( $i = 1; $i <= $form_data['capacity_range']['max']; $i++ ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $i ); ?>">
|
||||||
|
<?php echo esc_html( $i ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_room_type'] && ! empty( $form_data['room_types'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-room-type">
|
||||||
|
<label for="wp-bnb-room-type"><?php esc_html_e( 'Room Type', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-room-type" name="room_type">
|
||||||
|
<option value=""><?php esc_html_e( 'All Types', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $form_data['room_types'] as $type ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $type['slug'] ); ?>">
|
||||||
|
<?php echo esc_html( $type['name'] ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_building'] && ! empty( $form_data['buildings'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-building">
|
||||||
|
<label for="wp-bnb-building"><?php esc_html_e( 'Building', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-building" name="building_id">
|
||||||
|
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $form_data['buildings'] as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building['id'] ); ?>">
|
||||||
|
<?php echo esc_html( $building['title'] ); ?>
|
||||||
|
<?php if ( ! empty( $building['city'] ) ) : ?>
|
||||||
|
(<?php echo esc_html( $building['city'] ); ?>)
|
||||||
|
<?php endif; ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_price_range'] && $form_data['price_range']['max'] > 0 ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-price-range">
|
||||||
|
<label><?php esc_html_e( 'Price Range', 'wp-bnb' ); ?></label>
|
||||||
|
<div class="wp-bnb-price-range-inputs">
|
||||||
|
<input type="number" id="wp-bnb-price-min" name="price_min" placeholder="<?php esc_attr_e( 'Min', 'wp-bnb' ); ?>" min="0" step="10">
|
||||||
|
<span class="wp-bnb-price-separator">-</span>
|
||||||
|
<input type="number" id="wp-bnb-price-max" name="price_max" placeholder="<?php esc_attr_e( 'Max', 'wp-bnb' ); ?>" min="0" step="10">
|
||||||
|
<span class="wp-bnb-currency"><?php echo esc_html( $form_data['currency'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_amenities'] && ! empty( $form_data['amenities'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-search-amenities">
|
||||||
|
<label><?php esc_html_e( 'Amenities', 'wp-bnb' ); ?></label>
|
||||||
|
<div class="wp-bnb-amenities-list">
|
||||||
|
<?php foreach ( $form_data['amenities'] as $amenity ) : ?>
|
||||||
|
<label class="wp-bnb-amenity-checkbox">
|
||||||
|
<input type="checkbox" name="amenities[]" value="<?php echo esc_attr( $amenity['slug'] ); ?>">
|
||||||
|
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-actions">
|
||||||
|
<button type="submit" class="wp-bnb-button wp-bnb-button-primary">
|
||||||
|
<span class="dashicons dashicons-search"></span>
|
||||||
|
<?php esc_html_e( 'Search Rooms', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
<button type="reset" class="wp-bnb-button wp-bnb-button-secondary">
|
||||||
|
<?php esc_html_e( 'Clear', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-results-container">
|
||||||
|
<div class="wp-bnb-search-status">
|
||||||
|
<span class="wp-bnb-results-count"></span>
|
||||||
|
<div class="wp-bnb-sort-options">
|
||||||
|
<label for="wp-bnb-sort"><?php esc_html_e( 'Sort by:', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-sort" name="orderby">
|
||||||
|
<option value="title"><?php esc_html_e( 'Name', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="price"><?php esc_html_e( 'Price', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="capacity"><?php esc_html_e( 'Capacity', 'wp-bnb' ); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-results wp-bnb-rooms wp-bnb-rooms-<?php echo esc_attr( $layout ); ?> wp-bnb-columns-<?php echo esc_attr( $columns ); ?>">
|
||||||
|
<div class="wp-bnb-loading">
|
||||||
|
<span class="wp-bnb-spinner"></span>
|
||||||
|
<span><?php esc_html_e( 'Loading rooms...', 'wp-bnb' ); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-pagination">
|
||||||
|
<button type="button" class="wp-bnb-button wp-bnb-load-more" style="display:none;">
|
||||||
|
<?php esc_html_e( 'Load More', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render single building shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_single_building( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'id' => 0,
|
||||||
|
'show_rooms' => 'yes',
|
||||||
|
'show_address' => 'yes',
|
||||||
|
'show_contact' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_building'
|
||||||
|
);
|
||||||
|
|
||||||
|
$building_id = (int) $atts['id'];
|
||||||
|
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Building ID is required.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
|
||||||
|
if ( ! $building || Building::POST_TYPE !== $building->post_type ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Building not found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$show_rooms = 'yes' === $atts['show_rooms'];
|
||||||
|
$show_address = 'yes' === $atts['show_address'];
|
||||||
|
$show_contact = 'yes' === $atts['show_contact'];
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-building-single">
|
||||||
|
<?php if ( has_post_thumbnail( $building->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-featured-image">
|
||||||
|
<?php echo get_the_post_thumbnail( $building->ID, 'large' ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-building-header">
|
||||||
|
<h2 class="wp-bnb-building-title"><?php echo esc_html( $building->post_title ); ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-building-details">
|
||||||
|
<?php if ( $show_address ) : ?>
|
||||||
|
<?php $address = Building::get_formatted_address( $building->ID ); ?>
|
||||||
|
<?php if ( ! empty( $address ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-address">
|
||||||
|
<h4><?php esc_html_e( 'Address', 'wp-bnb' ); ?></h4>
|
||||||
|
<address><?php echo nl2br( esc_html( $address ) ); ?></address>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_contact ) : ?>
|
||||||
|
<?php
|
||||||
|
$phone = get_post_meta( $building->ID, '_bnb_building_phone', true );
|
||||||
|
$email = get_post_meta( $building->ID, '_bnb_building_email', true );
|
||||||
|
$website = get_post_meta( $building->ID, '_bnb_building_website', true );
|
||||||
|
?>
|
||||||
|
<?php if ( $phone || $email || $website ) : ?>
|
||||||
|
<div class="wp-bnb-building-contact">
|
||||||
|
<h4><?php esc_html_e( 'Contact', 'wp-bnb' ); ?></h4>
|
||||||
|
<?php if ( $phone ) : ?>
|
||||||
|
<p class="wp-bnb-contact-phone">
|
||||||
|
<span class="dashicons dashicons-phone"></span>
|
||||||
|
<a href="tel:<?php echo esc_attr( $phone ); ?>"><?php echo esc_html( $phone ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $email ) : ?>
|
||||||
|
<p class="wp-bnb-contact-email">
|
||||||
|
<span class="dashicons dashicons-email"></span>
|
||||||
|
<a href="mailto:<?php echo esc_attr( $email ); ?>"><?php echo esc_html( $email ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $website ) : ?>
|
||||||
|
<p class="wp-bnb-contact-website">
|
||||||
|
<span class="dashicons dashicons-admin-site"></span>
|
||||||
|
<a href="<?php echo esc_url( $website ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $website ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$check_in_time = get_post_meta( $building->ID, '_bnb_building_check_in_time', true );
|
||||||
|
$check_out_time = get_post_meta( $building->ID, '_bnb_building_check_out_time', true );
|
||||||
|
if ( $check_in_time || $check_out_time ) :
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-building-times">
|
||||||
|
<h4><?php esc_html_e( 'Check-in / Check-out', 'wp-bnb' ); ?></h4>
|
||||||
|
<?php if ( $check_in_time ) : ?>
|
||||||
|
<p><strong><?php esc_html_e( 'Check-in:', 'wp-bnb' ); ?></strong> <?php echo esc_html( $check_in_time ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $check_out_time ) : ?>
|
||||||
|
<p><strong><?php esc_html_e( 'Check-out:', 'wp-bnb' ); ?></strong> <?php echo esc_html( $check_out_time ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $building->post_content ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-description">
|
||||||
|
<?php echo wp_kses_post( apply_filters( 'the_content', $building->post_content ) ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_rooms ) : ?>
|
||||||
|
<?php $rooms = Room::get_rooms_for_building( $building->ID ); ?>
|
||||||
|
<?php if ( ! empty( $rooms ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-rooms">
|
||||||
|
<h3><?php esc_html_e( 'Available Rooms', 'wp-bnb' ); ?></h3>
|
||||||
|
<?php
|
||||||
|
echo self::render_rooms(
|
||||||
|
array(
|
||||||
|
'building_id' => $building->ID,
|
||||||
|
'show_building' => 'no',
|
||||||
|
'limit' => -1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render single room shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_single_room( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'id' => 0,
|
||||||
|
'show_gallery' => 'yes',
|
||||||
|
'show_pricing' => 'yes',
|
||||||
|
'show_amenities' => 'yes',
|
||||||
|
'show_availability' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_room'
|
||||||
|
);
|
||||||
|
|
||||||
|
$room_id = (int) $atts['id'];
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Room ID is required.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Room not found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$show_gallery = 'yes' === $atts['show_gallery'];
|
||||||
|
$show_pricing = 'yes' === $atts['show_pricing'];
|
||||||
|
$show_amenities = 'yes' === $atts['show_amenities'];
|
||||||
|
$show_availability = 'yes' === $atts['show_availability'];
|
||||||
|
|
||||||
|
// Get room data.
|
||||||
|
$room_data = Search::get_room_data( $room );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-room-single" data-room-id="<?php echo esc_attr( $room->ID ); ?>">
|
||||||
|
<?php if ( $show_gallery && ( has_post_thumbnail( $room->ID ) || ! empty( $room_data['gallery'] ) ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-gallery">
|
||||||
|
<?php if ( has_post_thumbnail( $room->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-featured-image">
|
||||||
|
<?php echo get_the_post_thumbnail( $room->ID, 'large' ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['gallery'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-gallery-thumbnails">
|
||||||
|
<?php foreach ( $room_data['gallery'] as $image ) : ?>
|
||||||
|
<a href="<?php echo esc_url( $image['url'] ); ?>" class="wp-bnb-gallery-thumb" data-gallery>
|
||||||
|
<img src="<?php echo esc_url( $image['thumb'] ); ?>" alt="">
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-header">
|
||||||
|
<div class="wp-bnb-room-header-content">
|
||||||
|
<h2 class="wp-bnb-room-title"><?php echo esc_html( $room->post_title ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['building'] ) ) : ?>
|
||||||
|
<p class="wp-bnb-room-building">
|
||||||
|
<span class="dashicons dashicons-building"></span>
|
||||||
|
<a href="<?php echo esc_url( $room_data['building']['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room_data['building']['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( ! empty( $room_data['building']['city'] ) ) : ?>
|
||||||
|
<span>, <?php echo esc_html( $room_data['building']['city'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['room_types'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-type"><?php echo esc_html( implode( ', ', $room_data['room_types'] ) ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( $show_pricing && ! empty( $room_data['price_formatted'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-header-price">
|
||||||
|
<span class="wp-bnb-price-label"><?php esc_html_e( 'From', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-price-amount"><?php echo esc_html( $room_data['price_formatted'] ); ?></span>
|
||||||
|
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-info">
|
||||||
|
<div class="wp-bnb-room-specs">
|
||||||
|
<?php if ( ! empty( $room_data['capacity'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Capacity', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of guests */
|
||||||
|
esc_html( _n( '%d guest', '%d guests', $room_data['capacity'], 'wp-bnb' ) ),
|
||||||
|
(int) $room_data['capacity']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['size'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-editor-expand"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Size', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['size'] ); ?> m²</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['beds'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Beds', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['beds'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['bathrooms'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-admin-page"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Bathrooms', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['bathrooms'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['floor'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-building"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Floor', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['floor'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( $show_amenities && ! empty( $room_data['amenities'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-amenities-full">
|
||||||
|
<h4><?php esc_html_e( 'Amenities', 'wp-bnb' ); ?></h4>
|
||||||
|
<ul class="wp-bnb-amenities-list">
|
||||||
|
<?php foreach ( $room_data['amenities'] as $amenity ) : ?>
|
||||||
|
<li class="wp-bnb-amenity">
|
||||||
|
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room->post_content ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-description">
|
||||||
|
<?php echo wp_kses_post( apply_filters( 'the_content', $room->post_content ) ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_pricing ) : ?>
|
||||||
|
<?php $pricing = Calculator::getRoomPricing( $room->ID ); ?>
|
||||||
|
<div class="wp-bnb-room-pricing-details">
|
||||||
|
<h4><?php esc_html_e( 'Pricing', 'wp-bnb' ); ?></h4>
|
||||||
|
<table class="wp-bnb-pricing-table">
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( PricingTier::cases() as $tier ) : ?>
|
||||||
|
<?php $price = $pricing[ $tier->value ]['price'] ?? null; ?>
|
||||||
|
<?php if ( $price ) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="wp-bnb-tier-label"><?php echo esc_html( $tier->label() ); ?></td>
|
||||||
|
<td class="wp-bnb-tier-price">
|
||||||
|
<?php echo esc_html( Calculator::formatPrice( $price ) ); ?>
|
||||||
|
<span class="wp-bnb-tier-unit"><?php echo esc_html( $tier->unit() ); ?></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_availability ) : ?>
|
||||||
|
<div class="wp-bnb-room-availability">
|
||||||
|
<h4><?php esc_html_e( 'Check Availability', 'wp-bnb' ); ?></h4>
|
||||||
|
<form class="wp-bnb-availability-form" data-room-id="<?php echo esc_attr( $room->ID ); ?>">
|
||||||
|
<div class="wp-bnb-availability-fields">
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-avail-check-in"><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-avail-check-in" name="check_in" min="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-avail-check-out"><?php esc_html_e( 'Check-out', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-avail-check-out" name="check_out" min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="wp-bnb-button wp-bnb-button-primary">
|
||||||
|
<?php esc_html_e( 'Check', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-availability-result" style="display:none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
298
src/Frontend/Widgets/AvailabilityCalendar.php
Normal file
298
src/Frontend/Widgets/AvailabilityCalendar.php
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Availability Calendar widget.
|
||||||
|
*
|
||||||
|
* Displays a mini calendar showing room availability.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend\Widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability Calendar widget class.
|
||||||
|
*/
|
||||||
|
class AvailabilityCalendar extends \WP_Widget {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'wp_bnb_availability_calendar',
|
||||||
|
__( 'WP BnB: Availability Calendar', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'classname' => 'wp-bnb-widget-availability-calendar',
|
||||||
|
'description' => __( 'Display a room availability calendar.', 'wp-bnb' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget content.
|
||||||
|
*
|
||||||
|
* @param array $args Widget arguments.
|
||||||
|
* @param array $instance Widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function widget( $args, $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Availability', 'wp-bnb' );
|
||||||
|
$room_id = ! empty( $instance['room_id'] ) ? (int) $instance['room_id'] : 0;
|
||||||
|
$months_to_show = ! empty( $instance['months'] ) ? (int) $instance['months'] : 1;
|
||||||
|
$show_legend = ! empty( $instance['show_legend'] );
|
||||||
|
$show_navigation = ! empty( $instance['show_navigation'] );
|
||||||
|
|
||||||
|
// Auto-detect room from single room page.
|
||||||
|
if ( ! $room_id && is_singular( Room::POST_TYPE ) ) {
|
||||||
|
$room_id = get_the_ID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit months to show.
|
||||||
|
$months_to_show = max( 1, min( 3, $months_to_show ) );
|
||||||
|
|
||||||
|
// Get current month or from request.
|
||||||
|
$year = (int) gmdate( 'Y' );
|
||||||
|
$month = (int) gmdate( 'n' );
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
if ( $title ) {
|
||||||
|
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-availability-calendar-widget" data-room-id="<?php echo esc_attr( $room_id ); ?>">
|
||||||
|
<?php for ( $i = 0; $i < $months_to_show; $i++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$display_year = $year;
|
||||||
|
$display_month = $month + $i;
|
||||||
|
|
||||||
|
if ( $display_month > 12 ) {
|
||||||
|
$display_month -= 12;
|
||||||
|
$display_year++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$calendar = Availability::get_calendar_data( $room_id, $display_year, $display_month );
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-calendar-month" data-year="<?php echo esc_attr( $display_year ); ?>" data-month="<?php echo esc_attr( $display_month ); ?>">
|
||||||
|
<div class="wp-bnb-calendar-header">
|
||||||
|
<?php if ( $show_navigation && 0 === $i ) : ?>
|
||||||
|
<button type="button" class="wp-bnb-calendar-nav wp-bnb-calendar-prev" data-direction="prev" aria-label="<?php esc_attr_e( 'Previous month', 'wp-bnb' ); ?>">
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<span class="wp-bnb-calendar-month-name">
|
||||||
|
<?php echo esc_html( $calendar['month_name'] . ' ' . $display_year ); ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<?php if ( $show_navigation && $i === $months_to_show - 1 ) : ?>
|
||||||
|
<button type="button" class="wp-bnb-calendar-nav wp-bnb-calendar-next" data-direction="next" aria-label="<?php esc_attr_e( 'Next month', 'wp-bnb' ); ?>">
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="wp-bnb-calendar-grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Su', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Mo', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Tu', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'We', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Th', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Fr', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Sa', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
$day = 1;
|
||||||
|
$total_days = $calendar['days_in_month'];
|
||||||
|
$first_day = $calendar['first_day_of_week']; // 0 = Sunday.
|
||||||
|
|
||||||
|
// Calculate weeks.
|
||||||
|
$weeks = ceil( ( $first_day + $total_days ) / 7 );
|
||||||
|
|
||||||
|
for ( $week = 0; $week < $weeks; $week++ ) :
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<?php for ( $dow = 0; $dow < 7; $dow++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$cell_index = $week * 7 + $dow;
|
||||||
|
|
||||||
|
if ( $cell_index < $first_day || $day > $total_days ) {
|
||||||
|
echo '<td class="wp-bnb-calendar-empty"></td>';
|
||||||
|
} else {
|
||||||
|
$day_data = $calendar['days'][ $day ] ?? null;
|
||||||
|
$classes = array( 'wp-bnb-calendar-day' );
|
||||||
|
|
||||||
|
if ( $day_data ) {
|
||||||
|
if ( $day_data['is_booked'] ) {
|
||||||
|
$classes[] = 'wp-bnb-booked';
|
||||||
|
} else {
|
||||||
|
$classes[] = 'wp-bnb-available';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $day_data['is_past'] ) {
|
||||||
|
$classes[] = 'wp-bnb-past';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $day_data['is_today'] ) {
|
||||||
|
$classes[] = 'wp-bnb-today';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<td class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>" data-date="<?php echo esc_attr( $day_data['date'] ?? '' ); ?>">
|
||||||
|
<?php echo esc_html( $day ); ?>
|
||||||
|
</td>
|
||||||
|
<?php
|
||||||
|
$day++;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_legend ) : ?>
|
||||||
|
<div class="wp-bnb-calendar-legend">
|
||||||
|
<span class="wp-bnb-legend-item wp-bnb-legend-available">
|
||||||
|
<span class="wp-bnb-legend-color"></span>
|
||||||
|
<?php esc_html_e( 'Available', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
<span class="wp-bnb-legend-item wp-bnb-legend-booked">
|
||||||
|
<span class="wp-bnb-legend-color"></span>
|
||||||
|
<?php esc_html_e( 'Booked', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget settings form.
|
||||||
|
*
|
||||||
|
* @param array $instance Current widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function form( $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Availability', 'wp-bnb' );
|
||||||
|
$room_id = ! empty( $instance['room_id'] ) ? (int) $instance['room_id'] : 0;
|
||||||
|
$months = ! empty( $instance['months'] ) ? (int) $instance['months'] : 1;
|
||||||
|
$show_legend = ! empty( $instance['show_legend'] );
|
||||||
|
$show_navigation = ! empty( $instance['show_navigation'] );
|
||||||
|
|
||||||
|
// Get all rooms.
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
|
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'room_id' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Room:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'room_id' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'room_id' ) ); ?>">
|
||||||
|
<option value="0"><?php esc_html_e( '— Auto-detect from page —', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $rooms as $room ) : ?>
|
||||||
|
<?php
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
?>
|
||||||
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
<?php if ( $building ) : ?>
|
||||||
|
(<?php echo esc_html( $building->post_title ); ?>)
|
||||||
|
<?php endif; ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small><?php esc_html_e( 'Leave as auto-detect to show calendar of the current room page.', 'wp-bnb' ); ?></small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'months' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Months to show:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'months' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'months' ) ); ?>">
|
||||||
|
<option value="1" <?php selected( $months, 1 ); ?>>1</option>
|
||||||
|
<option value="2" <?php selected( $months, 2 ); ?>>2</option>
|
||||||
|
<option value="3" <?php selected( $months, 3 ); ?>>3</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_legend' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_legend' ) ); ?>"
|
||||||
|
<?php checked( $show_legend ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_legend' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show legend', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_navigation' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_navigation' ) ); ?>"
|
||||||
|
<?php checked( $show_navigation ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_navigation' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Allow navigation', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget settings.
|
||||||
|
*
|
||||||
|
* @param array $new_instance New settings.
|
||||||
|
* @param array $old_instance Old settings.
|
||||||
|
* @return array Updated settings.
|
||||||
|
*/
|
||||||
|
public function update( $new_instance, $old_instance ): array {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['room_id'] = ! empty( $new_instance['room_id'] ) ? absint( $new_instance['room_id'] ) : 0;
|
||||||
|
$instance['months'] = ! empty( $new_instance['months'] ) ? absint( $new_instance['months'] ) : 1;
|
||||||
|
$instance['show_legend'] = ! empty( $new_instance['show_legend'] );
|
||||||
|
$instance['show_navigation'] = ! empty( $new_instance['show_navigation'] );
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/Frontend/Widgets/BuildingRooms.php
Normal file
261
src/Frontend/Widgets/BuildingRooms.php
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Building Rooms widget.
|
||||||
|
*
|
||||||
|
* Displays all rooms in a specific building.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend\Widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building Rooms widget class.
|
||||||
|
*/
|
||||||
|
class BuildingRooms extends \WP_Widget {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'wp_bnb_building_rooms',
|
||||||
|
__( 'WP BnB: Building Rooms', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'classname' => 'wp-bnb-widget-building-rooms',
|
||||||
|
'description' => __( 'Display all rooms in a building.', 'wp-bnb' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget content.
|
||||||
|
*
|
||||||
|
* @param array $args Widget arguments.
|
||||||
|
* @param array $instance Widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function widget( $args, $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Rooms', 'wp-bnb' );
|
||||||
|
$building_id = ! empty( $instance['building_id'] ) ? (int) $instance['building_id'] : 0;
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : -1;
|
||||||
|
$show_availability = ! empty( $instance['show_availability'] );
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$layout = ! empty( $instance['layout'] ) ? $instance['layout'] : 'list';
|
||||||
|
|
||||||
|
// Auto-detect building from single building page.
|
||||||
|
if ( ! $building_id && is_singular( Building::POST_TYPE ) ) {
|
||||||
|
$building_id = get_the_ID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rooms for building.
|
||||||
|
$search_args = array(
|
||||||
|
'building_id' => $building_id,
|
||||||
|
'limit' => $count,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = Search::search( $search_args );
|
||||||
|
|
||||||
|
if ( empty( $rooms ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
if ( $title ) {
|
||||||
|
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
$list_class = 'compact' === $layout ? 'wp-bnb-building-rooms-compact' : 'wp-bnb-building-rooms-list';
|
||||||
|
echo '<ul class="' . esc_attr( $list_class ) . '">';
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$status = get_post_meta( $room['id'], '_bnb_room_status', true ) ?: 'available';
|
||||||
|
?>
|
||||||
|
<li class="wp-bnb-building-room">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>" class="wp-bnb-building-room-link">
|
||||||
|
<span class="wp-bnb-building-room-title"><?php echo esc_html( $room['title'] ); ?></span>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['room_number'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-building-room-number">#<?php echo esc_html( $room['room_number'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_availability ) : ?>
|
||||||
|
<span class="wp-bnb-building-room-status wp-bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||||
|
<?php
|
||||||
|
$statuses = Room::get_room_statuses();
|
||||||
|
echo esc_html( $statuses[ $status ] ?? $status );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-building-room-price">
|
||||||
|
<?php echo esc_html( $room['price_formatted'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if ( 'list' === $layout ) : ?>
|
||||||
|
<div class="wp-bnb-building-room-meta">
|
||||||
|
<?php if ( ! empty( $room['capacity'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-meta-item">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
<?php echo esc_html( $room['capacity'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['room_types'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-meta-item">
|
||||||
|
<?php echo esc_html( $room['room_types'][0] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</ul>';
|
||||||
|
|
||||||
|
// Show view all link if there are more rooms.
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
if ( $building && $count > 0 && count( $rooms ) >= $count ) {
|
||||||
|
$all_rooms = Room::get_rooms_for_building( $building_id );
|
||||||
|
if ( count( $all_rooms ) > $count ) {
|
||||||
|
printf(
|
||||||
|
'<a href="%s" class="wp-bnb-view-all-rooms">%s</a>',
|
||||||
|
esc_url( get_permalink( $building_id ) ),
|
||||||
|
esc_html__( 'View all rooms', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget settings form.
|
||||||
|
*
|
||||||
|
* @param array $instance Current widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function form( $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Rooms', 'wp-bnb' );
|
||||||
|
$building_id = ! empty( $instance['building_id'] ) ? (int) $instance['building_id'] : 0;
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : -1;
|
||||||
|
$show_availability = ! empty( $instance['show_availability'] );
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$layout = ! empty( $instance['layout'] ) ? $instance['layout'] : 'list';
|
||||||
|
|
||||||
|
// Get all buildings.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
|
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'building_id' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Building:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'building_id' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'building_id' ) ); ?>">
|
||||||
|
<option value="0"><?php esc_html_e( '— Auto-detect from page —', '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>
|
||||||
|
<small><?php esc_html_e( 'Leave as auto-detect to show rooms of the current building page.', 'wp-bnb' ); ?></small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Number of rooms:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||||
|
type="number" min="-1" max="50" value="<?php echo esc_attr( $count ); ?>">
|
||||||
|
<small><?php esc_html_e( '-1 for all rooms', 'wp-bnb' ); ?></small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'layout' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Layout:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'layout' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'layout' ) ); ?>">
|
||||||
|
<option value="list" <?php selected( $layout, 'list' ); ?>>
|
||||||
|
<?php esc_html_e( 'List (with details)', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="compact" <?php selected( $layout, 'compact' ); ?>>
|
||||||
|
<?php esc_html_e( 'Compact', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_availability' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_availability' ) ); ?>"
|
||||||
|
<?php checked( $show_availability ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_availability' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show availability status', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_price' ) ); ?>"
|
||||||
|
<?php checked( $show_price ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show price', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget settings.
|
||||||
|
*
|
||||||
|
* @param array $new_instance New settings.
|
||||||
|
* @param array $old_instance Old settings.
|
||||||
|
* @return array Updated settings.
|
||||||
|
*/
|
||||||
|
public function update( $new_instance, $old_instance ): array {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['building_id'] = ! empty( $new_instance['building_id'] ) ? absint( $new_instance['building_id'] ) : 0;
|
||||||
|
$instance['count'] = isset( $new_instance['count'] ) ? (int) $new_instance['count'] : -1;
|
||||||
|
$instance['show_availability'] = ! empty( $new_instance['show_availability'] );
|
||||||
|
$instance['show_price'] = ! empty( $new_instance['show_price'] );
|
||||||
|
$instance['layout'] = ! empty( $new_instance['layout'] ) ? sanitize_text_field( $new_instance['layout'] ) : 'list';
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
233
src/Frontend/Widgets/SimilarRooms.php
Normal file
233
src/Frontend/Widgets/SimilarRooms.php
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Similar Rooms widget.
|
||||||
|
*
|
||||||
|
* Displays rooms similar to the current room based on building or room type.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend\Widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar Rooms widget class.
|
||||||
|
*/
|
||||||
|
class SimilarRooms extends \WP_Widget {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'wp_bnb_similar_rooms',
|
||||||
|
__( 'WP BnB: Similar Rooms', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'classname' => 'wp-bnb-widget-similar-rooms',
|
||||||
|
'description' => __( 'Display rooms similar to the current room.', 'wp-bnb' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget content.
|
||||||
|
*
|
||||||
|
* @param array $args Widget arguments.
|
||||||
|
* @param array $instance Widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function widget( $args, $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Similar Rooms', 'wp-bnb' );
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : 3;
|
||||||
|
$match_by = ! empty( $instance['match_by'] ) ? $instance['match_by'] : 'building';
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$show_image = ! empty( $instance['show_image'] );
|
||||||
|
|
||||||
|
// Get current room.
|
||||||
|
$current_room_id = 0;
|
||||||
|
if ( is_singular( Room::POST_TYPE ) ) {
|
||||||
|
$current_room_id = get_the_ID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $current_room_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query based on match type.
|
||||||
|
$search_args = array(
|
||||||
|
'limit' => $count + 1, // Get extra in case current room is included.
|
||||||
|
);
|
||||||
|
|
||||||
|
switch ( $match_by ) {
|
||||||
|
case 'building':
|
||||||
|
$building_id = get_post_meta( $current_room_id, '_bnb_room_building_id', true );
|
||||||
|
if ( $building_id ) {
|
||||||
|
$search_args['building_id'] = (int) $building_id;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'room_type':
|
||||||
|
$terms = wp_get_post_terms( $current_room_id, RoomType::TAXONOMY, array( 'fields' => 'slugs' ) );
|
||||||
|
if ( ! empty( $terms ) ) {
|
||||||
|
$search_args['room_type'] = $terms[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'amenities':
|
||||||
|
$amenities = wp_get_post_terms( $current_room_id, 'bnb_amenity', array( 'fields' => 'slugs' ) );
|
||||||
|
if ( ! empty( $amenities ) ) {
|
||||||
|
$search_args['amenities'] = array_slice( $amenities, 0, 3 );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = Search::search( $search_args );
|
||||||
|
|
||||||
|
// Remove current room from results.
|
||||||
|
$rooms = array_filter(
|
||||||
|
$rooms,
|
||||||
|
function ( $room ) use ( $current_room_id ) {
|
||||||
|
return $room['id'] !== $current_room_id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limit to requested count.
|
||||||
|
$rooms = array_slice( $rooms, 0, $count );
|
||||||
|
|
||||||
|
if ( empty( $rooms ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
if ( $title ) {
|
||||||
|
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<ul class="wp-bnb-similar-rooms-list">';
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
?>
|
||||||
|
<li class="wp-bnb-similar-room">
|
||||||
|
<?php if ( $show_image && ! empty( $room['thumbnail'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-similar-room-image">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<img src="<?php echo esc_url( $room['thumbnail'] ); ?>" alt="<?php echo esc_attr( $room['title'] ); ?>">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-similar-room-content">
|
||||||
|
<h4 class="wp-bnb-similar-room-title">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-similar-room-price">
|
||||||
|
<?php echo esc_html( $room['price_formatted'] ); ?>
|
||||||
|
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</ul>';
|
||||||
|
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget settings form.
|
||||||
|
*
|
||||||
|
* @param array $instance Current widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function form( $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Similar Rooms', 'wp-bnb' );
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : 3;
|
||||||
|
$match_by = ! empty( $instance['match_by'] ) ? $instance['match_by'] : 'building';
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$show_image = ! empty( $instance['show_image'] );
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
|
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Number of rooms:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||||
|
type="number" min="1" max="10" value="<?php echo esc_attr( $count ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'match_by' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Match by:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'match_by' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'match_by' ) ); ?>">
|
||||||
|
<option value="building" <?php selected( $match_by, 'building' ); ?>>
|
||||||
|
<?php esc_html_e( 'Same Building', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="room_type" <?php selected( $match_by, 'room_type' ); ?>>
|
||||||
|
<?php esc_html_e( 'Same Room Type', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="amenities" <?php selected( $match_by, 'amenities' ); ?>>
|
||||||
|
<?php esc_html_e( 'Similar Amenities', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_image' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_image' ) ); ?>"
|
||||||
|
<?php checked( $show_image ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_image' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show image', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_price' ) ); ?>"
|
||||||
|
<?php checked( $show_price ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show price', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget settings.
|
||||||
|
*
|
||||||
|
* @param array $new_instance New settings.
|
||||||
|
* @param array $old_instance Old settings.
|
||||||
|
* @return array Updated settings.
|
||||||
|
*/
|
||||||
|
public function update( $new_instance, $old_instance ): array {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 3;
|
||||||
|
$instance['match_by'] = ! empty( $new_instance['match_by'] ) ? sanitize_text_field( $new_instance['match_by'] ) : 'building';
|
||||||
|
$instance['show_price'] = ! empty( $new_instance['show_price'] );
|
||||||
|
$instance['show_image'] = ! empty( $new_instance['show_image'] );
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
1654
src/Integration/CF7.php
Normal file
1654
src/Integration/CF7.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -126,13 +126,60 @@ final class Manager {
|
|||||||
/**
|
/**
|
||||||
* Check if license is valid.
|
* Check if license is valid.
|
||||||
*
|
*
|
||||||
|
* Localhost environments bypass the license check to allow
|
||||||
|
* full functionality during development.
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function is_license_valid(): bool {
|
public static function is_license_valid(): bool {
|
||||||
|
// Bypass license check for localhost environments.
|
||||||
|
if ( self::is_localhost() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||||
return 'valid' === $status;
|
return 'valid' === $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on localhost.
|
||||||
|
*
|
||||||
|
* Detects common local development environments:
|
||||||
|
* - localhost / 127.0.0.1 / ::1
|
||||||
|
* - .local, .test, .localhost domains
|
||||||
|
* - Private IP ranges (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_localhost(): bool {
|
||||||
|
$site_url = get_site_url();
|
||||||
|
$parsed = wp_parse_url( $site_url );
|
||||||
|
$host = $parsed['host'] ?? '';
|
||||||
|
|
||||||
|
// Check for localhost variations.
|
||||||
|
if ( in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common local development TLDs.
|
||||||
|
$local_tlds = array( '.local', '.test', '.localhost', '.dev', '.ddev.site' );
|
||||||
|
foreach ( $local_tlds as $tld ) {
|
||||||
|
if ( str_ends_with( $host, $tld ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for private IP ranges.
|
||||||
|
if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
|
||||||
|
// 10.x.x.x, 172.16-31.x.x, 192.168.x.x.
|
||||||
|
if ( ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get license key.
|
* Get license key.
|
||||||
*
|
*
|
||||||
|
|||||||
473
src/License/Updater.php
Normal file
473
src/License/Updater.php
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Updater class.
|
||||||
|
*
|
||||||
|
* Integrates with WordPress plugin update system to check for and install
|
||||||
|
* updates from the license server.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\License
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\License;
|
||||||
|
|
||||||
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||||||
|
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles plugin auto-updates from the license server.
|
||||||
|
*/
|
||||||
|
final class Updater {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance.
|
||||||
|
*
|
||||||
|
* @var Updater|null
|
||||||
|
*/
|
||||||
|
private static ?Updater $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin basename (e.g., wp-bnb/wp-bnb.php).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $plugin_basename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $plugin_slug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current plugin version.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $current_version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for update info.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const CACHE_KEY = 'wp_bnb_update_info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for last check timestamp.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const LAST_CHECK_KEY = 'wp_bnb_update_last_check';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default cache duration in seconds (12 hours).
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private const DEFAULT_CHECK_FREQUENCY = 12;
|
||||||
|
|
||||||
|
// Option keys for update settings.
|
||||||
|
public const OPTION_NOTIFICATIONS_ENABLED = 'wp_bnb_update_notifications_enabled';
|
||||||
|
public const OPTION_AUTO_INSTALL_ENABLED = 'wp_bnb_auto_install_enabled';
|
||||||
|
public const OPTION_CHECK_FREQUENCY = 'wp_bnb_update_check_frequency';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License client instance.
|
||||||
|
*
|
||||||
|
* @var SecureLicenseClient|LicenseClient|null
|
||||||
|
*/
|
||||||
|
private SecureLicenseClient|LicenseClient|null $client = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param string $plugin_file Full path to the main plugin file.
|
||||||
|
* @param string $current_version Current plugin version.
|
||||||
|
*/
|
||||||
|
public function __construct( string $plugin_file, string $current_version ) {
|
||||||
|
$this->plugin_basename = plugin_basename( $plugin_file );
|
||||||
|
$this->plugin_slug = dirname( $this->plugin_basename );
|
||||||
|
$this->current_version = $current_version;
|
||||||
|
self::$instance = $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance.
|
||||||
|
*
|
||||||
|
* @return Updater|null
|
||||||
|
*/
|
||||||
|
public static function get_instance(): ?Updater {
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize update hooks.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function init(): void {
|
||||||
|
// Allow complete disable via constant.
|
||||||
|
if ( defined( 'WP_BNB_DISABLE_AUTO_UPDATE' ) && WP_BNB_DISABLE_AUTO_UPDATE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook into WordPress update system.
|
||||||
|
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_updates' ) );
|
||||||
|
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
|
||||||
|
add_action( 'upgrader_process_complete', array( $this, 'after_update' ), 10, 2 );
|
||||||
|
|
||||||
|
// Auto-install filter for WordPress background updates.
|
||||||
|
add_filter( 'auto_update_plugin', array( $this, 'auto_update_plugin' ), 10, 2 );
|
||||||
|
|
||||||
|
// Clear update cache when license settings change.
|
||||||
|
add_action( 'update_option_' . Manager::OPTION_LICENSE_KEY, array( $this, 'clear_cache' ) );
|
||||||
|
add_action( 'update_option_' . Manager::OPTION_SERVER_URL, array( $this, 'clear_cache' ) );
|
||||||
|
|
||||||
|
// AJAX handler for manual update check.
|
||||||
|
add_action( 'wp_ajax_wp_bnb_check_updates', array( $this, 'ajax_check_updates' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if update notifications are enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_notifications_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_NOTIFICATIONS_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if auto-install is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_auto_install_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_AUTO_INSTALL_ENABLED, 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the update check frequency in hours.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get_check_frequency(): int {
|
||||||
|
$frequency = (int) get_option( self::OPTION_CHECK_FREQUENCY, self::DEFAULT_CHECK_FREQUENCY );
|
||||||
|
// Clamp between 1 and 168 hours (1 week).
|
||||||
|
return max( 1, min( 168, $frequency ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache duration in seconds based on check frequency.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function get_cache_duration(): int {
|
||||||
|
return self::get_check_frequency() * 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter for WordPress auto-update system.
|
||||||
|
*
|
||||||
|
* @param bool|null $update Whether to update the plugin.
|
||||||
|
* @param object $item The plugin update object.
|
||||||
|
* @return bool|null
|
||||||
|
*/
|
||||||
|
public function auto_update_plugin( $update, object $item ) {
|
||||||
|
// Only affect our plugin.
|
||||||
|
if ( ! isset( $item->plugin ) || $item->plugin !== $this->plugin_basename ) {
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if auto-install is enabled and license is valid.
|
||||||
|
if ( self::is_auto_install_enabled() && Manager::is_license_valid() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current plugin version.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_current_version(): string {
|
||||||
|
return $this->current_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last update check timestamp.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get_last_check(): int {
|
||||||
|
return (int) get_option( self::LAST_CHECK_KEY, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler: Check for updates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_check_updates(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'update_plugins' ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'You do not have permission to check for updates.', 'wp-bnb' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_cached_update_info( true );
|
||||||
|
|
||||||
|
if ( null === $update_info ) {
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'update_available' => false,
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'message' => __( 'Could not check for updates. Please verify your license configuration.', 'wp-bnb' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = array(
|
||||||
|
'update_available' => $update_info->updateAvailable && version_compare( $this->current_version, $update_info->version ?? '', '<' ),
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'latest_version' => $update_info->version ?? $this->current_version,
|
||||||
|
'last_check' => time(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $response['update_available'] ) {
|
||||||
|
$response['message'] = sprintf(
|
||||||
|
/* translators: %s: New version number */
|
||||||
|
__( 'A new version (%s) is available.', 'wp-bnb' ),
|
||||||
|
$update_info->version
|
||||||
|
);
|
||||||
|
$response['changelog'] = $update_info->changelog ?? '';
|
||||||
|
} else {
|
||||||
|
$response['message'] = __( 'You are running the latest version.', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $response );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the license client.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function init_client(): bool {
|
||||||
|
if ( null !== $this->client ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$server_url = Manager::get_server_url();
|
||||||
|
$server_secret = Manager::get_server_secret();
|
||||||
|
|
||||||
|
if ( empty( $server_url ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! empty( $server_secret ) ) {
|
||||||
|
$this->client = new SecureLicenseClient(
|
||||||
|
httpClient: HttpClient::create(),
|
||||||
|
baseUrl: $server_url,
|
||||||
|
serverSecret: $server_secret,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->client = new LicenseClient(
|
||||||
|
httpClient: HttpClient::create(),
|
||||||
|
baseUrl: $server_url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch ( \Throwable $e ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for plugin updates.
|
||||||
|
*
|
||||||
|
* @param object $transient The update_plugins transient.
|
||||||
|
* @return object Modified transient.
|
||||||
|
*/
|
||||||
|
public function check_for_updates( object $transient ): object {
|
||||||
|
if ( empty( $transient->checked ) ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respect notifications enabled setting.
|
||||||
|
if ( ! self::is_notifications_enabled() ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_update_info();
|
||||||
|
|
||||||
|
if ( null === $update_info || ! $update_info->updateAvailable ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare versions.
|
||||||
|
if ( version_compare( $this->current_version, $update_info->version ?? '', '>=' ) ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to update response.
|
||||||
|
$transient->response[ $this->plugin_basename ] = (object) array(
|
||||||
|
'slug' => $update_info->slug ?? $this->plugin_slug,
|
||||||
|
'plugin' => $this->plugin_basename,
|
||||||
|
'new_version' => $update_info->version,
|
||||||
|
'url' => $update_info->homepage ?? '',
|
||||||
|
'package' => $update_info->downloadUrl,
|
||||||
|
'icons' => $update_info->icons ?? array(),
|
||||||
|
'tested' => $update_info->tested ?? '',
|
||||||
|
'requires' => $update_info->requires ?? '',
|
||||||
|
'requires_php' => $update_info->requiresPhp ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide plugin information for the details modal.
|
||||||
|
*
|
||||||
|
* @param false|object|array $result The result object or array.
|
||||||
|
* @param string $action The API action being performed.
|
||||||
|
* @param object $args Plugin API arguments.
|
||||||
|
* @return false|object
|
||||||
|
*/
|
||||||
|
public function plugin_info( $result, string $action, object $args ) {
|
||||||
|
if ( 'plugin_information' !== $action ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $args->slug ) || $args->slug !== $this->plugin_slug ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_update_info();
|
||||||
|
|
||||||
|
if ( null === $update_info ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_info = (object) array(
|
||||||
|
'name' => $update_info->name ?? 'WP BnB Manager',
|
||||||
|
'slug' => $update_info->slug ?? $this->plugin_slug,
|
||||||
|
'version' => $update_info->version ?? $this->current_version,
|
||||||
|
'author' => '<a href="https://src.bundespruefstelle.ch/magdev">Marco Graetsch</a>',
|
||||||
|
'homepage' => $update_info->homepage ?? 'https://src.bundespruefstelle.ch/magdev/wp-bnb',
|
||||||
|
'requires' => $update_info->requires ?? '6.0',
|
||||||
|
'tested' => $update_info->tested ?? '',
|
||||||
|
'requires_php' => $update_info->requiresPhp ?? '8.3',
|
||||||
|
'last_updated' => $update_info->lastUpdated?->format( 'Y-m-d' ) ?? '',
|
||||||
|
'download_link' => $update_info->downloadUrl ?? '',
|
||||||
|
'sections' => $update_info->sections ?? array(
|
||||||
|
'description' => __( 'A comprehensive Bed & Breakfast management plugin for WordPress.', 'wp-bnb' ),
|
||||||
|
'changelog' => $update_info->changelog ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $update_info->icons ) ) {
|
||||||
|
$plugin_info->icons = $update_info->icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plugin_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear update cache after upgrade.
|
||||||
|
*
|
||||||
|
* @param \WP_Upgrader $upgrader WP_Upgrader instance.
|
||||||
|
* @param array $hook_extra Extra arguments passed to hooked filters.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function after_update( \WP_Upgrader $upgrader, array $hook_extra ): void {
|
||||||
|
if ( ! isset( $hook_extra['plugins'] ) || ! is_array( $hook_extra['plugins'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( in_array( $this->plugin_basename, $hook_extra['plugins'], true ) ) {
|
||||||
|
$this->clear_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update info from cache or server.
|
||||||
|
*
|
||||||
|
* @param bool $force_refresh Force refresh from server.
|
||||||
|
* @return UpdateInfo|null
|
||||||
|
*/
|
||||||
|
public function get_cached_update_info( bool $force_refresh = false ): ?UpdateInfo {
|
||||||
|
if ( ! $force_refresh ) {
|
||||||
|
$cached = get_transient( self::CACHE_KEY );
|
||||||
|
if ( false !== $cached && $cached instanceof UpdateInfo ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if license is configured.
|
||||||
|
$license_key = Manager::get_license_key();
|
||||||
|
if ( empty( $license_key ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $this->init_client() ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$domain = $this->get_current_domain();
|
||||||
|
$update_info = $this->client->checkForUpdates(
|
||||||
|
licenseKey: $license_key,
|
||||||
|
domain: $domain,
|
||||||
|
pluginSlug: $this->plugin_slug,
|
||||||
|
currentVersion: $this->current_version,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache the result and update last check timestamp.
|
||||||
|
set_transient( self::CACHE_KEY, $update_info, $this->get_cache_duration() );
|
||||||
|
update_option( self::LAST_CHECK_KEY, time() );
|
||||||
|
|
||||||
|
return $update_info;
|
||||||
|
} catch ( \Throwable $e ) {
|
||||||
|
// Silently fail and return null - don't break WordPress.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update info from cache or server (alias for WordPress update system).
|
||||||
|
*
|
||||||
|
* @param bool $force_refresh Force refresh from server.
|
||||||
|
* @return UpdateInfo|null
|
||||||
|
*/
|
||||||
|
private function get_update_info( bool $force_refresh = false ): ?UpdateInfo {
|
||||||
|
return $this->get_cached_update_info( $force_refresh );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current domain.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_current_domain(): string {
|
||||||
|
$site_url = get_site_url();
|
||||||
|
$parsed = wp_parse_url( $site_url );
|
||||||
|
return $parsed['host'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the update cache.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function clear_cache(): void {
|
||||||
|
delete_transient( self::CACHE_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
814
src/Plugin.php
814
src/Plugin.php
@@ -11,14 +11,24 @@ namespace Magdev\WpBnb;
|
|||||||
|
|
||||||
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
|
use Magdev\WpBnb\Blocks\BlockRegistrar;
|
||||||
use Magdev\WpBnb\Booking\Availability;
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||||
|
use Magdev\WpBnb\Integration\CF7;
|
||||||
|
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||||
|
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||||
|
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
|
use Magdev\WpBnb\License\Updater as LicenseUpdater;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
use Magdev\WpBnb\PostTypes\Booking;
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
use Magdev\WpBnb\PostTypes\Building;
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
use Magdev\WpBnb\PostTypes\Guest;
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
use Magdev\WpBnb\PostTypes\Room;
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
use Magdev\WpBnb\PostTypes\Service;
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
||||||
use Magdev\WpBnb\Pricing\Season;
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
@@ -121,6 +131,9 @@ final class Plugin {
|
|||||||
// Initialize License Manager (always active for admin).
|
// Initialize License Manager (always active for admin).
|
||||||
LicenseManager::get_instance();
|
LicenseManager::get_instance();
|
||||||
|
|
||||||
|
// Initialize auto-updater (requires license configuration).
|
||||||
|
$this->init_updater();
|
||||||
|
|
||||||
// Initialize admin components.
|
// Initialize admin components.
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
$this->init_admin();
|
$this->init_admin();
|
||||||
@@ -132,6 +145,19 @@ final class Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the plugin auto-updater.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_updater(): void {
|
||||||
|
$updater = new LicenseUpdater(
|
||||||
|
plugin_file: WP_BNB_PATH . 'wp-bnb.php',
|
||||||
|
current_version: WP_BNB_VERSION,
|
||||||
|
);
|
||||||
|
$updater->init();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize admin components.
|
* Initialize admin components.
|
||||||
*
|
*
|
||||||
@@ -140,6 +166,7 @@ final class Plugin {
|
|||||||
private function init_admin(): void {
|
private function init_admin(): void {
|
||||||
// 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_menu', array( $this, 'reorder_admin_menu' ), 99 );
|
||||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||||
|
|
||||||
// Initialize seasons admin page.
|
// Initialize seasons admin page.
|
||||||
@@ -165,7 +192,33 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function init_frontend(): void {
|
private function init_frontend(): void {
|
||||||
// Frontend shortcodes, blocks, and widgets will be added here.
|
// Initialize search (registers AJAX handlers).
|
||||||
|
Search::init();
|
||||||
|
|
||||||
|
// Initialize shortcodes.
|
||||||
|
Shortcodes::init();
|
||||||
|
|
||||||
|
// Initialize Gutenberg blocks.
|
||||||
|
BlockRegistrar::init();
|
||||||
|
|
||||||
|
// Register widgets.
|
||||||
|
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
|
||||||
|
|
||||||
|
// Initialize Contact Form 7 integration if CF7 is active.
|
||||||
|
if ( class_exists( 'WPCF7' ) ) {
|
||||||
|
CF7::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register frontend widgets.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_widgets(): void {
|
||||||
|
register_widget( SimilarRooms::class );
|
||||||
|
register_widget( BuildingRooms::class );
|
||||||
|
register_widget( AvailabilityCalendar::class );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,6 +305,10 @@ final class Plugin {
|
|||||||
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
||||||
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
||||||
|
'justNow' => __( 'Just now', 'wp-bnb' ),
|
||||||
|
'updateAvailable' => __( 'Update available!', 'wp-bnb' ),
|
||||||
|
'upToDate' => __( '(You are up to date)', 'wp-bnb' ),
|
||||||
|
'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -282,6 +339,72 @@ final class Plugin {
|
|||||||
WP_BNB_VERSION,
|
WP_BNB_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
'wp-bnb-frontend',
|
||||||
|
'wpBnbFrontend',
|
||||||
|
array(
|
||||||
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'wp_bnb_frontend_nonce' ),
|
||||||
|
'i18n' => array(
|
||||||
|
'searching' => __( 'Searching...', 'wp-bnb' ),
|
||||||
|
'noResults' => __( 'No rooms found matching your criteria.', 'wp-bnb' ),
|
||||||
|
'resultsFound' => __( '%d rooms found', 'wp-bnb' ),
|
||||||
|
'loadMore' => __( 'Load More', 'wp-bnb' ),
|
||||||
|
'viewDetails' => __( 'View Details', 'wp-bnb' ),
|
||||||
|
'perNight' => __( 'night', 'wp-bnb' ),
|
||||||
|
'guests' => __( 'guests', 'wp-bnb' ),
|
||||||
|
'invalidDateRange' => __( 'Check-out must be after check-in', 'wp-bnb' ),
|
||||||
|
'selectDates' => __( 'Please select check-in and check-out dates.', 'wp-bnb' ),
|
||||||
|
'available' => __( 'Room is available!', 'wp-bnb' ),
|
||||||
|
'notAvailable' => __( 'Sorry, the room is not available for these dates.', 'wp-bnb' ),
|
||||||
|
'totalPrice' => __( 'Total', 'wp-bnb' ),
|
||||||
|
'bookNow' => __( 'Book Now', 'wp-bnb' ),
|
||||||
|
'total' => __( 'Total', 'wp-bnb' ),
|
||||||
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
|
'basePrice' => __( 'Base', 'wp-bnb' ),
|
||||||
|
'weekendSurcharge' => __( 'Weekend surcharge', 'wp-bnb' ),
|
||||||
|
'season' => __( 'Season', 'wp-bnb' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load CF7 integration assets if CF7 is active.
|
||||||
|
if ( class_exists( 'WPCF7' ) ) {
|
||||||
|
wp_enqueue_style(
|
||||||
|
'wp-bnb-cf7',
|
||||||
|
WP_BNB_URL . 'assets/css/cf7-integration.css',
|
||||||
|
array( 'contact-form-7' ),
|
||||||
|
WP_BNB_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'wp-bnb-cf7',
|
||||||
|
WP_BNB_URL . 'assets/js/cf7-integration.js',
|
||||||
|
array( 'contact-form-7' ),
|
||||||
|
WP_BNB_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
'wp-bnb-cf7',
|
||||||
|
'wpBnbCF7',
|
||||||
|
array(
|
||||||
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'wp_bnb_frontend_nonce' ),
|
||||||
|
'i18n' => array(
|
||||||
|
'selectRoom' => __( '-- Select Room --', 'wp-bnb' ),
|
||||||
|
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||||
|
'available' => __( 'Room is available!', 'wp-bnb' ),
|
||||||
|
'unavailable' => __( 'Room is not available for these dates', 'wp-bnb' ),
|
||||||
|
'invalidDateRange' => __( 'Check-out must be after check-in', 'wp-bnb' ),
|
||||||
|
'capacityExceeded' => __( 'Maximum %d guests for this room', 'wp-bnb' ),
|
||||||
|
'estimatedTotal' => __( 'Estimated Total', 'wp-bnb' ),
|
||||||
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -335,6 +458,59 @@ final class Plugin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorder the admin submenu items.
|
||||||
|
*
|
||||||
|
* Places Dashboard at top, Settings at bottom, and organizes
|
||||||
|
* the remaining items in logical order.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function reorder_admin_menu(): void {
|
||||||
|
global $submenu;
|
||||||
|
|
||||||
|
if ( ! isset( $submenu['wp-bnb'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the desired order of menu slugs.
|
||||||
|
$desired_order = array(
|
||||||
|
'wp-bnb', // Dashboard.
|
||||||
|
'edit.php?post_type=bnb_building', // Buildings.
|
||||||
|
'edit.php?post_type=bnb_room', // Rooms.
|
||||||
|
'edit.php?post_type=bnb_booking', // Bookings.
|
||||||
|
'edit.php?post_type=bnb_guest', // Guests.
|
||||||
|
'edit.php?post_type=bnb_service', // Services.
|
||||||
|
'wp-bnb-calendar', // Calendar.
|
||||||
|
'wp-bnb-seasons', // Seasons.
|
||||||
|
'wp-bnb-settings', // Settings (always last).
|
||||||
|
);
|
||||||
|
|
||||||
|
$current_menu = $submenu['wp-bnb'];
|
||||||
|
$ordered_menu = array();
|
||||||
|
$index = 0;
|
||||||
|
|
||||||
|
// Add items in the desired order.
|
||||||
|
foreach ( $desired_order as $slug ) {
|
||||||
|
foreach ( $current_menu as $key => $item ) {
|
||||||
|
if ( $item[2] === $slug ) {
|
||||||
|
$ordered_menu[ $index ] = $item;
|
||||||
|
unset( $current_menu[ $key ] );
|
||||||
|
++$index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any remaining items not in the desired order.
|
||||||
|
foreach ( $current_menu as $item ) {
|
||||||
|
$ordered_menu[ $index ] = $item;
|
||||||
|
++$index;
|
||||||
|
}
|
||||||
|
|
||||||
|
$submenu['wp-bnb'] = $ordered_menu;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register plugin settings.
|
* Register plugin settings.
|
||||||
*
|
*
|
||||||
@@ -351,12 +527,21 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function render_dashboard_page(): void {
|
public function render_dashboard_page(): void {
|
||||||
$license_status = LicenseManager::get_cached_status();
|
$license_valid = LicenseManager::is_license_valid();
|
||||||
|
$is_localhost = LicenseManager::is_localhost();
|
||||||
?>
|
?>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
||||||
|
|
||||||
<?php if ( 'valid' !== $license_status ) : ?>
|
<?php if ( $is_localhost ) : ?>
|
||||||
|
<div class="notice notice-info">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
||||||
|
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'You are running on a local development environment. All features are enabled.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php elseif ( ! $license_valid ) : ?>
|
||||||
<div class="notice notice-warning">
|
<div class="notice notice-warning">
|
||||||
<p>
|
<p>
|
||||||
<?php
|
<?php
|
||||||
@@ -407,6 +592,10 @@ final class Plugin {
|
|||||||
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' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=updates' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -418,6 +607,9 @@ final class Plugin {
|
|||||||
case 'license':
|
case 'license':
|
||||||
$this->render_license_settings();
|
$this->render_license_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'updates':
|
||||||
|
$this->render_updates_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->render_general_settings();
|
$this->render_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -438,6 +630,8 @@ final class Plugin {
|
|||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Business Information', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
<table class="form-table" role="presentation">
|
<table class="form-table" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
@@ -475,6 +669,143 @@ final class Plugin {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Address', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_street"><?php esc_html_e( 'Street Address', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_street" id="wp_bnb_address_street"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_street', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_city"><?php esc_html_e( 'City', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_city" id="wp_bnb_address_city"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_city', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_postal"><?php esc_html_e( 'Postal Code', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_postal" id="wp_bnb_address_postal"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_postal', '' ) ); ?>"
|
||||||
|
class="small-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_country"><?php esc_html_e( 'Country', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_country" id="wp_bnb_address_country"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_country', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Contact Information', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_contact_email"><?php esc_html_e( 'Email Address', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="email" name="wp_bnb_contact_email" id="wp_bnb_contact_email"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_email', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
<p class="description"><?php esc_html_e( 'Primary contact email for bookings and inquiries.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_contact_phone"><?php esc_html_e( 'Phone Number', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="tel" name="wp_bnb_contact_phone" id="wp_bnb_contact_phone"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_phone', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="+41 12 345 67 89">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_contact_website"><?php esc_html_e( 'Website', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_contact_website" id="wp_bnb_contact_website"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_website', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Social Media', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_facebook"><?php esc_html_e( 'Facebook', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_facebook" id="wp_bnb_social_facebook"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_facebook', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://facebook.com/yourpage">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_instagram"><?php esc_html_e( 'Instagram', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_instagram" id="wp_bnb_social_instagram"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_instagram', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://instagram.com/yourprofile">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_x"><?php esc_html_e( 'X (Twitter)', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_x" id="wp_bnb_social_x"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_x', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://x.com/yourhandle">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_linkedin"><?php esc_html_e( 'LinkedIn', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_linkedin" id="wp_bnb_social_linkedin"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_linkedin', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://linkedin.com/company/yourcompany">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_tripadvisor"><?php esc_html_e( 'TripAdvisor', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_tripadvisor" id="wp_bnb_social_tripadvisor"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_tripadvisor', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://tripadvisor.com/...">
|
||||||
|
<p class="description"><?php esc_html_e( 'Link to your TripAdvisor listing.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<?php submit_button( __( 'Save Settings', 'wp-bnb' ) ); ?>
|
<?php submit_button( __( 'Save Settings', 'wp-bnb' ) ); ?>
|
||||||
</form>
|
</form>
|
||||||
<?php
|
<?php
|
||||||
@@ -486,12 +817,14 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function render_pricing_settings(): void {
|
private function render_pricing_settings(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Subtab switching only.
|
||||||
|
$active_subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'tiers';
|
||||||
$short_term_max = get_option( 'wp_bnb_short_term_max_nights', 6 );
|
$short_term_max = get_option( 'wp_bnb_short_term_max_nights', 6 );
|
||||||
$mid_term_max = get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
$mid_term_max = get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
||||||
$weekend_days = get_option( 'wp_bnb_weekend_days', '5,6' );
|
$weekend_days = get_option( 'wp_bnb_weekend_days', '5,6' );
|
||||||
$seasons = Season::all();
|
$seasons = Season::all();
|
||||||
|
|
||||||
$days_of_week = array(
|
$days_of_week = array(
|
||||||
1 => __( 'Monday', 'wp-bnb' ),
|
1 => __( 'Monday', 'wp-bnb' ),
|
||||||
2 => __( 'Tuesday', 'wp-bnb' ),
|
2 => __( 'Tuesday', 'wp-bnb' ),
|
||||||
3 => __( 'Wednesday', 'wp-bnb' ),
|
3 => __( 'Wednesday', 'wp-bnb' ),
|
||||||
@@ -501,118 +834,155 @@ final class Plugin {
|
|||||||
7 => __( 'Sunday', 'wp-bnb' ),
|
7 => __( 'Sunday', 'wp-bnb' ),
|
||||||
);
|
);
|
||||||
$selected_days = array_map( 'intval', explode( ',', $weekend_days ) );
|
$selected_days = array_map( 'intval', explode( ',', $weekend_days ) );
|
||||||
|
|
||||||
|
$base_url = admin_url( 'admin.php?page=wp-bnb-settings&tab=pricing' );
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<!-- Pricing Subtabs -->
|
||||||
|
<div class="wp-bnb-subtabs">
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=tiers' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'tiers' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-chart-bar"></span>
|
||||||
|
<?php esc_html_e( 'Pricing Tiers', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=weekend' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'weekend' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=seasons' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'seasons' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-image-filter"></span>
|
||||||
|
<?php esc_html_e( 'Seasons', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
<h2><?php esc_html_e( 'Pricing Tier Thresholds', 'wp-bnb' ); ?></h2>
|
<?php if ( 'tiers' === $active_subtab ) : ?>
|
||||||
<p class="description"><?php esc_html_e( 'Define the number of nights that determine which pricing tier applies.', 'wp-bnb' ); ?></p>
|
<!-- Pricing Tiers Subtab -->
|
||||||
|
<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">
|
<table class="form-table" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="wp_bnb_short_term_max_nights"><?php esc_html_e( 'Short-term (Nightly)', 'wp-bnb' ); ?></label>
|
<label for="wp_bnb_short_term_max_nights"><?php esc_html_e( 'Short-term (Nightly)', 'wp-bnb' ); ?></label>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="number" name="wp_bnb_short_term_max_nights" id="wp_bnb_short_term_max_nights"
|
<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 ); ?>"
|
value="<?php echo esc_attr( $short_term_max ); ?>"
|
||||||
class="small-text" min="1" max="30">
|
class="small-text" min="1" max="30">
|
||||||
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
<?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>
|
<p class="description"><?php esc_html_e( 'Stays up to this many nights use the nightly rate.', 'wp-bnb' ); ?></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="wp_bnb_mid_term_max_nights"><?php esc_html_e( 'Mid-term (Weekly)', 'wp-bnb' ); ?></label>
|
<label for="wp_bnb_mid_term_max_nights"><?php esc_html_e( 'Mid-term (Weekly)', 'wp-bnb' ); ?></label>
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="number" name="wp_bnb_mid_term_max_nights" id="wp_bnb_mid_term_max_nights"
|
<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 ); ?>"
|
value="<?php echo esc_attr( $mid_term_max ); ?>"
|
||||||
class="small-text" min="7" max="90">
|
class="small-text" min="7" max="90">
|
||||||
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
<?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>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><?php esc_html_e( 'Long-term (Monthly)', 'wp-bnb' ); ?></th>
|
<th scope="row"><?php esc_html_e( 'Long-term (Monthly)', 'wp-bnb' ); ?></th>
|
||||||
<td>
|
<td>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
/* translators: %s: number of nights */
|
/* translators: %s: number of nights */
|
||||||
esc_html__( 'Stays longer than %s nights use the monthly rate.', 'wp-bnb' ),
|
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>'
|
'<strong id="wp-bnb-long-term-min">' . esc_html( $mid_term_max ) . '</strong>'
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</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' ) ); ?>
|
<?php submit_button( __( 'Save Pricing Tiers', 'wp-bnb' ) ); ?>
|
||||||
|
|
||||||
|
<?php elseif ( 'weekend' === $active_subtab ) : ?>
|
||||||
|
<!-- Weekend Days Subtab -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Weekend Days', 'wp-bnb' ) ); ?>
|
||||||
|
|
||||||
|
<?php else : ?>
|
||||||
|
<!-- Seasons Subtab -->
|
||||||
|
<h2><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h2>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Seasonal pricing allows you to adjust room rates based on time of year. Prices are multiplied by the season modifier.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ); ?>" class="button button-primary">
|
||||||
|
<span class="dashicons dashicons-admin-settings" style="vertical-align: text-top;"></span>
|
||||||
|
<?php esc_html_e( 'Manage Seasons', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $seasons ) ) : ?>
|
||||||
|
<table class="widefat striped" style="max-width: 700px; margin-top: 20px;">
|
||||||
|
<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( 'Priority', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $seasons as $season ) : ?>
|
||||||
|
<tr>
|
||||||
|
<td><strong><?php echo esc_html( $season->name ); ?></strong></td>
|
||||||
|
<td><?php echo esc_html( $season->start_date . ' - ' . $season->end_date ); ?></td>
|
||||||
|
<td><?php echo esc_html( $season->getModifierLabel() ); ?></td>
|
||||||
|
<td><?php echo esc_html( $season->priority ); ?></td>
|
||||||
|
<td>
|
||||||
|
<?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 else : ?>
|
||||||
|
<div class="notice notice-info inline" style="margin: 20px 0;">
|
||||||
|
<p><?php esc_html_e( 'No seasons configured yet. Create seasons to apply price modifiers during specific periods.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -628,10 +998,21 @@ final class Plugin {
|
|||||||
$license_status = LicenseManager::get_cached_status();
|
$license_status = LicenseManager::get_cached_status();
|
||||||
$license_data = LicenseManager::get_cached_data();
|
$license_data = LicenseManager::get_cached_data();
|
||||||
$last_check = LicenseManager::get_last_check();
|
$last_check = LicenseManager::get_last_check();
|
||||||
|
$is_localhost = LicenseManager::is_localhost();
|
||||||
?>
|
?>
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<?php if ( $is_localhost ) : ?>
|
||||||
|
<div class="notice notice-info inline" style="margin: 0 0 20px 0;">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
||||||
|
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'You are running on a local development environment. License validation is bypassed and all features are enabled.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<h2><?php esc_html_e( 'License Status', 'wp-bnb' ); ?></h2>
|
<h2><?php esc_html_e( 'License Status', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
<table class="form-table" role="presentation">
|
<table class="form-table" role="presentation">
|
||||||
@@ -716,6 +1097,160 @@ final class Plugin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render updates settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_updates_settings(): void {
|
||||||
|
$updater = LicenseUpdater::get_instance();
|
||||||
|
|
||||||
|
if ( null === $updater ) {
|
||||||
|
?>
|
||||||
|
<p><?php esc_html_e( 'Update checker is not available.', 'wp-bnb' ); ?></p>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_version = $updater->get_current_version();
|
||||||
|
$last_check = LicenseUpdater::get_last_check();
|
||||||
|
$update_info = $updater->get_cached_update_info();
|
||||||
|
$update_available = false;
|
||||||
|
$latest_version = $current_version;
|
||||||
|
$notifications_enabled = LicenseUpdater::is_notifications_enabled();
|
||||||
|
$auto_install_enabled = LicenseUpdater::is_auto_install_enabled();
|
||||||
|
$check_frequency = LicenseUpdater::get_check_frequency();
|
||||||
|
$license_valid = LicenseManager::is_license_valid();
|
||||||
|
|
||||||
|
if ( $update_info instanceof UpdateInfo && $update_info->updateAvailable ) {
|
||||||
|
$latest_version = $update_info->version ?? $current_version;
|
||||||
|
$update_available = version_compare( $current_version, $latest_version, '<' );
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Update Status', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Current Version', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<strong><?php echo esc_html( $current_version ); ?></strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Latest Version', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<span id="wp-bnb-latest-version">
|
||||||
|
<?php if ( $update_available ) : ?>
|
||||||
|
<span style="color: #00a32a; font-weight: 600;">
|
||||||
|
<?php echo esc_html( $latest_version ); ?>
|
||||||
|
</span>
|
||||||
|
<span class="dashicons dashicons-yes" style="color: #00a32a;"></span>
|
||||||
|
<em><?php esc_html_e( 'Update available!', 'wp-bnb' ); ?></em>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php echo esc_html( $latest_version ); ?>
|
||||||
|
<span style="color: #646970;">
|
||||||
|
<?php esc_html_e( '(You are up to date)', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Last Check', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<span id="wp-bnb-update-last-check">
|
||||||
|
<?php if ( $last_check > 0 ) : ?>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Time ago string */
|
||||||
|
esc_html__( '%s ago', 'wp-bnb' ),
|
||||||
|
esc_html( human_time_diff( $last_check, time() ) )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php esc_html_e( 'Never', 'wp-bnb' ); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 30px;">
|
||||||
|
<button type="button" id="wp-bnb-check-updates" class="button button-secondary">
|
||||||
|
<span class="dashicons dashicons-update" style="vertical-align: text-top;"></span>
|
||||||
|
<?php esc_html_e( 'Check for Updates', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
<span class="spinner" id="wp-bnb-update-spinner"></span>
|
||||||
|
<?php if ( $update_available ) : ?>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'plugins.php' ) ); ?>" class="button button-primary">
|
||||||
|
<?php esc_html_e( 'Go to Plugins Page', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="wp-bnb-update-message" style="display: none; margin-bottom: 20px;"></div>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Update Settings', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable Update Notifications', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_update_notifications_enabled"
|
||||||
|
value="yes" <?php checked( $notifications_enabled ); ?>>
|
||||||
|
<?php esc_html_e( 'Check for updates from the license server', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, the plugin will check for updates and show notifications in the WordPress admin.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Automatic Updates', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_auto_install_enabled"
|
||||||
|
value="yes" <?php checked( $auto_install_enabled ); ?>
|
||||||
|
<?php disabled( ! $license_valid ); ?>>
|
||||||
|
<?php esc_html_e( 'Automatically install updates', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<?php if ( ! $license_valid ) : ?>
|
||||||
|
<span style="color: #d63638; margin-left: 10px;">
|
||||||
|
<?php esc_html_e( '(Requires valid license)', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, updates will be automatically installed during WordPress background updates.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_update_check_frequency"><?php esc_html_e( 'Check Frequency', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_update_check_frequency" id="wp_bnb_update_check_frequency"
|
||||||
|
value="<?php echo esc_attr( $check_frequency ); ?>"
|
||||||
|
min="1" max="168" class="small-text">
|
||||||
|
<?php esc_html_e( 'hours', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'How often to check for updates (1-168 hours). Default: 12 hours.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<?php submit_button( __( 'Save Update Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render license status badge.
|
* Render license status badge.
|
||||||
*
|
*
|
||||||
@@ -784,6 +1319,9 @@ final class Plugin {
|
|||||||
case 'license':
|
case 'license':
|
||||||
$this->save_license_settings();
|
$this->save_license_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'updates':
|
||||||
|
$this->save_updates_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->save_general_settings();
|
$this->save_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -796,6 +1334,7 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function save_general_settings(): void {
|
private function save_general_settings(): void {
|
||||||
|
// Business Information.
|
||||||
if ( isset( $_POST['wp_bnb_business_name'] ) ) {
|
if ( isset( $_POST['wp_bnb_business_name'] ) ) {
|
||||||
update_option( 'wp_bnb_business_name', sanitize_text_field( wp_unslash( $_POST['wp_bnb_business_name'] ) ) );
|
update_option( 'wp_bnb_business_name', sanitize_text_field( wp_unslash( $_POST['wp_bnb_business_name'] ) ) );
|
||||||
}
|
}
|
||||||
@@ -803,6 +1342,35 @@ final class Plugin {
|
|||||||
update_option( 'wp_bnb_currency', sanitize_text_field( wp_unslash( $_POST['wp_bnb_currency'] ) ) );
|
update_option( 'wp_bnb_currency', sanitize_text_field( wp_unslash( $_POST['wp_bnb_currency'] ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Address fields.
|
||||||
|
$address_fields = array( 'street', 'city', 'postal', 'country' );
|
||||||
|
foreach ( $address_fields as $field ) {
|
||||||
|
$key = 'wp_bnb_address_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_option( $key, sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact fields.
|
||||||
|
if ( isset( $_POST['wp_bnb_contact_email'] ) ) {
|
||||||
|
update_option( 'wp_bnb_contact_email', sanitize_email( wp_unslash( $_POST['wp_bnb_contact_email'] ) ) );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['wp_bnb_contact_phone'] ) ) {
|
||||||
|
update_option( 'wp_bnb_contact_phone', sanitize_text_field( wp_unslash( $_POST['wp_bnb_contact_phone'] ) ) );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['wp_bnb_contact_website'] ) ) {
|
||||||
|
update_option( 'wp_bnb_contact_website', esc_url_raw( wp_unslash( $_POST['wp_bnb_contact_website'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Social media fields.
|
||||||
|
$social_fields = array( 'facebook', 'instagram', 'x', 'linkedin', 'tripadvisor' );
|
||||||
|
foreach ( $social_fields as $field ) {
|
||||||
|
$key = 'wp_bnb_social_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_option( $key, esc_url_raw( wp_unslash( $_POST[ $key ] ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Settings saved.', 'wp-bnb' ), 'success' );
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Settings saved.', 'wp-bnb' ), 'success' );
|
||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
@@ -855,6 +1423,34 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save updates settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_updates_settings(): void {
|
||||||
|
$notifications_enabled = isset( $_POST['wp_bnb_update_notifications_enabled'] ) ? 'yes' : 'no';
|
||||||
|
update_option( LicenseUpdater::OPTION_NOTIFICATIONS_ENABLED, $notifications_enabled );
|
||||||
|
|
||||||
|
$auto_install_enabled = isset( $_POST['wp_bnb_auto_install_enabled'] ) ? 'yes' : 'no';
|
||||||
|
update_option( LicenseUpdater::OPTION_AUTO_INSTALL_ENABLED, $auto_install_enabled );
|
||||||
|
|
||||||
|
if ( isset( $_POST['wp_bnb_update_check_frequency'] ) ) {
|
||||||
|
$frequency = absint( $_POST['wp_bnb_update_check_frequency'] );
|
||||||
|
$frequency = max( 1, min( 168, $frequency ) ); // Clamp between 1-168 hours.
|
||||||
|
update_option( LicenseUpdater::OPTION_CHECK_FREQUENCY, $frequency );
|
||||||
|
|
||||||
|
// Clear update cache when frequency changes so new frequency takes effect.
|
||||||
|
$updater = LicenseUpdater::get_instance();
|
||||||
|
if ( null !== $updater ) {
|
||||||
|
$updater->clear_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Update settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for checking room availability.
|
* AJAX handler for checking room availability.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -54,8 +54,24 @@ final class Booking {
|
|||||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
add_filter( 'wp_insert_post_data', array( self::class, 'auto_generate_title' ), 10, 2 );
|
|
||||||
add_action( 'admin_notices', array( self::class, 'show_conflict_notice' ) );
|
add_action( 'admin_notices', array( self::class, 'show_conflict_notice' ) );
|
||||||
|
|
||||||
|
// Disable Gutenberg block editor for Bookings - use classic editor for form-based UI.
|
||||||
|
add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable block editor for Bookings post type.
|
||||||
|
*
|
||||||
|
* @param bool $use_block_editor Whether to use block editor.
|
||||||
|
* @param string $post_type Post type.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool {
|
||||||
|
if ( self::POST_TYPE === $post_type ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $use_block_editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,7 +312,7 @@ final class Booking {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function render_guest_meta_box( \WP_Post $post ): void {
|
public static function render_guest_meta_box( \WP_Post $post ): void {
|
||||||
$guest_id = get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true );
|
$guest_id = (int) get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true );
|
||||||
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
|
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
|
||||||
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
|
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
|
||||||
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
|
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
|
||||||
@@ -314,7 +330,7 @@ final class Booking {
|
|||||||
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
|
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
|
||||||
} else {
|
} else {
|
||||||
$linked_guest = null;
|
$linked_guest = null;
|
||||||
$guest_id = '';
|
$guest_id = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -757,23 +773,26 @@ final class Booking {
|
|||||||
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
// No guest_id selected - get guest data from form fields.
|
||||||
|
$guest_name = isset( $_POST['bnb_booking_guest_name'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_guest_name'] ) ) : '';
|
||||||
|
$guest_email = isset( $_POST['bnb_booking_guest_email'] ) ? sanitize_email( wp_unslash( $_POST['bnb_booking_guest_email'] ) ) : '';
|
||||||
|
$guest_phone = isset( $_POST['bnb_booking_guest_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_guest_phone'] ) ) : '';
|
||||||
|
|
||||||
// Guest text fields (only save if no guest_id).
|
// Try to find or create a Guest post.
|
||||||
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
|
$linked_guest_id = self::find_or_create_guest( $guest_name, $guest_email, $guest_phone );
|
||||||
foreach ( $guest_fields as $field ) {
|
|
||||||
$key = 'bnb_booking_' . $field;
|
if ( $linked_guest_id ) {
|
||||||
if ( isset( $_POST[ $key ] ) ) {
|
// Link to the guest and sync data.
|
||||||
$value = wp_unslash( $_POST[ $key ] );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $linked_guest_id );
|
||||||
if ( 'guest_email' === $field ) {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $linked_guest_id ) );
|
||||||
$value = sanitize_email( $value );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $linked_guest_id, '_bnb_guest_email', true ) );
|
||||||
} elseif ( 'guest_notes' === $field ) {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $linked_guest_id, '_bnb_guest_phone', true ) );
|
||||||
$value = sanitize_textarea_field( $value );
|
} else {
|
||||||
} else {
|
// Fallback: save guest data directly to booking meta if guest creation failed.
|
||||||
$value = sanitize_text_field( $value );
|
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
||||||
}
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', $guest_name );
|
||||||
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', $guest_email );
|
||||||
}
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', $guest_phone );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,6 +896,130 @@ final class Booking {
|
|||||||
*/
|
*/
|
||||||
do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status );
|
do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate comprehensive title with guest name, room, and dates.
|
||||||
|
self::generate_comprehensive_title( $post_id, $room_id, $check_in, $check_out );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a comprehensive title for a booking.
|
||||||
|
*
|
||||||
|
* Format: "Guest Name (DD.MM - DD.MM.YYYY)"
|
||||||
|
*
|
||||||
|
* @param int $post_id Booking post ID.
|
||||||
|
* @param int $room_id Room post ID (unused, kept for signature compatibility).
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function generate_comprehensive_title( int $post_id, int $room_id, string $check_in, string $check_out ): void {
|
||||||
|
// Get guest name.
|
||||||
|
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
||||||
|
if ( empty( $guest_name ) ) {
|
||||||
|
$guest_name = __( 'Unknown Guest', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format dates.
|
||||||
|
$date_part = '';
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
|
||||||
|
if ( $check_in_date && $check_out_date ) {
|
||||||
|
// Same year: "01.02 - 05.02.2026"
|
||||||
|
// Different year: "28.12.2025 - 02.01.2026"
|
||||||
|
if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) {
|
||||||
|
$date_part = sprintf(
|
||||||
|
'%s - %s',
|
||||||
|
$check_in_date->format( 'd.m' ),
|
||||||
|
$check_out_date->format( 'd.m.Y' )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$date_part = sprintf(
|
||||||
|
'%s - %s',
|
||||||
|
$check_in_date->format( 'd.m.Y' ),
|
||||||
|
$check_out_date->format( 'd.m.Y' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build title: "Guest Name (dates)".
|
||||||
|
$title = $guest_name;
|
||||||
|
if ( $date_part ) {
|
||||||
|
$title .= sprintf( ' (%s)', $date_part );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the post title directly in the database to avoid infinite loop.
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->posts,
|
||||||
|
array( 'post_title' => $title ),
|
||||||
|
array( 'ID' => $post_id ),
|
||||||
|
array( '%s' ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear post cache.
|
||||||
|
clean_post_cache( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an existing guest by email or create a new one.
|
||||||
|
*
|
||||||
|
* @param string $name Guest full name.
|
||||||
|
* @param string $email Guest email.
|
||||||
|
* @param string $phone Guest phone (optional).
|
||||||
|
* @return int|null Guest post ID or null on failure.
|
||||||
|
*/
|
||||||
|
private static function find_or_create_guest( string $name, string $email, string $phone = '' ): ?int {
|
||||||
|
// Need at least a name to create a guest.
|
||||||
|
if ( empty( $name ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find existing guest by email.
|
||||||
|
if ( ! empty( $email ) ) {
|
||||||
|
$existing_guest = Guest::get_by_email( $email );
|
||||||
|
if ( $existing_guest ) {
|
||||||
|
return $existing_guest->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse name into first/last name.
|
||||||
|
$name_parts = explode( ' ', trim( $name ), 2 );
|
||||||
|
$first_name = $name_parts[0] ?? '';
|
||||||
|
$last_name = $name_parts[1] ?? '';
|
||||||
|
|
||||||
|
// Create new guest post.
|
||||||
|
$guest_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $name,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $guest_id ) || ! $guest_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save guest meta.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name );
|
||||||
|
|
||||||
|
if ( ! empty( $email ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_email', $email );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $phone ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_phone', $phone );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default status.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
|
||||||
|
|
||||||
|
return $guest_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -913,7 +1056,7 @@ final class Booking {
|
|||||||
public static function render_column( string $column, int $post_id ): void {
|
public static function render_column( string $column, int $post_id ): void {
|
||||||
switch ( $column ) {
|
switch ( $column ) {
|
||||||
case 'room':
|
case 'room':
|
||||||
$room_id = get_post_meta( $post_id, self::META_PREFIX . 'room_id', true );
|
$room_id = (int) get_post_meta( $post_id, self::META_PREFIX . 'room_id', true );
|
||||||
if ( $room_id ) {
|
if ( $room_id ) {
|
||||||
$room = get_post( $room_id );
|
$room = get_post( $room_id );
|
||||||
if ( $room ) {
|
if ( $room ) {
|
||||||
@@ -935,7 +1078,7 @@ final class Booking {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'guest':
|
case 'guest':
|
||||||
$guest_id = get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true );
|
$guest_id = (int) get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true );
|
||||||
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
||||||
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
|
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
|
||||||
if ( $guest_name ) {
|
if ( $guest_name ) {
|
||||||
@@ -1096,6 +1239,13 @@ final class Booking {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exclude auto-drafts from the list - they're not real bookings.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
$post_status = isset( $_GET['post_status'] ) ? sanitize_key( $_GET['post_status'] ) : '';
|
||||||
|
if ( empty( $post_status ) || 'all' === $post_status ) {
|
||||||
|
$query->set( 'post_status', array( 'publish', 'pending', 'draft', 'private' ) );
|
||||||
|
}
|
||||||
|
|
||||||
$meta_query = array();
|
$meta_query = array();
|
||||||
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
@@ -1142,31 +1292,11 @@ final class Booking {
|
|||||||
*/
|
*/
|
||||||
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
||||||
if ( self::POST_TYPE === $post->post_type ) {
|
if ( self::POST_TYPE === $post->post_type ) {
|
||||||
return __( 'Booking reference (auto-generated)', 'wp-bnb' );
|
return __( 'Title auto-generated from guest name and dates', 'wp-bnb' );
|
||||||
}
|
}
|
||||||
return $placeholder;
|
return $placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-generate booking reference as title.
|
|
||||||
*
|
|
||||||
* @param array $data Post data.
|
|
||||||
* @param array $postarr Post array.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function auto_generate_title( array $data, array $postarr ): array {
|
|
||||||
if ( self::POST_TYPE !== $data['post_type'] ) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only generate if title is empty or matches auto-generated pattern.
|
|
||||||
if ( empty( $data['post_title'] ) || preg_match( '/^BNB-\d{4}-\d{5}$/', $data['post_title'] ) ) {
|
|
||||||
$data['post_title'] = self::generate_reference();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show conflict notice in admin.
|
* Show conflict notice in admin.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,6 +30,75 @@ final class Guest {
|
|||||||
*/
|
*/
|
||||||
private const META_PREFIX = '_bnb_guest_';
|
private const META_PREFIX = '_bnb_guest_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encryption method.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const ENCRYPTION_METHOD = 'aes-256-cbc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt sensitive data.
|
||||||
|
*
|
||||||
|
* Uses WordPress AUTH_KEY for encryption key derivation.
|
||||||
|
*
|
||||||
|
* @param string $data Plain text data to encrypt.
|
||||||
|
* @return string Encrypted data (base64 encoded).
|
||||||
|
*/
|
||||||
|
private static function encrypt( string $data ): string {
|
||||||
|
if ( empty( $data ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = hash( 'sha256', AUTH_KEY . 'wp_bnb_guest_encryption', true );
|
||||||
|
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::ENCRYPTION_METHOD ) );
|
||||||
|
|
||||||
|
$encrypted = openssl_encrypt( $data, self::ENCRYPTION_METHOD, $key, 0, $iv );
|
||||||
|
|
||||||
|
if ( false === $encrypted ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store IV with encrypted data (IV is not secret, just needs to be unique).
|
||||||
|
return base64_encode( $iv . '::' . $encrypted );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt sensitive data.
|
||||||
|
*
|
||||||
|
* @param string $data Encrypted data (base64 encoded).
|
||||||
|
* @return string Decrypted plain text.
|
||||||
|
*/
|
||||||
|
private static function decrypt( string $data ): string {
|
||||||
|
if ( empty( $data ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = base64_decode( $data, true );
|
||||||
|
if ( false === $decoded ) {
|
||||||
|
// Data might be stored unencrypted (legacy), return as-is.
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode( '::', $decoded, 2 );
|
||||||
|
if ( count( $parts ) !== 2 ) {
|
||||||
|
// Data might be stored unencrypted (legacy), return as-is.
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
list( $iv, $encrypted ) = $parts;
|
||||||
|
$key = hash( 'sha256', AUTH_KEY . 'wp_bnb_guest_encryption', true );
|
||||||
|
|
||||||
|
$decrypted = openssl_decrypt( $encrypted, self::ENCRYPTION_METHOD, $key, 0, $iv );
|
||||||
|
|
||||||
|
if ( false === $decrypted ) {
|
||||||
|
// Decryption failed, might be legacy unencrypted data.
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the post type.
|
* Initialize the post type.
|
||||||
*
|
*
|
||||||
@@ -45,6 +114,23 @@ final class Guest {
|
|||||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
|
|
||||||
|
// Disable Gutenberg block editor for Guests - use classic editor for simpler UI.
|
||||||
|
add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable block editor for Guests post type.
|
||||||
|
*
|
||||||
|
* @param bool $use_block_editor Whether to use block editor.
|
||||||
|
* @param string $post_type Post type.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool {
|
||||||
|
if ( self::POST_TYPE === $post_type ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $use_block_editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,12 +397,12 @@ final class Guest {
|
|||||||
*/
|
*/
|
||||||
public static function render_identification_meta_box( \WP_Post $post ): void {
|
public static function render_identification_meta_box( \WP_Post $post ): void {
|
||||||
$id_type = get_post_meta( $post->ID, self::META_PREFIX . 'id_type', true );
|
$id_type = get_post_meta( $post->ID, self::META_PREFIX . 'id_type', true );
|
||||||
$id_number = get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true );
|
$id_number = self::decrypt( get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true ) );
|
||||||
$id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true );
|
$id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true );
|
||||||
?>
|
?>
|
||||||
<p class="description" style="margin-bottom: 15px;">
|
<p class="description" style="margin-bottom: 15px;">
|
||||||
<span class="dashicons dashicons-shield" style="color: #d63638;"></span>
|
<span class="dashicons dashicons-shield" style="color: #00a32a;"></span>
|
||||||
<?php esc_html_e( 'This information is sensitive. Handle with care according to privacy regulations.', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'This information is encrypted and stored securely.', 'wp-bnb' ); ?>
|
||||||
</p>
|
</p>
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -443,9 +529,9 @@ final class Guest {
|
|||||||
$room = get_post( $room_id );
|
$room = get_post( $room_id );
|
||||||
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
$price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true );
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
$statuses = Booking::get_statuses();
|
$statuses = Booking::get_booking_statuses();
|
||||||
$colors = Booking::get_status_colors();
|
$colors = Booking::get_status_colors();
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -563,7 +649,7 @@ final class Guest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text fields.
|
// Text fields (non-sensitive).
|
||||||
$text_fields = array(
|
$text_fields = array(
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
@@ -574,7 +660,6 @@ final class Guest {
|
|||||||
'country',
|
'country',
|
||||||
'nationality',
|
'nationality',
|
||||||
'id_type',
|
'id_type',
|
||||||
'id_number',
|
|
||||||
'status',
|
'status',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -589,6 +674,16 @@ final class Guest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sensitive field: ID number (encrypted).
|
||||||
|
if ( isset( $_POST['bnb_guest_id_number'] ) ) {
|
||||||
|
$id_number = sanitize_text_field( wp_unslash( $_POST['bnb_guest_id_number'] ) );
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . 'id_number',
|
||||||
|
self::encrypt( $id_number )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Email field (special sanitization).
|
// Email field (special sanitization).
|
||||||
if ( isset( $_POST['bnb_guest_email'] ) ) {
|
if ( isset( $_POST['bnb_guest_email'] ) ) {
|
||||||
update_post_meta(
|
update_post_meta(
|
||||||
@@ -1035,7 +1130,7 @@ final class Guest {
|
|||||||
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
// Only count completed bookings (checked_out) or confirmed ones.
|
// Only count completed bookings (checked_out) or confirmed ones.
|
||||||
if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) {
|
if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) {
|
||||||
$price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true );
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
$total += floatval( $price );
|
$total += floatval( $price );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1083,4 +1178,15 @@ final class Guest {
|
|||||||
|
|
||||||
return trim( $first_name . ' ' . $last_name );
|
return trim( $first_name . ' ' . $last_name );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest's ID number (decrypted).
|
||||||
|
*
|
||||||
|
* @param int $guest_id Guest post ID.
|
||||||
|
* @return string Decrypted ID number.
|
||||||
|
*/
|
||||||
|
public static function get_id_number( int $guest_id ): string {
|
||||||
|
$encrypted = get_post_meta( $guest_id, self::META_PREFIX . 'id_number', true );
|
||||||
|
return self::decrypt( $encrypted );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,23 @@ final class Service {
|
|||||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
|
|
||||||
|
// Disable Gutenberg block editor for Services - use classic editor for simpler UI.
|
||||||
|
add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable block editor for Services post type.
|
||||||
|
*
|
||||||
|
* @param bool $use_block_editor Whether to use block editor.
|
||||||
|
* @param string $post_type Post type.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool {
|
||||||
|
if ( self::POST_TYPE === $post_type ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $use_block_editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.5.0
|
* Version: 0.7.1
|
||||||
* 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.5.0' );
|
define( 'WP_BNB_VERSION', '0.7.1' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user