Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81c97c31d7 | |||
| 87aa89b1a6 | |||
| 13fd25f84c | |||
| b6d7eeb5ec | |||
| b137fec4fb | |||
| 992d961066 | |||
| be6d9d68b5 | |||
| a784d92cc9 | |||
| f61dca5f45 | |||
| 28350aabfa | |||
| 3579904bad | |||
| 602549208f | |||
| 45a73e15aa |
190
CHANGELOG.md
190
CHANGELOG.md
@@ -5,6 +5,196 @@ 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.10.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- REST API Infrastructure:
|
||||||
|
- New `src/Api/` directory with complete REST API implementation
|
||||||
|
- `ResponseFormatter.php` - Standardized response formatting (success, collection, error responses)
|
||||||
|
- `RateLimiter.php` - Transient-based rate limiting with tiered limits
|
||||||
|
- `Controllers/AbstractController.php` - Base controller with common functionality
|
||||||
|
- `RestApi.php` - Main registration class with namespace `wp-bnb/v1`
|
||||||
|
- Buildings API:
|
||||||
|
- `GET /wp-bnb/v1/buildings` - List buildings with pagination and search
|
||||||
|
- `GET /wp-bnb/v1/buildings/{id}` - Get single building with address, contact, details
|
||||||
|
- `GET /wp-bnb/v1/buildings/{id}/rooms` - Get rooms in a building with status filter
|
||||||
|
- Rooms API:
|
||||||
|
- `GET /wp-bnb/v1/rooms` - List rooms with filters (building, room_type, amenities, capacity, status)
|
||||||
|
- `GET /wp-bnb/v1/rooms/{id}` - Get room details with gallery, pricing, amenities
|
||||||
|
- `GET /wp-bnb/v1/rooms/{id}/availability` - Check availability with price calculation
|
||||||
|
- `GET /wp-bnb/v1/rooms/{id}/calendar` - Get monthly calendar data
|
||||||
|
- `POST /wp-bnb/v1/availability/search` - Search available rooms by date range and criteria
|
||||||
|
- Bookings API:
|
||||||
|
- `POST /wp-bnb/v1/bookings` - Create booking (public, creates pending status)
|
||||||
|
- `GET /wp-bnb/v1/bookings` - List bookings with filters (admin)
|
||||||
|
- `GET /wp-bnb/v1/bookings/{id}` - Get booking details (admin)
|
||||||
|
- `PATCH /wp-bnb/v1/bookings/{id}` - Update booking (admin)
|
||||||
|
- `DELETE /wp-bnb/v1/bookings/{id}` - Cancel booking (admin)
|
||||||
|
- `POST /wp-bnb/v1/bookings/{id}/confirm` - Confirm pending booking (admin)
|
||||||
|
- `POST /wp-bnb/v1/bookings/{id}/check-in` - Check in guest (admin)
|
||||||
|
- `POST /wp-bnb/v1/bookings/{id}/check-out` - Check out guest (admin)
|
||||||
|
- Guests API (admin only):
|
||||||
|
- `GET /wp-bnb/v1/guests` - List guests with pagination
|
||||||
|
- `GET /wp-bnb/v1/guests/{id}` - Get guest details (excludes encrypted ID numbers)
|
||||||
|
- `GET /wp-bnb/v1/guests/search` - Search guests by name/email
|
||||||
|
- `GET /wp-bnb/v1/guests/{id}/bookings` - Get guest's booking history
|
||||||
|
- Services API:
|
||||||
|
- `GET /wp-bnb/v1/services` - List active services with categories
|
||||||
|
- `GET /wp-bnb/v1/services/{id}` - Get service details with pricing info
|
||||||
|
- `POST /wp-bnb/v1/services/{id}/calculate` - Calculate service price for booking
|
||||||
|
- Pricing API:
|
||||||
|
- `POST /wp-bnb/v1/pricing/calculate` - Full price calculation with services
|
||||||
|
- `GET /wp-bnb/v1/pricing/seasons` - Get configured seasons and pricing modifiers
|
||||||
|
- API Settings tab in plugin settings:
|
||||||
|
- Enable/disable REST API toggle
|
||||||
|
- Enable/disable rate limiting toggle
|
||||||
|
- Endpoint documentation table
|
||||||
|
- Authentication instructions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to initialize REST API on `rest_api_init` hook
|
||||||
|
- Settings page now has seven tabs: General, Pricing, License, Updates, Metrics, API
|
||||||
|
- README.md updated with comprehensive REST API documentation
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Rate limiting: public (60/min), availability (30/min), booking (10/min), admin (120/min)
|
||||||
|
- Admin endpoints require `edit_posts` capability
|
||||||
|
- Supports WordPress Application Passwords for external API access
|
||||||
|
- Client identification by user ID (authenticated) or IP address (anonymous)
|
||||||
|
- Proxy/Cloudflare IP detection via X-Forwarded-For and CF-Connecting-IP headers
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Prometheus Metrics Integration:
|
||||||
|
- New `src/Integration/Prometheus.php` class for metrics collection
|
||||||
|
- Integration with wp-prometheus plugin via `wp_prometheus_collect_metrics` hook
|
||||||
|
- Inventory metrics: buildings total, rooms by status, services by status
|
||||||
|
- Booking metrics: bookings by status, check-ins/check-outs today, upcoming 7 days, avg duration
|
||||||
|
- Guest metrics: total guests, guests by status, repeat guests, new guests this month
|
||||||
|
- Occupancy metrics: current rate, monthly rate, occupied rooms, total bed capacity
|
||||||
|
- Revenue metrics: this month, YTD, average booking value, services revenue
|
||||||
|
- Grafana Dashboard:
|
||||||
|
- Pre-configured dashboard at `assets/grafana/wp-bnb-dashboard.json`
|
||||||
|
- Automatic registration with wp-prometheus dashboard provider
|
||||||
|
- Occupancy gauges with color-coded thresholds
|
||||||
|
- Pie charts for bookings, rooms, and guests by status
|
||||||
|
- Revenue and guest statistics panels
|
||||||
|
- Responsive grid layout with 24 panels
|
||||||
|
- Settings page Metrics tab:
|
||||||
|
- Enable/disable metrics collection toggle
|
||||||
|
- WP Prometheus detection with status indicator
|
||||||
|
- Complete metrics reference table
|
||||||
|
- Dashboard file location and export info
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to initialize Prometheus integration
|
||||||
|
- Settings page now has six tabs: General, Pricing, License, Updates, Metrics
|
||||||
|
|
||||||
|
## [0.8.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Admin Dashboard with comprehensive statistics:
|
||||||
|
- Occupancy overview card with current rate and comparison to last month
|
||||||
|
- Revenue summary card with this month, YTD, and comparison
|
||||||
|
- Bookings stat card with pending/confirmed counts
|
||||||
|
- Guests stat card with total, new, and repeat counts
|
||||||
|
- Today's Activity widget showing check-ins and check-outs
|
||||||
|
- Upcoming Bookings widget (next 7 days)
|
||||||
|
- Quick Actions widget for common tasks
|
||||||
|
- Chart.js integration for visual trend charts:
|
||||||
|
- Occupancy trend line chart (30 days)
|
||||||
|
- Revenue trend bar chart (6 months)
|
||||||
|
- Reports page with three report types:
|
||||||
|
- Occupancy Report: by room, by building, with progress bars
|
||||||
|
- Revenue Report: by room, by pricing tier, with averages
|
||||||
|
- Guest Statistics: top guests, nationality breakdown
|
||||||
|
- Date range filters (this month, last month, this year, custom)
|
||||||
|
- Export functionality:
|
||||||
|
- CSV export for all report types (native PHP)
|
||||||
|
- PDF export using mPDF library with professional styling
|
||||||
|
- New Composer dependency: mpdf/mpdf ^8.2 for PDF generation
|
||||||
|
- Dashboard and Reports CSS styles in admin.css (~350 lines)
|
||||||
|
- JavaScript chart initialization and report page handlers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Dashboard now uses dedicated `src/Admin/Dashboard.php` class
|
||||||
|
- Admin menu now includes Reports submenu item
|
||||||
|
- Asset enqueuing conditionally loads Chart.js on dashboard page
|
||||||
|
|
||||||
|
## [0.7.2] - 2026-02-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- CF7 tag generator buttons not appearing in admin form editor
|
||||||
|
- Moved CF7 initialization from frontend-only to run in both admin and frontend contexts
|
||||||
|
- Tag generators now properly register via `wpcf7_admin_init` hook
|
||||||
|
|
||||||
|
## [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
|
## [0.6.1] - 2026-02-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
255
CLAUDE.md
255
CLAUDE.md
@@ -40,7 +40,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
### Known Bugs
|
### Known Bugs
|
||||||
|
|
||||||
No known bugs at this time.
|
(none)
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -260,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)
|
||||||
@@ -270,10 +272,12 @@ wp-bnb/
|
|||||||
│ ├── css/
|
│ ├── css/
|
||||||
│ │ ├── admin.css # Admin styles
|
│ │ ├── admin.css # Admin styles
|
||||||
│ │ ├── blocks-editor.css # Gutenberg editor styles
|
│ │ ├── blocks-editor.css # Gutenberg editor styles
|
||||||
|
│ │ ├── cf7-integration.css # CF7 form styles
|
||||||
│ │ └── frontend.css # Frontend styles (~1250 lines)
|
│ │ └── frontend.css # Frontend styles (~1250 lines)
|
||||||
│ └── js/
|
│ └── js/
|
||||||
│ ├── admin.js # Admin scripts
|
│ ├── admin.js # Admin scripts
|
||||||
│ ├── blocks-editor.js # Gutenberg editor scripts
|
│ ├── blocks-editor.js # Gutenberg editor scripts
|
||||||
|
│ ├── cf7-integration.js # CF7 form scripts
|
||||||
│ └── frontend.js # Frontend scripts (~825 lines)
|
│ └── frontend.js # Frontend scripts (~825 lines)
|
||||||
├── templates/ # Twig templates (future)
|
├── templates/ # Twig templates (future)
|
||||||
├── languages/ # Translation files (future)
|
├── languages/ # Translation files (future)
|
||||||
@@ -338,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
|
||||||
@@ -658,7 +662,6 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Tagged: `v0.6.0`
|
- Tagged: `v0.6.0`
|
||||||
- Pushed to origin: dev, main, v0.6.0
|
- Pushed to origin: dev, main, v0.6.0
|
||||||
|
|
||||||
|
|
||||||
### 2026-02-03 - Bug Fixes and Enhancements
|
### 2026-02-03 - Bug Fixes and Enhancements
|
||||||
|
|
||||||
**Completed:**
|
**Completed:**
|
||||||
@@ -743,3 +746,249 @@ Admin features always work; frontend requires valid license.
|
|||||||
- `use_block_editor_for_post_type` filter disables Gutenberg per post type
|
- `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
|
- 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
|
- 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
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.7.1 (CF7 Tag Generators)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Added CF7 tag generator buttons for admin form editor
|
||||||
|
- Hook into `wpcf7_admin_init` to register tag generators
|
||||||
|
- `register_tag_generators()` method using `WPCF7_TagGenerator::add()`
|
||||||
|
- BnB Building select generator with `first_as_label` option
|
||||||
|
- BnB Room select generator with `building_field` and `include_price` options
|
||||||
|
- BnB Check-in date generator with `min_advance` and `max_advance` options
|
||||||
|
- BnB Check-out date generator with `checkin_field`, `min_nights`, `max_nights` options
|
||||||
|
- BnB Guests count generator with `room_field`, `min`, `max`, `default` options
|
||||||
|
- All generators support `id` and `class` attribute configuration
|
||||||
|
- CF7 v2 tag generator format with `version => '2'` option
|
||||||
|
- Removed bug from Known Bugs section in CLAUDE.md
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `src/Integration/CF7.php` - Added ~560 lines for tag generator registration and modal callbacks
|
||||||
|
- `CLAUDE.md` - Removed bug from Known Bugs section
|
||||||
|
- `wp-bnb.php` - Version bump to 0.7.1
|
||||||
|
- `CHANGELOG.md` - Added v0.7.1 release notes
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- CF7 tag generators use `WPCF7_TagGenerator::get_instance()->add()` for registration
|
||||||
|
- Tag generator callbacks receive `$contact_form` and `$options` parameters
|
||||||
|
- CF7 v2 tag generator format requires `'version' => '2'` in options array
|
||||||
|
- Modal HTML structure: `<header class="description-box">`, `<div class="control-box">`, `<footer class="insert-box">`
|
||||||
|
- Form inputs use classes like `tg-name`, `oneline`, `option`, `idvalue`, `classvalue` for CF7's JavaScript handling
|
||||||
|
- The `tag-generator-insert-button` class triggers CF7's tag insertion JavaScript
|
||||||
|
- Mail tag tip shows users which tag to use in the Mail tab
|
||||||
|
- Tag generators are registered at priority 60 in `wpcf7_admin_init` to appear after core tags
|
||||||
|
|
||||||
|
**Released:**
|
||||||
|
|
||||||
|
- Committed: `a784d92` on dev branch
|
||||||
|
- Merged to main (fast-forward)
|
||||||
|
- Tagged: `v0.7.1`
|
||||||
|
- Pushed to origin: dev, main, v0.7.1
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.8.0 (Dashboard & Reports)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Admin/Dashboard.php` class (~700 lines)
|
||||||
|
- `render()` method for full dashboard page
|
||||||
|
- Occupancy stat card with current rate, room count, comparison to last month
|
||||||
|
- Revenue stat card with this month, YTD, comparison
|
||||||
|
- Bookings stat card with pending/confirmed counts
|
||||||
|
- Guests stat card with total, new this month, repeat guests
|
||||||
|
- Today's Activity widget showing check-ins and check-outs
|
||||||
|
- Upcoming Bookings widget with next 7 days' bookings
|
||||||
|
- Quick Actions widget (New Booking, New Guest, Calendar, Reports)
|
||||||
|
- Occupancy trend chart (30-day line chart)
|
||||||
|
- Revenue trend chart (6-month bar chart)
|
||||||
|
- Data methods: `get_occupancy_stats()`, `get_revenue_stats()`, `get_booking_stats()`, `get_guest_stats()`
|
||||||
|
- Chart data methods: `get_occupancy_trend_data()`, `get_revenue_trend_data()`
|
||||||
|
- Transient caching for expensive calculations (1-hour expiry)
|
||||||
|
- Created `src/Admin/Reports.php` class (~1100 lines)
|
||||||
|
- Tabbed interface: Occupancy, Revenue, Guests
|
||||||
|
- Date range filters with presets (this month, last month, this year, custom)
|
||||||
|
- Occupancy Report: by room, by building with progress bars and status labels
|
||||||
|
- Revenue Report: by room, by pricing tier, with averages
|
||||||
|
- Guest Statistics: top guests by revenue, nationality breakdown
|
||||||
|
- CSV export using native PHP `fputcsv()`
|
||||||
|
- PDF export using mPDF with professional HTML styling
|
||||||
|
- Summary cards with key metrics
|
||||||
|
- Progress bar visualizations for occupancy rates
|
||||||
|
- Added mPDF dependency to `composer.json` (`mpdf/mpdf ^8.2`)
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added Dashboard and Reports class imports
|
||||||
|
- `render_dashboard_page()` delegates to `Dashboard::render()`
|
||||||
|
- Added `render_reports_page()` method
|
||||||
|
- Reports submenu registration
|
||||||
|
- Updated menu ordering to include Reports
|
||||||
|
- Chart.js CDN enqueuing on dashboard page
|
||||||
|
- Chart data passed via `wp_localize_script()`
|
||||||
|
- Dashboard CSS styles (~350 lines in admin.css)
|
||||||
|
- Responsive grid layout (4-col stats, 2-col charts, 3-col activity)
|
||||||
|
- Stat cards with icons and gradients
|
||||||
|
- Widget components with headers
|
||||||
|
- Activity list styling
|
||||||
|
- Upcoming bookings table
|
||||||
|
- Quick action buttons grid
|
||||||
|
- Reports CSS styles (~200 lines in admin.css)
|
||||||
|
- Filter form layout
|
||||||
|
- Summary cards with primary variant
|
||||||
|
- Progress bars for occupancy
|
||||||
|
- Status labels (high/medium/low)
|
||||||
|
- Export buttons styling
|
||||||
|
- JavaScript additions in admin.js
|
||||||
|
- `initDashboardCharts()` for Chart.js initialization
|
||||||
|
- Occupancy line chart with tooltips and styling
|
||||||
|
- Revenue bar chart with currency formatting
|
||||||
|
- `initReportsPage()` for custom date toggle
|
||||||
|
- Updated version to 0.8.0
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
|
||||||
|
- `src/Admin/Dashboard.php` - Dashboard page with widgets and charts
|
||||||
|
- `src/Admin/Reports.php` - Reports page with tabs and export
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `composer.json` - Added mpdf/mpdf dependency
|
||||||
|
- `composer.lock` - Updated with mPDF and dependencies
|
||||||
|
- `src/Plugin.php` - Dashboard/Reports integration, Chart.js enqueuing
|
||||||
|
- `assets/css/admin.css` - Dashboard and Reports styles (~550 lines added)
|
||||||
|
- `assets/js/admin.js` - Chart initialization, reports page handlers
|
||||||
|
- `wp-bnb.php` - Version bump to 0.8.0
|
||||||
|
- `CHANGELOG.md` - Added v0.8.0 release notes
|
||||||
|
- `PLAN.md` - Marked Phase 8 as complete
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Chart.js CDN loading requires conditional enqueuing to avoid loading on all admin pages
|
||||||
|
- Dashboard data methods should use transient caching for expensive queries
|
||||||
|
- PDF export with mPDF requires HTML string generation with inline CSS
|
||||||
|
- Reports use `get_posts()` with meta queries for date range filtering
|
||||||
|
- Progress bar visualization done with CSS positioning and `min(100, value)` clamping
|
||||||
|
- Chart.js 4.x uses `new Chart()` constructor with configuration object
|
||||||
|
- PDF generation needs `try/catch` for mPDF exceptions
|
||||||
|
- CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility
|
||||||
|
- Guest data aggregation from bookings uses unique key pattern for anonymous guests
|
||||||
|
- Occupancy calculation: (booked nights / total room nights) * 100
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.9.0 (Prometheus Metrics)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Integration/Prometheus.php` class (~700 lines)
|
||||||
|
- Integration with wp-prometheus via `wp_prometheus_collect_metrics` hook
|
||||||
|
- Dashboard registration via `wp_prometheus_register_dashboards` hook
|
||||||
|
- Option to enable/disable metrics collection
|
||||||
|
- Inventory metrics: buildings total, rooms by status, services by status
|
||||||
|
- Booking metrics: by status, check-ins/outs today, upcoming 7 days, avg duration
|
||||||
|
- Guest metrics: total, by status, repeat guests, new this month
|
||||||
|
- Occupancy metrics: current rate, monthly rate, occupied rooms, bed capacity
|
||||||
|
- Revenue metrics: this month, YTD, avg booking value, services revenue
|
||||||
|
- Optimized SQL queries using `$wpdb->prepare()` throughout
|
||||||
|
- Created `assets/grafana/wp-bnb-dashboard.json` Grafana dashboard
|
||||||
|
- 24 panels with responsive grid layout
|
||||||
|
- Occupancy gauges with color-coded thresholds (red < 30%, orange < 50%, yellow < 70%, green ≥ 70%)
|
||||||
|
- Pie charts for bookings, rooms, and guests by status
|
||||||
|
- Revenue stat panels (this month, YTD, avg value, services)
|
||||||
|
- Guest stat panels (total, new, repeat, active services)
|
||||||
|
- Today's activity panels (check-ins, check-outs, upcoming)
|
||||||
|
- Prometheus datasource variable for flexibility
|
||||||
|
- Auto-refresh every 5 minutes
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added Prometheus class import
|
||||||
|
- Initialized Prometheus integration in `init_components()`
|
||||||
|
- Added "Metrics" tab to settings page (6 tabs total)
|
||||||
|
- Added `render_metrics_settings()` method with WP Prometheus detection
|
||||||
|
- Added `save_metrics_settings()` method
|
||||||
|
- Metrics reference table showing all available metrics
|
||||||
|
- Updated version to 0.9.0
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
|
||||||
|
- `src/Integration/Prometheus.php` - Prometheus metrics integration class
|
||||||
|
- `assets/grafana/wp-bnb-dashboard.json` - Pre-configured Grafana dashboard
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `src/Plugin.php` - Prometheus initialization, metrics settings tab
|
||||||
|
- `wp-bnb.php` - Version bump to 0.9.0 (header and constant)
|
||||||
|
- `CHANGELOG.md` - Added v0.9.0 release notes
|
||||||
|
- `PLAN.md` - Marked Phase 9 as complete
|
||||||
|
- `README.md` - Added Prometheus metrics documentation
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- wp-prometheus uses `wp_prometheus_collect_metrics` action with collector object
|
||||||
|
- Collector provides `register_gauge()` for fluctuating values
|
||||||
|
- Labels are passed as array to `register_gauge()`, values to `set()`
|
||||||
|
- Grafana dashboard JSON requires proper panel IDs and grid positions
|
||||||
|
- Occupancy queries need careful date range handling for month boundaries
|
||||||
|
- Revenue queries use `DECIMAL(10,2)` casting for accurate sums
|
||||||
|
- Metrics should be cached or computed efficiently as they're scraped frequently
|
||||||
|
- Dashboard registration requires file path, title, description, icon, and plugin name
|
||||||
|
- Settings tab detection uses `$prometheus_active` to show WP Prometheus status
|
||||||
|
|||||||
98
PLAN.md
98
PLAN.md
@@ -149,36 +149,66 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Building rooms widget
|
- [x] Building rooms widget
|
||||||
- [x] 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) - Complete
|
||||||
|
|
||||||
### Admin Dashboard
|
### Admin Dashboard
|
||||||
|
|
||||||
- [ ] Occupancy overview
|
- [x] Occupancy overview
|
||||||
- [ ] Upcoming check-ins/check-outs
|
- [x] Upcoming check-ins/check-outs
|
||||||
- [ ] Revenue summary
|
- [x] Revenue summary
|
||||||
- [ ] Quick actions
|
- [x] Quick actions
|
||||||
|
|
||||||
### Reports
|
### Reports
|
||||||
|
|
||||||
- [ ] Occupancy report
|
- [x] Occupancy report
|
||||||
- [ ] Revenue report
|
- [x] Revenue report
|
||||||
- [ ] Guest statistics
|
- [x] Guest statistics
|
||||||
- [ ] Export functionality (CSV, PDF)
|
- [x] Export functionality (CSV, PDF)
|
||||||
|
|
||||||
|
## Phase 9: Prometheus Metrics (v0.9.0) - Complete
|
||||||
|
|
||||||
|
- [x] Meaningful Metrics for this Plugin:
|
||||||
|
- Inventory: buildings, rooms by status, services by status
|
||||||
|
- Bookings: by status, check-ins/check-outs today, upcoming, avg duration
|
||||||
|
- Guests: total, by status, repeat guests, new this month
|
||||||
|
- Occupancy: current rate, monthly rate, occupied rooms, bed capacity
|
||||||
|
- Revenue: this month, YTD, average booking value, services revenue
|
||||||
|
- [x] Example Grafana Dashboard:
|
||||||
|
- Pre-configured dashboard JSON at `assets/grafana/wp-bnb-dashboard.json`
|
||||||
|
- Automatic registration with wp-prometheus
|
||||||
|
- 24 panels with gauges, pie charts, and stat displays
|
||||||
|
- [x] Update settings page to enable/disable metrics
|
||||||
|
|
||||||
|
### Phase 10: API Endpoints (v0.10.0) - Complete
|
||||||
|
|
||||||
|
- [x] REST API for rooms (list, details, availability, calendar)
|
||||||
|
- [x] REST API for availability (search available rooms)
|
||||||
|
- [x] REST API for bookings (CRUD, status transitions)
|
||||||
|
- [x] REST API for buildings, guests, services, pricing
|
||||||
|
- [x] Authentication (Application Passwords, edit_posts capability)
|
||||||
|
- [x] Transient-based rate limiting with tiered limits
|
||||||
|
- [x] API settings tab with enable/disable toggles
|
||||||
|
|
||||||
|
## Phase 11: Security Audit (v0.11.0)
|
||||||
|
|
||||||
|
- [ ] Check for Wordpress best-practices
|
||||||
|
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
|
||||||
|
- [ ] Test the API-Endpoints against a local live system under <http://localhost:9080/> for common vulnerabilities
|
||||||
|
|
||||||
## Future Considerations (v1.0.0+)
|
## Future Considerations (v1.0.0+)
|
||||||
|
|
||||||
@@ -189,13 +219,6 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [ ] Order management
|
- [ ] Order management
|
||||||
- [ ] Refund handling
|
- [ ] Refund handling
|
||||||
|
|
||||||
### API Endpoints
|
|
||||||
|
|
||||||
- [ ] REST API for rooms
|
|
||||||
- [ ] REST API for availability
|
|
||||||
- [ ] REST API for bookings
|
|
||||||
- [ ] Authentication and rate limiting
|
|
||||||
|
|
||||||
### Multi-language Support
|
### Multi-language Support
|
||||||
|
|
||||||
- [ ] Full translation support
|
- [ ] Full translation support
|
||||||
@@ -286,15 +309,18 @@ 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 | Complete |
|
| 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 | Complete |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 0.9.0 | Prometheus Metrics | Complete |
|
||||||
|
| 0.10.0 | API Endpoints | TBD |
|
||||||
|
| 0.11.0 | Security Audit | TBD |
|
||||||
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
484
README.md
484
README.md
@@ -18,13 +18,17 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
|
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
|
||||||
- **Auto-Updates**: Automatic update checks and installation from license server
|
- **Auto-Updates**: Automatic update checks and installation from license server
|
||||||
- **Development Mode**: License bypass for local development environments
|
- **Development Mode**: License bypass for local development environments
|
||||||
- **Contact Form 7 Integration**: Accept booking requests through forms (planned)
|
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
|
||||||
|
- **Dashboard**: Comprehensive admin dashboard with statistics and charts
|
||||||
|
- **Reports**: Detailed reports with CSV and PDF export
|
||||||
|
- **Prometheus Metrics**: Expose operational metrics for monitoring with Grafana
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
@@ -97,6 +101,80 @@ The plugin automatically detects local development environments and bypasses lic
|
|||||||
2. View guest records and booking history
|
2. View guest records and booking history
|
||||||
3. Manage guest information
|
3. Manage guest information
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
The dashboard (**WP BnB → Dashboard**) provides an at-a-glance overview of your B&B operations:
|
||||||
|
|
||||||
|
**Statistics Cards:**
|
||||||
|
|
||||||
|
- **Occupancy Rate** - Current percentage of rooms occupied with trend indicator
|
||||||
|
- **Monthly Revenue** - This month's revenue with comparison to previous month
|
||||||
|
- **Total Bookings** - Active bookings count with status breakdown
|
||||||
|
- **Total Guests** - Guest count with new guests this month
|
||||||
|
|
||||||
|
**Today's Activity:**
|
||||||
|
|
||||||
|
- Check-ins scheduled for today with guest names and room assignments
|
||||||
|
- Check-outs scheduled for today
|
||||||
|
- Quick links to manage each booking
|
||||||
|
|
||||||
|
**Upcoming Bookings:**
|
||||||
|
|
||||||
|
- Next 7 days of arrivals
|
||||||
|
- Guest name, room, dates, and booking status
|
||||||
|
- Direct links to booking details
|
||||||
|
|
||||||
|
**Quick Actions:**
|
||||||
|
|
||||||
|
- New Booking - Create a booking directly
|
||||||
|
- New Guest - Add a guest record
|
||||||
|
- View Calendar - Open the availability calendar
|
||||||
|
- View Reports - Access detailed reports
|
||||||
|
|
||||||
|
**Trend Charts:**
|
||||||
|
|
||||||
|
- 30-day occupancy trend line chart
|
||||||
|
- 6-month revenue bar chart
|
||||||
|
|
||||||
|
### Reports
|
||||||
|
|
||||||
|
Access detailed reports at **WP BnB → Reports**. All reports support date range filtering and export.
|
||||||
|
|
||||||
|
**Occupancy Report:**
|
||||||
|
|
||||||
|
- Overall occupancy percentage for the selected period
|
||||||
|
- Breakdown by room showing nights booked, available, and occupancy rate
|
||||||
|
- Visual progress bars for easy comparison
|
||||||
|
- Total nights booked vs. available across all rooms
|
||||||
|
|
||||||
|
**Revenue Report:**
|
||||||
|
|
||||||
|
- Total revenue for the selected period
|
||||||
|
- Revenue breakdown by room
|
||||||
|
- Revenue breakdown by pricing tier (short-term, mid-term, long-term)
|
||||||
|
- Revenue from additional services
|
||||||
|
- Average booking value
|
||||||
|
|
||||||
|
**Guest Statistics:**
|
||||||
|
|
||||||
|
- Total guests and new guests in period
|
||||||
|
- Repeat guest rate (guests with 2+ bookings)
|
||||||
|
- Top guests by total spending
|
||||||
|
- Guest nationality distribution
|
||||||
|
- Average spending per guest
|
||||||
|
|
||||||
|
**Export Options:**
|
||||||
|
|
||||||
|
- **CSV Export** - Download report data as spreadsheet-compatible CSV
|
||||||
|
- **PDF Export** - Generate formatted PDF reports for printing or archiving
|
||||||
|
|
||||||
|
**Date Filters:**
|
||||||
|
|
||||||
|
- This Month (default)
|
||||||
|
- Last Month
|
||||||
|
- This Year
|
||||||
|
- Custom date range
|
||||||
|
|
||||||
## Shortcodes
|
## Shortcodes
|
||||||
|
|
||||||
Display buildings and rooms on your site using shortcodes:
|
Display buildings and rooms on your site using shortcodes:
|
||||||
@@ -143,6 +221,152 @@ Available sidebar widgets:
|
|||||||
- **Building Rooms** - List all rooms in a building
|
- **Building Rooms** - List all rooms in a building
|
||||||
- **Availability Calendar** - Mini calendar showing booking status
|
- **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
|
||||||
|
|
||||||
Developers can customize behavior using these hooks:
|
Developers can customize behavior using these hooks:
|
||||||
@@ -160,6 +384,264 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) {
|
|||||||
} );
|
} );
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Prometheus Metrics
|
||||||
|
|
||||||
|
The plugin integrates with [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) to expose operational metrics for monitoring with Prometheus and Grafana.
|
||||||
|
|
||||||
|
### Enabling Metrics
|
||||||
|
|
||||||
|
1. Install and activate the WP Prometheus plugin
|
||||||
|
2. Navigate to **WP BnB → Settings → Metrics**
|
||||||
|
3. Enable "Expose BnB metrics via Prometheus"
|
||||||
|
4. Metrics will be available at your site's `/metrics/` endpoint
|
||||||
|
|
||||||
|
### Available Metrics
|
||||||
|
|
||||||
|
**Inventory Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_buildings_total` - Total number of buildings
|
||||||
|
- `wp_bnb_rooms_total{status}` - Rooms by status (available, occupied, maintenance, inactive)
|
||||||
|
- `wp_bnb_services_total{status}` - Services by status (active, inactive)
|
||||||
|
- `wp_bnb_total_capacity_beds` - Total bed capacity across all rooms
|
||||||
|
|
||||||
|
**Booking Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_bookings_total{status}` - Bookings by status (pending, confirmed, checked_in, checked_out, cancelled)
|
||||||
|
- `wp_bnb_checkins_today` - Check-ins scheduled for today
|
||||||
|
- `wp_bnb_checkouts_today` - Check-outs scheduled for today
|
||||||
|
- `wp_bnb_bookings_upcoming_7days` - Bookings starting in next 7 days
|
||||||
|
- `wp_bnb_booking_avg_duration_nights` - Average booking duration
|
||||||
|
|
||||||
|
**Occupancy Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_occupancy_rate_current` - Current room occupancy rate (percentage)
|
||||||
|
- `wp_bnb_occupancy_rate_this_month` - Monthly occupancy rate (percentage)
|
||||||
|
- `wp_bnb_rooms_currently_occupied` - Rooms currently occupied
|
||||||
|
|
||||||
|
**Revenue Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_revenue_this_month{currency}` - Revenue for current month
|
||||||
|
- `wp_bnb_revenue_ytd{currency}` - Revenue year to date
|
||||||
|
- `wp_bnb_booking_avg_value{currency}` - Average booking value
|
||||||
|
- `wp_bnb_services_revenue_this_month{currency}` - Services revenue this month
|
||||||
|
|
||||||
|
**Guest Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_guests_total` - Total registered guests
|
||||||
|
- `wp_bnb_guests_by_status{status}` - Guests by status (active, blocked, vip)
|
||||||
|
- `wp_bnb_guests_repeat` - Guests with more than one booking
|
||||||
|
- `wp_bnb_guests_new_this_month` - New guests this month
|
||||||
|
|
||||||
|
### Grafana Dashboard
|
||||||
|
|
||||||
|
A pre-configured Grafana dashboard is included at `assets/grafana/wp-bnb-dashboard.json`. If WP Prometheus is installed, the dashboard is automatically registered and available for export.
|
||||||
|
|
||||||
|
The dashboard includes:
|
||||||
|
|
||||||
|
- Occupancy gauges with color-coded thresholds
|
||||||
|
- Bookings, rooms, and guests pie charts by status
|
||||||
|
- Revenue and guest statistics panels
|
||||||
|
- Today's check-ins/check-outs
|
||||||
|
- Trend indicators
|
||||||
|
|
||||||
|
## REST API
|
||||||
|
|
||||||
|
The plugin provides a comprehensive REST API for integration with external applications, mobile apps, and third-party services.
|
||||||
|
|
||||||
|
### Enabling the API
|
||||||
|
|
||||||
|
1. Navigate to **WP BnB → Settings → API**
|
||||||
|
2. Enable "Enable REST API"
|
||||||
|
3. Optionally enable rate limiting for protection against abuse
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
|
||||||
|
All API endpoints are prefixed with:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://your-site.com/wp-json/wp-bnb/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
**Public endpoints** (room listings, availability checks) require no authentication.
|
||||||
|
|
||||||
|
**Admin endpoints** (booking management, guest data) require authentication via:
|
||||||
|
|
||||||
|
- **Cookie + Nonce**: For same-domain JavaScript requests
|
||||||
|
- **Application Passwords**: For external applications (WordPress 5.6+, recommended)
|
||||||
|
|
||||||
|
To create an Application Password:
|
||||||
|
|
||||||
|
1. Go to **Users → Profile**
|
||||||
|
2. Scroll to "Application Passwords"
|
||||||
|
3. Enter a name and click "Add New Application Password"
|
||||||
|
4. Use the generated password with HTTP Basic Auth
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -u "username:app-password" https://site.com/wp-json/wp-bnb/v1/bookings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
| ------ | -------- | ----------- |
|
||||||
|
| GET | `/buildings` | List all buildings |
|
||||||
|
| GET | `/buildings/{id}` | Get building details |
|
||||||
|
| GET | `/buildings/{id}/rooms` | Get rooms in a building |
|
||||||
|
| GET | `/rooms` | List/search rooms |
|
||||||
|
| GET | `/rooms/{id}` | Get room details |
|
||||||
|
| GET | `/rooms/{id}/availability` | Check room availability |
|
||||||
|
| GET | `/rooms/{id}/calendar` | Get monthly calendar data |
|
||||||
|
| POST | `/availability/search` | Search available rooms |
|
||||||
|
| GET | `/services` | List all services |
|
||||||
|
| GET | `/services/{id}` | Get service details |
|
||||||
|
| POST | `/pricing/calculate` | Calculate booking price |
|
||||||
|
| POST | `/bookings` | Create a new booking (pending status) |
|
||||||
|
|
||||||
|
### Admin Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
| ------ | -------- | ----------- |
|
||||||
|
| GET | `/bookings` | List all bookings |
|
||||||
|
| GET | `/bookings/{id}` | Get booking details |
|
||||||
|
| PATCH | `/bookings/{id}` | Update a booking |
|
||||||
|
| DELETE | `/bookings/{id}` | Cancel a booking |
|
||||||
|
| POST | `/bookings/{id}/confirm` | Confirm a pending booking |
|
||||||
|
| POST | `/bookings/{id}/check-in` | Check in a guest |
|
||||||
|
| POST | `/bookings/{id}/check-out` | Check out a guest |
|
||||||
|
| GET | `/guests` | List all guests |
|
||||||
|
| GET | `/guests/{id}` | Get guest details |
|
||||||
|
| GET | `/guests/search` | Search guests |
|
||||||
|
| GET | `/guests/{id}/bookings` | Get guest's booking history |
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
When enabled, rate limits are applied per client (by user ID or IP address):
|
||||||
|
|
||||||
|
| Type | Limit | Applies To |
|
||||||
|
| ---- | ----- | ---------- |
|
||||||
|
| Public | 60/min | Room/building listings |
|
||||||
|
| Availability | 30/min | Availability and calendar endpoints |
|
||||||
|
| Booking | 10/min | Booking creation |
|
||||||
|
| Admin | 120/min | All admin endpoints |
|
||||||
|
|
||||||
|
Rate limit headers are included in responses:
|
||||||
|
|
||||||
|
- `X-RateLimit-Limit`: Maximum requests allowed
|
||||||
|
- `X-RateLimit-Remaining`: Requests remaining in window
|
||||||
|
- `X-RateLimit-Reset`: Unix timestamp when limit resets
|
||||||
|
|
||||||
|
### Example: Check Room Availability
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://site.com/wp-json/wp-bnb/v1/rooms/42/availability?check_in=2026-03-15&check_out=2026-03-20"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"available": true,
|
||||||
|
"room_id": 42,
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"nights": 5,
|
||||||
|
"pricing": {
|
||||||
|
"base_price": 500.00,
|
||||||
|
"seasonal_modifier": 1.0,
|
||||||
|
"weekend_surcharge": 40.00,
|
||||||
|
"total": 540.00,
|
||||||
|
"currency": "CHF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Create a Booking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://site.com/wp-json/wp-bnb/v1/bookings \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"room_id": 42,
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"guests": 2,
|
||||||
|
"guest_info": {
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"phone": "+41 79 123 4567"
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{"service_id": 5, "quantity": 1}
|
||||||
|
],
|
||||||
|
"notes": "Late arrival expected"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"reference": "BNB-2026-00042",
|
||||||
|
"status": "pending",
|
||||||
|
"room": {
|
||||||
|
"id": 42,
|
||||||
|
"title": "Deluxe Suite"
|
||||||
|
},
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"nights": 5,
|
||||||
|
"guests": 2,
|
||||||
|
"pricing": {
|
||||||
|
"room_total": 540.00,
|
||||||
|
"services_total": 50.00,
|
||||||
|
"grand_total": 590.00,
|
||||||
|
"currency": "CHF"
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": [{"href": "https://site.com/wp-json/wp-bnb/v1/bookings/123"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Search Available Rooms
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://site.com/wp-json/wp-bnb/v1/availability/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"guests": 2,
|
||||||
|
"amenities": ["wifi", "parking"]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
Errors follow WordPress REST API conventions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "rest_not_found",
|
||||||
|
"message": "Room not found.",
|
||||||
|
"data": {
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common error codes:
|
||||||
|
|
||||||
|
- `rest_invalid_param` (400): Invalid request parameters
|
||||||
|
- `rest_forbidden` (403): Insufficient permissions
|
||||||
|
- `rest_not_found` (404): Resource not found
|
||||||
|
- `rest_conflict` (409): Booking conflict
|
||||||
|
- `rest_rate_limit_exceeded` (429): Rate limit exceeded
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
### Do I need a license to use this plugin?
|
### Do I need a license to use this plugin?
|
||||||
|
|||||||
@@ -4,7 +4,382 @@
|
|||||||
* @package Magdev\WpBnb
|
* @package Magdev\WpBnb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Dashboard */
|
/* ============================================
|
||||||
|
Dashboard
|
||||||
|
============================================ */
|
||||||
|
.wp-bnb-dashboard-grid {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-dashboard-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Row - 4 columns */
|
||||||
|
.wp-bnb-stats-row {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Charts Row - 2 columns */
|
||||||
|
.wp-bnb-charts-row {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Row - 3 columns */
|
||||||
|
.wp-bnb-activity-row {
|
||||||
|
grid-template-columns: 1fr 1.5fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
.wp-bnb-stats-row {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.wp-bnb-activity-row {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.wp-bnb-activity-row .wp-bnb-quick-actions {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.wp-bnb-stats-row,
|
||||||
|
.wp-bnb-charts-row,
|
||||||
|
.wp-bnb-activity-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.wp-bnb-activity-row .wp-bnb-quick-actions {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat Cards */
|
||||||
|
.wp-bnb-stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #2271b1, #135e96);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon.revenue {
|
||||||
|
background: linear-gradient(135deg, #00a32a, #007017);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon.bookings {
|
||||||
|
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon.guests {
|
||||||
|
background: linear-gradient(135deg, #e67e22, #d35400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon .dashicons {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #50575e;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change.positive {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change.negative {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widgets */
|
||||||
|
.wp-bnb-widget {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header .wp-bnb-widget-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header .wp-bnb-view-all {
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Widgets */
|
||||||
|
.wp-bnb-chart-widget .wp-bnb-widget-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.wp-bnb-empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-empty-state .dashicons {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
color: #c3c4c7;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-empty-state p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Section */
|
||||||
|
.wp-bnb-activity-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section h4 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section h4 .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section h4 .count {
|
||||||
|
background: #2271b1;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list a:hover {
|
||||||
|
background: #dcdcde;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list strong {
|
||||||
|
color: #1d2327;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list .room {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upcoming Bookings Table */
|
||||||
|
.wp-bnb-upcoming-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #50575e;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table a {
|
||||||
|
color: #2271b1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table a:hover {
|
||||||
|
color: #135e96;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table .wp-bnb-status-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Actions */
|
||||||
|
.wp-bnb-quick-actions .wp-bnb-widget-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-actions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 20px 10px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #1d2327;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn:hover {
|
||||||
|
background: #2271b1;
|
||||||
|
border-color: #2271b1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn .dashicons {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn span:last-child {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy Dashboard (backward compatibility) */
|
||||||
.wp-bnb-dashboard {
|
.wp-bnb-dashboard {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #c3c4c7;
|
border: 1px solid #c3c4c7;
|
||||||
@@ -1345,3 +1720,305 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #135e96;
|
color: #135e96;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Reports Page
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Reports Tabs */
|
||||||
|
.wp-bnb-reports-tabs .nav-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-reports-tabs .nav-tab .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reports Content Container */
|
||||||
|
.wp-bnb-reports-content {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-top: none;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reports Filters */
|
||||||
|
.wp-bnb-reports-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-filter-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-filter-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-filter-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-period-select {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-custom-dates {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-custom-dates input[type="date"] {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-period {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-period strong {
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report Body */
|
||||||
|
.wp-bnb-report-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-section h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
margin: 25px 0 15px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-section h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary Cards */
|
||||||
|
.wp-bnb-summary-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-cards.secondary {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.wp-bnb-summary-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.wp-bnb-summary-cards.secondary {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.wp-bnb-summary-cards,
|
||||||
|
.wp-bnb-summary-cards.secondary {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card {
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.primary {
|
||||||
|
background: linear-gradient(135deg, #d4edda, #c3e6cb);
|
||||||
|
border-color: #a3d4aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.small {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2271b1;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.primary .wp-bnb-summary-value {
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.small .wp-bnb-summary-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #50575e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report Tables */
|
||||||
|
.wp-bnb-report-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table th,
|
||||||
|
.wp-bnb-report-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table th.num,
|
||||||
|
.wp-bnb-report-table td.num {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table tfoot th {
|
||||||
|
background: #f0f6fc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table a {
|
||||||
|
color: #2271b1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table a:hover {
|
||||||
|
color: #135e96;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.wp-bnb-progress-bar {
|
||||||
|
position: relative;
|
||||||
|
background: #dcdcde;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #2271b1, #135e96);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-progress-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
text-shadow: 0 0 3px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Labels */
|
||||||
|
.wp-bnb-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-status.high {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-status.medium {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-status.low {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Data Message */
|
||||||
|
.wp-bnb-no-data {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #787c82;
|
||||||
|
font-style: italic;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export Buttons */
|
||||||
|
.wp-bnb-export-buttons {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-buttons h4 {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-actions .button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-actions .button .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1580
assets/grafana/wp-bnb-dashboard.json
Normal file
1580
assets/grafana/wp-bnb-dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1024,6 +1024,181 @@
|
|||||||
updateServicesTotal();
|
updateServicesTotal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize dashboard charts.
|
||||||
|
*/
|
||||||
|
function initDashboardCharts() {
|
||||||
|
// Only run on dashboard page.
|
||||||
|
if (!wpBnbAdmin.isDashboard || typeof Chart === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chartData = wpBnbAdmin.chartData;
|
||||||
|
if (!chartData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart.js default configuration.
|
||||||
|
Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
|
||||||
|
Chart.defaults.font.size = 12;
|
||||||
|
Chart.defaults.color = '#50575e';
|
||||||
|
|
||||||
|
// Initialize Occupancy Chart.
|
||||||
|
var occupancyCtx = document.getElementById('wp-bnb-occupancy-chart');
|
||||||
|
if (occupancyCtx && chartData.occupancy) {
|
||||||
|
new Chart(occupancyCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: chartData.occupancy.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: wpBnbAdmin.i18n.occupancy,
|
||||||
|
data: chartData.occupancy.data,
|
||||||
|
borderColor: '#2271b1',
|
||||||
|
backgroundColor: 'rgba(34, 113, 177, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 5,
|
||||||
|
pointBackgroundColor: '#2271b1',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointBorderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1d2327',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.parsed.y.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Revenue Chart.
|
||||||
|
var revenueCtx = document.getElementById('wp-bnb-revenue-chart');
|
||||||
|
if (revenueCtx && chartData.revenue) {
|
||||||
|
new Chart(revenueCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: chartData.revenue.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: wpBnbAdmin.i18n.revenue,
|
||||||
|
data: chartData.revenue.data,
|
||||||
|
backgroundColor: 'rgba(0, 163, 42, 0.8)',
|
||||||
|
borderColor: '#00a32a',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
barPercentage: 0.6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1d2327',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return new Intl.NumberFormat('de-CH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CHF'
|
||||||
|
}).format(context.parsed.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return new Intl.NumberFormat('de-CH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CHF',
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize reports page functionality.
|
||||||
|
*/
|
||||||
|
function initReportsPage() {
|
||||||
|
var $periodSelect = $('.wp-bnb-period-select');
|
||||||
|
var $customDates = $('.wp-bnb-custom-dates');
|
||||||
|
|
||||||
|
if (!$periodSelect.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle custom date fields based on period selection.
|
||||||
|
$periodSelect.on('change', function() {
|
||||||
|
if ($(this).val() === 'custom') {
|
||||||
|
$customDates.show();
|
||||||
|
} else {
|
||||||
|
$customDates.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
@@ -1037,6 +1212,8 @@
|
|||||||
initGuestSearch();
|
initGuestSearch();
|
||||||
initServicePricing();
|
initServicePricing();
|
||||||
initBookingServices();
|
initBookingServices();
|
||||||
|
initDashboardCharts();
|
||||||
|
initReportsPage();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
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;
|
||||||
|
})();
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3.0",
|
"php": ">=8.3.0",
|
||||||
"twig/twig": "^3.0",
|
"twig/twig": "^3.0",
|
||||||
"magdev/wc-licensed-product-client": "^0.2"
|
"magdev/wc-licensed-product-client": "^0.2",
|
||||||
|
"mpdf/mpdf": "^8.2"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
357
composer.lock
generated
357
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "aed1e4dd36ea76994768a8379100314b",
|
"content-hash": "ae9fdb5fb51bbef492ad4f2a40406fd3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "magdev/wc-licensed-product-client",
|
"name": "magdev/wc-licensed-product-client",
|
||||||
@@ -56,6 +56,289 @@
|
|||||||
"relative": true
|
"relative": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/mpdf",
|
||||||
|
"version": "v8.2.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/mpdf.git",
|
||||||
|
"reference": "b59670a09498689c33ce639bac8f5ba26721dab3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3",
|
||||||
|
"reference": "b59670a09498689c33ce639bac8f5ba26721dab3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
|
||||||
|
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
|
||||||
|
"myclabs/deep-copy": "^1.7",
|
||||||
|
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||||
|
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"setasign/fpdi": "^2.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.3.0",
|
||||||
|
"mpdf/qrcode": "^1.1.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5.0",
|
||||||
|
"tracy/tracy": "~2.5",
|
||||||
|
"yoast/phpunit-polyfills": "^1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Needed for generation of some types of barcodes",
|
||||||
|
"ext-xml": "Needed mainly for SVG manipulation",
|
||||||
|
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-2.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matěj Humpál",
|
||||||
|
"role": "Developer, maintainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ian Back",
|
||||||
|
"role": "Developer (retired)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
|
||||||
|
"homepage": "https://mpdf.github.io",
|
||||||
|
"keywords": [
|
||||||
|
"pdf",
|
||||||
|
"php",
|
||||||
|
"utf-8"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://mpdf.github.io",
|
||||||
|
"issues": "https://github.com/mpdf/mpdf/issues",
|
||||||
|
"source": "https://github.com/mpdf/mpdf"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.me/mpdf",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-01T10:18:02+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-http-message-shim",
|
||||||
|
"version": "v2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-http-message-shim.git",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrHttpMessageShim\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nigel Cunningham",
|
||||||
|
"email": "nigel.cunningham@technocrat.com.au"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Shim to allow support of different psr/message versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
|
||||||
|
},
|
||||||
|
"time": "2023-10-02T14:34:03+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-log-aware-trait",
|
||||||
|
"version": "v3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/log": "^3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrLogAwareTrait\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Trait to allow support of different psr/log versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2023-05-03T06:19:36+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "myclabs/deep-copy",
|
||||||
|
"version": "1.13.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/collections": "<1.6.8",
|
||||||
|
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/collections": "^1.6.8",
|
||||||
|
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||||
|
"phpspec/prophecy": "^1.10",
|
||||||
|
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/DeepCopy/deep_copy.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopy\\": "src/DeepCopy/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Create deep copies (clones) of your objects",
|
||||||
|
"keywords": [
|
||||||
|
"clone",
|
||||||
|
"copy",
|
||||||
|
"duplicate",
|
||||||
|
"object",
|
||||||
|
"object graph"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||||
|
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-01T08:46:24+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paragonie/random_compat",
|
||||||
|
"version": "v9.99.100",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/paragonie/random_compat.git",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">= 7"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "4.*|5.*",
|
||||||
|
"vimeo/psalm": "^1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paragon Initiative Enterprises",
|
||||||
|
"email": "security@paragonie.com",
|
||||||
|
"homepage": "https://paragonie.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||||
|
"keywords": [
|
||||||
|
"csprng",
|
||||||
|
"polyfill",
|
||||||
|
"pseudorandom",
|
||||||
|
"random"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"email": "info@paragonie.com",
|
||||||
|
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||||
|
"source": "https://github.com/paragonie/random_compat"
|
||||||
|
},
|
||||||
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -313,6 +596,78 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-09-11T13:17:53+00:00"
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "setasign/fpdi",
|
||||||
|
"version": "v2.6.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Setasign/FPDI.git",
|
||||||
|
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||||
|
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"setasign/tfpdf": "<1.31"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7",
|
||||||
|
"setasign/fpdf": "~1.8.6",
|
||||||
|
"setasign/tfpdf": "~1.33",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"tecnickcom/tcpdf": "^6.8"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"setasign\\Fpdi\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jan Slabon",
|
||||||
|
"email": "jan.slabon@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maximilian Kresse",
|
||||||
|
"email": "maximilian.kresse@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||||
|
"homepage": "https://www.setasign.com/fpdi",
|
||||||
|
"keywords": [
|
||||||
|
"fpdf",
|
||||||
|
"fpdi",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||||
|
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-05T09:57:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/deprecation-contracts",
|
"name": "symfony/deprecation-contracts",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
|
|||||||
942
src/Admin/Dashboard.php
Normal file
942
src/Admin/Dashboard.php
Normal file
@@ -0,0 +1,942 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin Dashboard page.
|
||||||
|
*
|
||||||
|
* Displays comprehensive dashboard with statistics, charts, and quick actions.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Admin;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard class.
|
||||||
|
*/
|
||||||
|
final class Dashboard {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for dashboard stats.
|
||||||
|
*/
|
||||||
|
private const CACHE_KEY = 'wp_bnb_dashboard_stats';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache expiry in seconds (1 hour).
|
||||||
|
*/
|
||||||
|
private const CACHE_EXPIRY = HOUR_IN_SECONDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the dashboard page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render(): void {
|
||||||
|
$license_valid = LicenseManager::is_license_valid();
|
||||||
|
$is_localhost = LicenseManager::is_localhost();
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
||||||
|
|
||||||
|
<?php self::render_notices( $license_valid, $is_localhost ); ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-dashboard-grid">
|
||||||
|
<!-- Row 1: Stats Cards -->
|
||||||
|
<div class="wp-bnb-dashboard-row wp-bnb-stats-row">
|
||||||
|
<?php self::render_occupancy_card(); ?>
|
||||||
|
<?php self::render_revenue_card(); ?>
|
||||||
|
<?php self::render_bookings_card(); ?>
|
||||||
|
<?php self::render_guests_card(); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Charts -->
|
||||||
|
<div class="wp-bnb-dashboard-row wp-bnb-charts-row">
|
||||||
|
<?php self::render_occupancy_chart(); ?>
|
||||||
|
<?php self::render_revenue_chart(); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Activity and Quick Actions -->
|
||||||
|
<div class="wp-bnb-dashboard-row wp-bnb-activity-row">
|
||||||
|
<?php self::render_today_activity(); ?>
|
||||||
|
<?php self::render_upcoming_bookings(); ?>
|
||||||
|
<?php self::render_quick_actions(); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render admin notices.
|
||||||
|
*
|
||||||
|
* @param bool $license_valid Whether license is valid.
|
||||||
|
* @param bool $is_localhost Whether running on localhost.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_notices( bool $license_valid, bool $is_localhost ): void {
|
||||||
|
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">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Link to settings page */
|
||||||
|
esc_html__( 'Your license is not active. Please %s to unlock all features.', 'wp-bnb' ),
|
||||||
|
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ) . '">' . esc_html__( 'activate your license', 'wp-bnb' ) . '</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render occupancy stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_occupancy_card(): void {
|
||||||
|
$stats = self::get_occupancy_stats();
|
||||||
|
$rate = $stats['rate'];
|
||||||
|
$occupied = $stats['occupied'];
|
||||||
|
$total = $stats['total'];
|
||||||
|
$previous_rate = $stats['previous_rate'];
|
||||||
|
$change = $rate - $previous_rate;
|
||||||
|
$change_class = $change >= 0 ? 'positive' : 'negative';
|
||||||
|
$change_icon = $change >= 0 ? 'arrow-up-alt' : 'arrow-down-alt';
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Current Occupancy', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( number_format( $rate, 1 ) ); ?>%</div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: Number of occupied rooms, 2: Total rooms */
|
||||||
|
esc_html__( '%1$d of %2$d rooms', 'wp-bnb' ),
|
||||||
|
$occupied,
|
||||||
|
$total
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php if ( $previous_rate > 0 ) : ?>
|
||||||
|
<div class="wp-bnb-stat-change <?php echo esc_attr( $change_class ); ?>">
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $change_icon ); ?>"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Percentage change */
|
||||||
|
esc_html__( '%s%% vs last month', 'wp-bnb' ),
|
||||||
|
number_format( abs( $change ), 1 )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render revenue stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_revenue_card(): void {
|
||||||
|
$stats = self::get_revenue_stats();
|
||||||
|
$this_month = $stats['this_month'];
|
||||||
|
$last_month = $stats['last_month'];
|
||||||
|
$change = $last_month > 0 ? ( ( $this_month - $last_month ) / $last_month ) * 100 : 0;
|
||||||
|
$change_class = $change >= 0 ? 'positive' : 'negative';
|
||||||
|
$change_icon = $change >= 0 ? 'arrow-up-alt' : 'arrow-down-alt';
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon revenue">
|
||||||
|
<span class="dashicons dashicons-chart-area"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Revenue This Month', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( Calculator::formatPrice( $this_month ) ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Year-to-date revenue */
|
||||||
|
esc_html__( 'YTD: %s', 'wp-bnb' ),
|
||||||
|
Calculator::formatPrice( $stats['ytd'] )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php if ( $last_month > 0 ) : ?>
|
||||||
|
<div class="wp-bnb-stat-change <?php echo esc_attr( $change_class ); ?>">
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $change_icon ); ?>"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Percentage change */
|
||||||
|
esc_html__( '%s%% vs last month', 'wp-bnb' ),
|
||||||
|
number_format( abs( $change ), 1 )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render bookings stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_bookings_card(): void {
|
||||||
|
$stats = self::get_booking_stats();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon bookings">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Bookings This Month', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( $stats['this_month'] ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: Pending count, 2: Confirmed count */
|
||||||
|
esc_html__( '%1$d pending, %2$d confirmed', 'wp-bnb' ),
|
||||||
|
$stats['pending'],
|
||||||
|
$stats['confirmed']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render guests stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_guests_card(): void {
|
||||||
|
$stats = self::get_guest_stats();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon guests">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Total Guests', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( $stats['total'] ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: New guests this month, 2: Repeat guests count */
|
||||||
|
esc_html__( '%1$d new this month, %2$d repeat', 'wp-bnb' ),
|
||||||
|
$stats['new_this_month'],
|
||||||
|
$stats['repeat']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render occupancy trend chart.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_occupancy_chart(): void {
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget wp-bnb-chart-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Occupancy Trend (Last 30 Days)', 'wp-bnb' ); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<canvas id="wp-bnb-occupancy-chart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render revenue trend chart.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_revenue_chart(): void {
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget wp-bnb-chart-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Revenue Trend (Last 6 Months)', 'wp-bnb' ); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<canvas id="wp-bnb-revenue-chart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render today's activity widget.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_today_activity(): void {
|
||||||
|
$checkins = Availability::get_todays_checkins();
|
||||||
|
$checkouts = Availability::get_todays_checkouts();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( "Today's Activity", 'wp-bnb' ); ?></h3>
|
||||||
|
<span class="wp-bnb-widget-date"><?php echo esc_html( wp_date( get_option( 'date_format' ) ) ); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<?php if ( empty( $checkins ) && empty( $checkouts ) ) : ?>
|
||||||
|
<div class="wp-bnb-empty-state">
|
||||||
|
<span class="dashicons dashicons-calendar"></span>
|
||||||
|
<p><?php esc_html_e( 'No check-ins or check-outs scheduled for today.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php if ( ! empty( $checkins ) ) : ?>
|
||||||
|
<div class="wp-bnb-activity-section">
|
||||||
|
<h4>
|
||||||
|
<span class="dashicons dashicons-migrate"></span>
|
||||||
|
<?php esc_html_e( 'Check-ins', 'wp-bnb' ); ?>
|
||||||
|
<span class="count"><?php echo count( $checkins ); ?></span>
|
||||||
|
</h4>
|
||||||
|
<ul class="wp-bnb-activity-list">
|
||||||
|
<?php foreach ( $checkins as $booking ) : ?>
|
||||||
|
<?php
|
||||||
|
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
|
||||||
|
<strong><?php echo esc_html( $guest_name ); ?></strong>
|
||||||
|
<?php if ( $room ) : ?>
|
||||||
|
<span class="room"><?php echo esc_html( $room->post_title ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $checkouts ) ) : ?>
|
||||||
|
<div class="wp-bnb-activity-section">
|
||||||
|
<h4>
|
||||||
|
<span class="dashicons dashicons-external"></span>
|
||||||
|
<?php esc_html_e( 'Check-outs', 'wp-bnb' ); ?>
|
||||||
|
<span class="count"><?php echo count( $checkouts ); ?></span>
|
||||||
|
</h4>
|
||||||
|
<ul class="wp-bnb-activity-list">
|
||||||
|
<?php foreach ( $checkouts as $booking ) : ?>
|
||||||
|
<?php
|
||||||
|
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
|
||||||
|
<strong><?php echo esc_html( $guest_name ); ?></strong>
|
||||||
|
<?php if ( $room ) : ?>
|
||||||
|
<span class="room"><?php echo esc_html( $room->post_title ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render upcoming bookings widget.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_upcoming_bookings(): void {
|
||||||
|
$bookings = self::get_upcoming_bookings( 7 );
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Upcoming Bookings', 'wp-bnb' ); ?></h3>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=' . Booking::POST_TYPE ) ); ?>" class="wp-bnb-view-all">
|
||||||
|
<?php esc_html_e( 'View All', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<?php if ( empty( $bookings ) ) : ?>
|
||||||
|
<div class="wp-bnb-empty-state">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<p><?php esc_html_e( 'No upcoming bookings in the next 7 days.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<table class="wp-bnb-upcoming-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Guest', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $bookings as $booking ) : ?>
|
||||||
|
<?php
|
||||||
|
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
$statuses = Booking::get_booking_statuses();
|
||||||
|
$colors = Booking::get_status_colors();
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
|
||||||
|
<?php echo esc_html( $guest_name ); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><?php echo $room ? esc_html( $room->post_title ) : '—'; ?></td>
|
||||||
|
<td><?php echo esc_html( wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) ); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="wp-bnb-status-badge" style="background-color: <?php echo esc_attr( $colors[ $status ] ?? '#666' ); ?>">
|
||||||
|
<?php echo esc_html( $statuses[ $status ] ?? $status ); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render quick actions widget.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_quick_actions(): void {
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget wp-bnb-quick-actions">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Quick Actions', 'wp-bnb' ); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<div class="wp-bnb-actions-grid">
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Booking::POST_TYPE ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-plus-alt"></span>
|
||||||
|
<span><?php esc_html_e( 'New Booking', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Guest::POST_TYPE ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-admin-users"></span>
|
||||||
|
<span><?php esc_html_e( 'New Guest', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-calendar' ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<span><?php esc_html_e( 'View Calendar', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-reports' ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-analytics"></span>
|
||||||
|
<span><?php esc_html_e( 'View Reports', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get occupancy statistics.
|
||||||
|
*
|
||||||
|
* @return array{rate: float, occupied: int, total: int, previous_rate: float}
|
||||||
|
*/
|
||||||
|
public static function get_occupancy_stats(): array {
|
||||||
|
// Get total rooms.
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_rooms = count( $rooms );
|
||||||
|
|
||||||
|
// Get currently occupied rooms.
|
||||||
|
$current_bookings = Availability::get_current_bookings();
|
||||||
|
$occupied_rooms = count( $current_bookings );
|
||||||
|
|
||||||
|
$rate = $total_rooms > 0 ? ( $occupied_rooms / $total_rooms ) * 100 : 0;
|
||||||
|
|
||||||
|
// Calculate last month's average occupancy.
|
||||||
|
$previous_rate = self::get_average_occupancy_for_month(
|
||||||
|
(int) gmdate( 'Y', strtotime( '-1 month' ) ),
|
||||||
|
(int) gmdate( 'n', strtotime( '-1 month' ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'rate' => $rate,
|
||||||
|
'occupied' => $occupied_rooms,
|
||||||
|
'total' => $total_rooms,
|
||||||
|
'previous_rate' => $previous_rate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average occupancy rate for a specific month.
|
||||||
|
*
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month.
|
||||||
|
* @return float Average occupancy percentage.
|
||||||
|
*/
|
||||||
|
private static function get_average_occupancy_for_month( int $year, int $month ): float {
|
||||||
|
$cache_key = self::CACHE_KEY . "_occupancy_{$year}_{$month}";
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return (float) $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_rooms = count( $rooms );
|
||||||
|
|
||||||
|
if ( $total_rooms === 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$days_in_month = (int) gmdate( 't', mktime( 0, 0, 0, $month, 1, $year ) );
|
||||||
|
$total_room_nights = $total_rooms * $days_in_month;
|
||||||
|
$booked_nights = 0;
|
||||||
|
|
||||||
|
$month_start = sprintf( '%04d-%02d-01', $year, $month );
|
||||||
|
$month_end = sprintf( '%04d-%02d-%02d', $year, $month, $days_in_month );
|
||||||
|
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $month_end,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $month_start,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
|
||||||
|
// Clamp to month boundaries.
|
||||||
|
$start = max( $check_in, $month_start );
|
||||||
|
$end = min( $check_out, gmdate( 'Y-m-d', strtotime( $month_end . ' +1 day' ) ) );
|
||||||
|
|
||||||
|
$nights = Booking::calculate_nights( $start, $end );
|
||||||
|
$booked_nights += $nights;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rate = ( $booked_nights / $total_room_nights ) * 100;
|
||||||
|
|
||||||
|
set_transient( $cache_key, $rate, self::CACHE_EXPIRY );
|
||||||
|
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue statistics.
|
||||||
|
*
|
||||||
|
* @return array{this_month: float, last_month: float, ytd: float}
|
||||||
|
*/
|
||||||
|
public static function get_revenue_stats(): array {
|
||||||
|
$this_month = self::get_revenue_for_period(
|
||||||
|
gmdate( 'Y-m-01' ),
|
||||||
|
gmdate( 'Y-m-t' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$last_month = self::get_revenue_for_period(
|
||||||
|
gmdate( 'Y-m-01', strtotime( '-1 month' ) ),
|
||||||
|
gmdate( 'Y-m-t', strtotime( '-1 month' ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
$ytd = self::get_revenue_for_period(
|
||||||
|
gmdate( 'Y-01-01' ),
|
||||||
|
gmdate( 'Y-m-d' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'this_month' => $this_month,
|
||||||
|
'last_month' => $last_month,
|
||||||
|
'ytd' => $ytd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue for a specific period.
|
||||||
|
*
|
||||||
|
* @param string $start_date Start date (Y-m-d).
|
||||||
|
* @param string $end_date End date (Y-m-d).
|
||||||
|
* @return float Total revenue.
|
||||||
|
*/
|
||||||
|
public static function get_revenue_for_period( string $start_date, string $end_date ): float {
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $start_date,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $end_date,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$total = 0.0;
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
|
$total += (float) $price;
|
||||||
|
|
||||||
|
// Add services total.
|
||||||
|
$services_total = Booking::calculate_booking_services_total( $booking->ID );
|
||||||
|
$total += $services_total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get booking statistics.
|
||||||
|
*
|
||||||
|
* @return array{this_month: int, pending: int, confirmed: int}
|
||||||
|
*/
|
||||||
|
public static function get_booking_stats(): array {
|
||||||
|
$month_start = gmdate( 'Y-m-01' );
|
||||||
|
$month_end = gmdate( 'Y-m-t' );
|
||||||
|
|
||||||
|
// Bookings created this month.
|
||||||
|
$this_month = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'date_query' => array(
|
||||||
|
array(
|
||||||
|
'after' => $month_start,
|
||||||
|
'before' => $month_end,
|
||||||
|
'inclusive' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pending bookings.
|
||||||
|
$pending = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'pending',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Confirmed bookings.
|
||||||
|
$confirmed = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'confirmed',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'this_month' => count( $this_month ),
|
||||||
|
'pending' => count( $pending ),
|
||||||
|
'confirmed' => count( $confirmed ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest statistics.
|
||||||
|
*
|
||||||
|
* @return array{total: int, new_this_month: int, repeat: int}
|
||||||
|
*/
|
||||||
|
public static function get_guest_stats(): array {
|
||||||
|
$month_start = gmdate( 'Y-m-01' );
|
||||||
|
$month_end = gmdate( 'Y-m-t' );
|
||||||
|
|
||||||
|
// Total guests.
|
||||||
|
$total = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// New guests this month.
|
||||||
|
$new_this_month = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'date_query' => array(
|
||||||
|
array(
|
||||||
|
'after' => $month_start,
|
||||||
|
'before' => $month_end,
|
||||||
|
'inclusive' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Repeat guests (2+ bookings).
|
||||||
|
$all_guests = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$repeat = 0;
|
||||||
|
foreach ( $all_guests as $guest ) {
|
||||||
|
$booking_count = Guest::get_booking_count( $guest->ID );
|
||||||
|
if ( $booking_count >= 2 ) {
|
||||||
|
++$repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'total' => count( $total ),
|
||||||
|
'new_this_month' => count( $new_this_month ),
|
||||||
|
'repeat' => $repeat,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upcoming bookings.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to look ahead.
|
||||||
|
* @return array<\WP_Post> Array of booking posts.
|
||||||
|
*/
|
||||||
|
private static function get_upcoming_bookings( int $days = 7 ): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'pending', 'confirmed' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $end_date,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_check_in',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get occupancy trend data for charts.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to include.
|
||||||
|
* @return array{labels: array<string>, data: array<float>}
|
||||||
|
*/
|
||||||
|
public static function get_occupancy_trend_data( int $days = 30 ): array {
|
||||||
|
$labels = array();
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_rooms = count( $rooms );
|
||||||
|
|
||||||
|
if ( $total_rooms === 0 ) {
|
||||||
|
return array(
|
||||||
|
'labels' => array(),
|
||||||
|
'data' => array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( $i = $days - 1; $i >= 0; $i-- ) {
|
||||||
|
$date = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
|
||||||
|
$labels[] = wp_date( 'M j', strtotime( $date ) );
|
||||||
|
|
||||||
|
// Count bookings active on this date.
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $date,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $date,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rate = ( count( $bookings ) / $total_rooms ) * 100;
|
||||||
|
$data[] = round( $rate, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue trend data for charts.
|
||||||
|
*
|
||||||
|
* @param int $months Number of months to include.
|
||||||
|
* @return array{labels: array<string>, data: array<float>}
|
||||||
|
*/
|
||||||
|
public static function get_revenue_trend_data( int $months = 6 ): array {
|
||||||
|
$labels = array();
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
for ( $i = $months - 1; $i >= 0; $i-- ) {
|
||||||
|
$month_start = gmdate( 'Y-m-01', strtotime( "-{$i} months" ) );
|
||||||
|
$month_end = gmdate( 'Y-m-t', strtotime( "-{$i} months" ) );
|
||||||
|
$month_name = gmdate( 'M Y', strtotime( $month_start ) );
|
||||||
|
|
||||||
|
$labels[] = $month_name;
|
||||||
|
$data[] = self::get_revenue_for_period( $month_start, $month_end );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1368
src/Admin/Reports.php
Normal file
1368
src/Admin/Reports.php
Normal file
File diff suppressed because it is too large
Load Diff
382
src/Api/Controllers/AbstractController.php
Normal file
382
src/Api/Controllers/AbstractController.php
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Abstract REST Controller
|
||||||
|
*
|
||||||
|
* Base class for all REST API controllers with common functionality.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Api\RestApi;
|
||||||
|
use Magdev\WpBnb\Api\RateLimiter;
|
||||||
|
use Magdev\WpBnb\Api\ResponseFormatter;
|
||||||
|
use WP_REST_Controller;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Controller class.
|
||||||
|
*/
|
||||||
|
abstract class AbstractController extends WP_REST_Controller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API namespace.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $namespace = RestApi::NAMESPACE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiter instance.
|
||||||
|
*
|
||||||
|
* @var RateLimiter
|
||||||
|
*/
|
||||||
|
protected RateLimiter $rate_limiter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response formatter instance.
|
||||||
|
*
|
||||||
|
* @var ResponseFormatter
|
||||||
|
*/
|
||||||
|
protected ResponseFormatter $formatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->rate_limiter = new RateLimiter();
|
||||||
|
$this->formatter = new ResponseFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check rate limit before processing request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_Error|null Error if rate limited, null otherwise.
|
||||||
|
*/
|
||||||
|
protected function check_rate_limit( WP_REST_Request $request ): ?WP_Error {
|
||||||
|
// Skip rate limiting if disabled.
|
||||||
|
if ( 'yes' !== get_option( 'wp_bnb_api_rate_limiting', 'yes' ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $this->get_client_identifier( $request );
|
||||||
|
$endpoint = $request->get_route();
|
||||||
|
|
||||||
|
if ( ! $this->rate_limiter->check( $identifier, $endpoint ) ) {
|
||||||
|
return $this->formatter->rate_limit_error(
|
||||||
|
$this->rate_limiter->get_retry_after( $identifier, $endpoint )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add rate limit headers to response.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Response $response Current response.
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response Response with headers.
|
||||||
|
*/
|
||||||
|
protected function add_rate_limit_headers( WP_REST_Response $response, WP_REST_Request $request ): WP_REST_Response {
|
||||||
|
if ( 'yes' !== get_option( 'wp_bnb_api_rate_limiting', 'yes' ) ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $this->get_client_identifier( $request );
|
||||||
|
$endpoint = $request->get_route();
|
||||||
|
$info = $this->rate_limiter->get_rate_limit_info( $identifier, $endpoint );
|
||||||
|
|
||||||
|
$response->header( 'X-RateLimit-Limit', (string) $info['limit'] );
|
||||||
|
$response->header( 'X-RateLimit-Remaining', (string) $info['remaining'] );
|
||||||
|
$response->header( 'X-RateLimit-Reset', (string) $info['reset'] );
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client identifier for rate limiting.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return string Client identifier.
|
||||||
|
*/
|
||||||
|
protected function get_client_identifier( WP_REST_Request $request ): string {
|
||||||
|
// Use user ID if authenticated.
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
if ( $user_id > 0 ) {
|
||||||
|
return 'user_' . $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ip_' . $this->get_client_ip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP address.
|
||||||
|
*
|
||||||
|
* Supports proxies and Cloudflare.
|
||||||
|
*
|
||||||
|
* @return string Client IP address.
|
||||||
|
*/
|
||||||
|
protected function get_client_ip(): string {
|
||||||
|
$headers = array(
|
||||||
|
'HTTP_CF_CONNECTING_IP', // Cloudflare.
|
||||||
|
'HTTP_X_FORWARDED_FOR', // Proxy.
|
||||||
|
'HTTP_X_REAL_IP', // Nginx.
|
||||||
|
'REMOTE_ADDR',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $headers as $header ) {
|
||||||
|
if ( ! empty( $_SERVER[ $header ] ) ) {
|
||||||
|
$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
|
||||||
|
// Handle comma-separated list (X-Forwarded-For).
|
||||||
|
if ( str_contains( $ip, ',' ) ) {
|
||||||
|
$ip = trim( explode( ',', $ip )[0] );
|
||||||
|
}
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate date format (Y-m-d).
|
||||||
|
*
|
||||||
|
* @param string $date Date string.
|
||||||
|
* @return bool True if valid.
|
||||||
|
*/
|
||||||
|
protected function validate_date( string $date ): bool {
|
||||||
|
$d = \DateTimeImmutable::createFromFormat( 'Y-m-d', $date );
|
||||||
|
return $d && $d->format( 'Y-m-d' ) === $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate date is not in the past.
|
||||||
|
*
|
||||||
|
* @param string $date Date string (Y-m-d).
|
||||||
|
* @return bool True if date is today or future.
|
||||||
|
*/
|
||||||
|
protected function validate_future_date( string $date ): bool {
|
||||||
|
if ( ! $this->validate_date( $date ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$date_obj = \DateTimeImmutable::createFromFormat( 'Y-m-d', $date );
|
||||||
|
$today = new \DateTimeImmutable( 'today' );
|
||||||
|
return $date_obj >= $today;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for public endpoints.
|
||||||
|
*
|
||||||
|
* @return bool Always true.
|
||||||
|
*/
|
||||||
|
public function public_permission(): bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for authenticated endpoints.
|
||||||
|
*
|
||||||
|
* @return bool True if logged in.
|
||||||
|
*/
|
||||||
|
public function authenticated_permission(): bool {
|
||||||
|
return is_user_logged_in();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for admin endpoints.
|
||||||
|
*
|
||||||
|
* @return bool True if user can edit posts.
|
||||||
|
*/
|
||||||
|
public function admin_permission(): bool {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for managing bookings.
|
||||||
|
*
|
||||||
|
* @return bool True if user can edit posts.
|
||||||
|
*/
|
||||||
|
public function manage_bookings_permission(): bool {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pagination parameters from request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return array{page: int, per_page: int, offset: int}
|
||||||
|
*/
|
||||||
|
protected function get_pagination_params( WP_REST_Request $request ): array {
|
||||||
|
$page = max( 1, (int) $request->get_param( 'page' ) ?: 1 );
|
||||||
|
$per_page = min( 100, max( 1, (int) $request->get_param( 'per_page' ) ?: 10 ) );
|
||||||
|
$offset = ( $page - 1 ) * $per_page;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
'offset' => $offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sorting parameters from request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @param array $allowed_orderby Allowed orderby values.
|
||||||
|
* @param string $default_orderby Default orderby value.
|
||||||
|
* @return array{orderby: string, order: string}
|
||||||
|
*/
|
||||||
|
protected function get_sorting_params( WP_REST_Request $request, array $allowed_orderby = array( 'title', 'date' ), string $default_orderby = 'title' ): array {
|
||||||
|
$orderby = $request->get_param( 'orderby' ) ?: $default_orderby;
|
||||||
|
$order = strtoupper( $request->get_param( 'order' ) ?: 'ASC' );
|
||||||
|
|
||||||
|
// Validate orderby.
|
||||||
|
if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
|
||||||
|
$orderby = $default_orderby;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate order.
|
||||||
|
if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
|
||||||
|
$order = 'ASC';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'orderby' => $orderby,
|
||||||
|
'order' => $order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format post for API response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return array Basic post data.
|
||||||
|
*/
|
||||||
|
protected function format_post_base( \WP_Post $post ): array {
|
||||||
|
return array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'title' => get_the_title( $post ),
|
||||||
|
'slug' => $post->post_name,
|
||||||
|
'excerpt' => get_the_excerpt( $post ),
|
||||||
|
'content' => apply_filters( 'the_content', $post->post_content ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format featured image for API response.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return array|null Image data or null.
|
||||||
|
*/
|
||||||
|
protected function format_featured_image( int $post_id ): ?array {
|
||||||
|
$thumbnail_id = get_post_thumbnail_id( $post_id );
|
||||||
|
if ( ! $thumbnail_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->format_image( $thumbnail_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format image attachment for API response.
|
||||||
|
*
|
||||||
|
* @param int $attachment_id Attachment ID.
|
||||||
|
* @return array|null Image data or null.
|
||||||
|
*/
|
||||||
|
protected function format_image( int $attachment_id ): ?array {
|
||||||
|
$full = wp_get_attachment_image_src( $attachment_id, 'full' );
|
||||||
|
if ( ! $full ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sizes = array();
|
||||||
|
foreach ( array( 'thumbnail', 'medium', 'large' ) as $size ) {
|
||||||
|
$src = wp_get_attachment_image_src( $attachment_id, $size );
|
||||||
|
if ( $src ) {
|
||||||
|
$sizes[ $size ] = array(
|
||||||
|
'url' => $src[0],
|
||||||
|
'width' => $src[1],
|
||||||
|
'height' => $src[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $attachment_id,
|
||||||
|
'url' => $full[0],
|
||||||
|
'width' => $full[1],
|
||||||
|
'height' => $full[2],
|
||||||
|
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
|
||||||
|
'sizes' => $sizes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add HATEOAS links to response item.
|
||||||
|
*
|
||||||
|
* @param array $item Response item.
|
||||||
|
* @param string $route Base route for self link.
|
||||||
|
* @param int $id Item ID.
|
||||||
|
* @return array Item with _links.
|
||||||
|
*/
|
||||||
|
protected function add_links( array $item, string $route, int $id ): array {
|
||||||
|
$item['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array(
|
||||||
|
'href' => rest_url( $this->namespace . '/' . $route . '/' . $id ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common collection parameters for schema.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
public function get_collection_params(): array {
|
||||||
|
return array(
|
||||||
|
'page' => array(
|
||||||
|
'description' => __( 'Current page of the collection.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'per_page' => array(
|
||||||
|
'description' => __( 'Maximum number of items to be returned per page.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 10,
|
||||||
|
'minimum' => 1,
|
||||||
|
'maximum' => 100,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'search' => array(
|
||||||
|
'description' => __( 'Limit results to those matching a string.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'orderby' => array(
|
||||||
|
'description' => __( 'Sort collection by attribute.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'title',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'order' => array(
|
||||||
|
'description' => __( 'Order sort attribute ascending or descending.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'asc',
|
||||||
|
'enum' => array( 'asc', 'desc' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
930
src/Api/Controllers/BookingsController.php
Normal file
930
src/Api/Controllers/BookingsController.php
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bookings REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for bookings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bookings Controller class.
|
||||||
|
*/
|
||||||
|
final class BookingsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'bookings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /bookings - List bookings (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_items' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => $this->get_bookings_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings - Create booking (public).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'create_item' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => $this->get_create_booking_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /bookings/{id} - Get single booking.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_item' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// PATCH /bookings/{id} - Update booking (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => array( $this, 'update_item' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => $this->get_update_booking_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /bookings/{id} - Cancel booking (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::DELETABLE,
|
||||||
|
'callback' => array( $this, 'delete_item' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings/{id}/confirm - Confirm booking.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/confirm',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'confirm_booking' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings/{id}/check-in - Check in guest.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/check-in',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'check_in_booking' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings/{id}/check-out - Check out guest.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/check-out',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'check_out_booking' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of bookings.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagination = $this->get_pagination_params( $request );
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $pagination['per_page'],
|
||||||
|
'offset' => $pagination['offset'],
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$meta_query = array();
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room filter.
|
||||||
|
$room_id = $request->get_param( 'room_id' );
|
||||||
|
if ( $room_id ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guest filter.
|
||||||
|
$guest_id = $request->get_param( 'guest_id' );
|
||||||
|
if ( $guest_id ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_guest_id',
|
||||||
|
'value' => $guest_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range filter.
|
||||||
|
$date_from = $request->get_param( 'date_from' );
|
||||||
|
if ( $date_from && $this->validate_date( $date_from ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $date_from,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$date_to = $request->get_param( 'date_to' );
|
||||||
|
if ( $date_to && $this->validate_date( $date_to ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $date_to,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta_query ) ) {
|
||||||
|
$meta_query['relation'] = 'AND';
|
||||||
|
$args['meta_query'] = $meta_query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_booking_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function create_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'room_id' );
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
$guest = $request->get_param( 'guest' );
|
||||||
|
|
||||||
|
// Validate room.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability.
|
||||||
|
if ( ! Availability::is_available( $room_id, $check_in, $check_out ) ) {
|
||||||
|
return $this->formatter->conflict(
|
||||||
|
__( 'Room is not available for the selected dates.', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate guest info.
|
||||||
|
if ( empty( $guest['first_name'] ) || empty( $guest['last_name'] ) || empty( $guest['email'] ) ) {
|
||||||
|
return $this->formatter->validation_error( 'guest', __( 'Guest first name, last name, and email are required.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_email( $guest['email'] ) ) {
|
||||||
|
return $this->formatter->validation_error( 'guest.email', __( 'Invalid email address.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create guest.
|
||||||
|
$guest_id = $this->find_or_create_guest( $guest );
|
||||||
|
|
||||||
|
// Calculate price.
|
||||||
|
$price = Calculator::calculate( $room_id, $check_in, $check_out );
|
||||||
|
$services = $request->get_param( 'services' ) ?? array();
|
||||||
|
$room_price = $price['price'] ?? 0;
|
||||||
|
|
||||||
|
// Calculate services total.
|
||||||
|
$services_total = 0;
|
||||||
|
$services_data = array();
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
|
||||||
|
foreach ( $services as $service_item ) {
|
||||||
|
$service_id = $service_item['service_id'] ?? 0;
|
||||||
|
$quantity = $service_item['quantity'] ?? 1;
|
||||||
|
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
if ( $service_data ) {
|
||||||
|
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
$services_total += $service_price;
|
||||||
|
$services_data[] = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $service_price,
|
||||||
|
'pricing_type' => $service_data['pricing_type'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_price = $room_price + $services_total;
|
||||||
|
|
||||||
|
// Generate reference.
|
||||||
|
$reference = Booking::generate_reference();
|
||||||
|
|
||||||
|
// Create booking post.
|
||||||
|
$guest_name = trim( $guest['first_name'] . ' ' . $guest['last_name'] );
|
||||||
|
$post_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $guest_name . ' (' . $check_in . ' - ' . $check_out . ')',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $post_id ) ) {
|
||||||
|
return $this->formatter->server_error( __( 'Failed to create booking.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save meta.
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_room_id', $room_id );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_id', $guest_id );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_name', $guest_name );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_email', sanitize_email( $guest['email'] ) );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_phone', sanitize_text_field( $guest['phone'] ?? '' ) );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_check_in', $check_in );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_check_out', $check_out );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_status', 'pending' );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_adults', absint( $request->get_param( 'guests_count' ) ?? 1 ) );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_calculated_price', $room_price );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_total_price', $total_price );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_reference', $reference );
|
||||||
|
|
||||||
|
if ( ! empty( $services_data ) ) {
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_services', wp_json_encode( $services_data ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$notes = $request->get_param( 'notes' );
|
||||||
|
if ( $notes ) {
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_notes', sanitize_textarea_field( $notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification email.
|
||||||
|
if ( class_exists( EmailNotifier::class ) ) {
|
||||||
|
EmailNotifier::send_admin_notification( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response.
|
||||||
|
$booking = get_post( $post_id );
|
||||||
|
$data = $this->prepare_booking_response( $booking, true );
|
||||||
|
$location = rest_url( $this->namespace . '/bookings/' . $post_id );
|
||||||
|
|
||||||
|
$response = $this->formatter->created( $data, $location );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_booking_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function update_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status if provided.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$current_status = get_post_meta( $id, '_bnb_booking_status', true );
|
||||||
|
if ( ! Booking::can_transition_to( $current_status, $status ) ) {
|
||||||
|
return $this->formatter->validation_error(
|
||||||
|
'status',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %1$s: current status, %2$s: target status */
|
||||||
|
__( 'Cannot transition from %1$s to %2$s.', 'wp-bnb' ),
|
||||||
|
$current_status,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
update_post_meta( $id, '_bnb_booking_status', $status );
|
||||||
|
|
||||||
|
if ( 'confirmed' === $status ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update notes if provided.
|
||||||
|
$notes = $request->get_param( 'notes' );
|
||||||
|
if ( null !== $notes ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_notes', sanitize_textarea_field( $notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update guest notes if provided.
|
||||||
|
$guest_notes = $request->get_param( 'guest_notes' );
|
||||||
|
if ( null !== $guest_notes ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_guest_notes', sanitize_textarea_field( $guest_notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking = get_post( $id );
|
||||||
|
$data = $this->prepare_booking_response( $booking, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function delete_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the booking (don't delete).
|
||||||
|
update_post_meta( $id, '_bnb_booking_status', 'cancelled' );
|
||||||
|
|
||||||
|
// Send cancellation email.
|
||||||
|
if ( class_exists( EmailNotifier::class ) ) {
|
||||||
|
EmailNotifier::send_cancellation_email( $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->formatter->no_content();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function confirm_booking( $request ) {
|
||||||
|
return $this->transition_status( $request, 'confirmed' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check in a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function check_in_booking( $request ) {
|
||||||
|
return $this->transition_status( $request, 'checked_in' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check out a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function check_out_booking( $request ) {
|
||||||
|
return $this->transition_status( $request, 'checked_out' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition booking status.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @param string $new_status Target status.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
private function transition_status( WP_REST_Request $request, string $new_status ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_status = get_post_meta( $id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
if ( ! Booking::can_transition_to( $current_status, $new_status ) ) {
|
||||||
|
return $this->formatter->validation_error(
|
||||||
|
'status',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %1$s: current status, %2$s: target status */
|
||||||
|
__( 'Cannot transition from %1$s to %2$s.', 'wp-bnb' ),
|
||||||
|
$current_status,
|
||||||
|
$new_status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta( $id, '_bnb_booking_status', $new_status );
|
||||||
|
|
||||||
|
if ( 'confirmed' === $new_status ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||||
|
if ( class_exists( EmailNotifier::class ) ) {
|
||||||
|
EmailNotifier::send_confirmation_email( $id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking = get_post( $id );
|
||||||
|
$data = $this->prepare_booking_response( $booking, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find or create a guest from booking data.
|
||||||
|
*
|
||||||
|
* @param array $guest_data Guest data.
|
||||||
|
* @return int Guest post ID.
|
||||||
|
*/
|
||||||
|
private function find_or_create_guest( array $guest_data ): int {
|
||||||
|
$email = sanitize_email( $guest_data['email'] );
|
||||||
|
|
||||||
|
// Check if guest exists.
|
||||||
|
$existing = Guest::get_by_email( $email );
|
||||||
|
if ( $existing ) {
|
||||||
|
return $existing->ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new guest.
|
||||||
|
$guest_name = trim( $guest_data['first_name'] . ' ' . $guest_data['last_name'] );
|
||||||
|
$guest_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $guest_name,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $guest_id ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save guest meta.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_first_name', sanitize_text_field( $guest_data['first_name'] ) );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_last_name', sanitize_text_field( $guest_data['last_name'] ) );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_email', $email );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
|
||||||
|
|
||||||
|
if ( ! empty( $guest_data['phone'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_phone', sanitize_text_field( $guest_data['phone'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $guest_data['address'] ) ) {
|
||||||
|
$address = $guest_data['address'];
|
||||||
|
if ( ! empty( $address['street'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_street', sanitize_text_field( $address['street'] ) );
|
||||||
|
}
|
||||||
|
if ( ! empty( $address['city'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_city', sanitize_text_field( $address['city'] ) );
|
||||||
|
}
|
||||||
|
if ( ! empty( $address['postal_code'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_postal_code', sanitize_text_field( $address['postal_code'] ) );
|
||||||
|
}
|
||||||
|
if ( ! empty( $address['country'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_country', sanitize_text_field( $address['country'] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $guest_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare booking data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Booking post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Booking data.
|
||||||
|
*/
|
||||||
|
private function prepare_booking_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$room_id = get_post_meta( $post->ID, '_bnb_booking_room_id', true );
|
||||||
|
$guest_id = get_post_meta( $post->ID, '_bnb_booking_guest_id', true );
|
||||||
|
$check_in = get_post_meta( $post->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $post->ID, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $post->ID, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$nights = 0;
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = $room_id ? get_post( $room_id ) : null;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'reference' => get_post_meta( $post->ID, '_bnb_booking_reference', true ) ?: $post->post_title,
|
||||||
|
'status' => $status,
|
||||||
|
'room' => $room ? array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
|
||||||
|
) : null,
|
||||||
|
'guest' => array(
|
||||||
|
'id' => (int) $guest_id,
|
||||||
|
'name' => get_post_meta( $post->ID, '_bnb_booking_guest_name', true ),
|
||||||
|
'email' => get_post_meta( $post->ID, '_bnb_booking_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $post->ID, '_bnb_booking_guest_phone', true ),
|
||||||
|
),
|
||||||
|
'dates' => array(
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
),
|
||||||
|
'pricing' => array(
|
||||||
|
'room_total' => (float) get_post_meta( $post->ID, '_bnb_booking_calculated_price', true ),
|
||||||
|
'services_total' => 0,
|
||||||
|
'grand_total' => (float) get_post_meta( $post->ID, '_bnb_booking_total_price', true ),
|
||||||
|
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||||
|
),
|
||||||
|
'created_at' => $post->post_date_gmt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get building info.
|
||||||
|
if ( $room ) {
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
if ( $building ) {
|
||||||
|
$data['building'] = array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => get_the_title( $building ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate services total.
|
||||||
|
$services_json = get_post_meta( $post->ID, '_bnb_booking_services', true );
|
||||||
|
if ( $services_json ) {
|
||||||
|
$services = json_decode( $services_json, true );
|
||||||
|
if ( is_array( $services ) ) {
|
||||||
|
$services_total = 0;
|
||||||
|
$services_list = array();
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$services_total += (float) ( $service['price'] ?? 0 );
|
||||||
|
$service_post = get_post( $service['service_id'] );
|
||||||
|
$services_list[] = array(
|
||||||
|
'id' => $service['service_id'],
|
||||||
|
'name' => $service_post ? get_the_title( $service_post ) : '',
|
||||||
|
'quantity' => $service['quantity'] ?? 1,
|
||||||
|
'price' => (float) ( $service['price'] ?? 0 ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$data['pricing']['services_total'] = $services_total;
|
||||||
|
$data['services'] = $services_list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $full ) {
|
||||||
|
$data['notes'] = get_post_meta( $post->ID, '_bnb_booking_notes', true );
|
||||||
|
$data['guest_notes'] = get_post_meta( $post->ID, '_bnb_booking_guest_notes', true );
|
||||||
|
$data['adults'] = (int) get_post_meta( $post->ID, '_bnb_booking_adults', true );
|
||||||
|
$data['children'] = (int) get_post_meta( $post->ID, '_bnb_booking_children', true );
|
||||||
|
$data['confirmed_at'] = get_post_meta( $post->ID, '_bnb_booking_confirmed_at', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/bookings/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'room' => $room ? array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $room->ID ) ),
|
||||||
|
) : array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bookings collection parameters.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
private function get_bookings_collection_params(): array {
|
||||||
|
$params = $this->get_collection_params();
|
||||||
|
|
||||||
|
$params['status'] = array(
|
||||||
|
'description' => __( 'Filter by booking status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['room_id'] = array(
|
||||||
|
'description' => __( 'Filter by room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['guest_id'] = array(
|
||||||
|
'description' => __( 'Filter by guest ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['date_from'] = array(
|
||||||
|
'description' => __( 'Filter bookings with check-in from this date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['date_to'] = array(
|
||||||
|
'description' => __( 'Filter bookings with check-in until this date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get create booking parameters.
|
||||||
|
*
|
||||||
|
* @return array Create parameters.
|
||||||
|
*/
|
||||||
|
private function get_create_booking_params(): array {
|
||||||
|
return array(
|
||||||
|
'room_id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'guest' => array(
|
||||||
|
'description' => __( 'Guest information.', 'wp-bnb' ),
|
||||||
|
'type' => 'object',
|
||||||
|
'required' => true,
|
||||||
|
'properties' => array(
|
||||||
|
'first_name' => array( 'type' => 'string', 'required' => true ),
|
||||||
|
'last_name' => array( 'type' => 'string', 'required' => true ),
|
||||||
|
'email' => array( 'type' => 'string', 'required' => true, 'format' => 'email' ),
|
||||||
|
'phone' => array( 'type' => 'string' ),
|
||||||
|
'address' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'street' => array( 'type' => 'string' ),
|
||||||
|
'city' => array( 'type' => 'string' ),
|
||||||
|
'postal_code' => array( 'type' => 'string' ),
|
||||||
|
'country' => array( 'type' => 'string' ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'guests_count' => array(
|
||||||
|
'description' => __( 'Number of guests.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'services' => array(
|
||||||
|
'description' => __( 'Additional services.', 'wp-bnb' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'service_id' => array( 'type' => 'integer', 'required' => true ),
|
||||||
|
'quantity' => array( 'type' => 'integer', 'default' => 1 ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'notes' => array(
|
||||||
|
'description' => __( 'Guest notes or special requests.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update booking parameters.
|
||||||
|
*
|
||||||
|
* @return array Update parameters.
|
||||||
|
*/
|
||||||
|
private function get_update_booking_params(): array {
|
||||||
|
return array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'status' => array(
|
||||||
|
'description' => __( 'Booking status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'notes' => array(
|
||||||
|
'description' => __( 'Internal staff notes.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
),
|
||||||
|
'guest_notes' => array(
|
||||||
|
'description' => __( 'Guest notes or special requests.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
323
src/Api/Controllers/BuildingsController.php
Normal file
323
src/Api/Controllers/BuildingsController.php
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Buildings REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for buildings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buildings Controller class.
|
||||||
|
*/
|
||||||
|
final class BuildingsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'buildings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /buildings - List all buildings.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_items' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => $this->get_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /buildings/{id} - Get single building.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_item' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /buildings/{id}/rooms - Get rooms in building.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/rooms',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_building_rooms' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'status' => array(
|
||||||
|
'description' => __( 'Filter by room status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'available', 'occupied', 'maintenance', 'blocked' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of buildings.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagination = $this->get_pagination_params( $request );
|
||||||
|
$sorting = $this->get_sorting_params( $request, array( 'title', 'date' ), 'title' );
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $pagination['per_page'],
|
||||||
|
'offset' => $pagination['offset'],
|
||||||
|
'orderby' => $sorting['orderby'],
|
||||||
|
'order' => $sorting['order'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Search filter.
|
||||||
|
$search = $request->get_param( 'search' );
|
||||||
|
if ( $search ) {
|
||||||
|
$args['s'] = $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_building_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single building.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Building::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Building', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_building_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rooms in a building.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_building_rooms( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$building_id = $request->get_param( 'id' );
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
|
||||||
|
if ( ! $building || Building::POST_TYPE !== $building->post_type || 'publish' !== $building->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Building', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => $building_id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_room_room_number',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by status.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = get_posts( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$items[] = $this->prepare_room_summary( $room );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare building data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Building post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Building data.
|
||||||
|
*/
|
||||||
|
private function prepare_building_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$data = $this->format_post_base( $post );
|
||||||
|
|
||||||
|
// Featured image.
|
||||||
|
$data['featured_image'] = $this->format_featured_image( $post->ID );
|
||||||
|
$data['permalink'] = get_permalink( $post->ID );
|
||||||
|
|
||||||
|
// Address.
|
||||||
|
$data['address'] = array(
|
||||||
|
'street' => get_post_meta( $post->ID, '_bnb_building_street', true ),
|
||||||
|
'street2' => get_post_meta( $post->ID, '_bnb_building_street2', true ),
|
||||||
|
'city' => get_post_meta( $post->ID, '_bnb_building_city', true ),
|
||||||
|
'state' => get_post_meta( $post->ID, '_bnb_building_state', true ),
|
||||||
|
'postal_code' => get_post_meta( $post->ID, '_bnb_building_zip', true ),
|
||||||
|
'country' => get_post_meta( $post->ID, '_bnb_building_country', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Contact.
|
||||||
|
$data['contact'] = array(
|
||||||
|
'phone' => get_post_meta( $post->ID, '_bnb_building_phone', true ),
|
||||||
|
'email' => get_post_meta( $post->ID, '_bnb_building_email', true ),
|
||||||
|
'website' => get_post_meta( $post->ID, '_bnb_building_website', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Details.
|
||||||
|
$data['details'] = array(
|
||||||
|
'rooms_count' => (int) get_post_meta( $post->ID, '_bnb_building_total_rooms', true ),
|
||||||
|
'floors' => (int) get_post_meta( $post->ID, '_bnb_building_floors', true ),
|
||||||
|
'year_built' => (int) get_post_meta( $post->ID, '_bnb_building_year_built', true ),
|
||||||
|
'check_in_time' => get_post_meta( $post->ID, '_bnb_building_check_in_time', true ) ?: '14:00',
|
||||||
|
'check_out_time' => get_post_meta( $post->ID, '_bnb_building_check_out_time', true ) ?: '11:00',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Count actual rooms.
|
||||||
|
$actual_rooms = Room::get_rooms_for_building( $post->ID );
|
||||||
|
$data['details']['actual_rooms_count'] = count( $actual_rooms );
|
||||||
|
|
||||||
|
// Full address formatted.
|
||||||
|
if ( $full ) {
|
||||||
|
$data['address']['formatted'] = Building::get_formatted_address( $post->ID );
|
||||||
|
|
||||||
|
// Country name.
|
||||||
|
$countries = Building::get_countries();
|
||||||
|
$country_code = $data['address']['country'];
|
||||||
|
$data['address']['country_name'] = $countries[ $country_code ] ?? $country_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HATEOAS links.
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/buildings/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'rooms' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/buildings/' . $post->ID . '/rooms' ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare room summary for building rooms list.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @return array Room summary data.
|
||||||
|
*/
|
||||||
|
private function prepare_room_summary( \WP_Post $room ): array {
|
||||||
|
return array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
'slug' => $room->post_name,
|
||||||
|
'permalink' => get_permalink( $room->ID ),
|
||||||
|
'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 ),
|
||||||
|
'status' => get_post_meta( $room->ID, '_bnb_room_status', true ) ?: 'available',
|
||||||
|
'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'thumbnail' ) ?: null,
|
||||||
|
'_links' => array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $room->ID ) ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
452
src/Api/Controllers/GuestsController.php
Normal file
452
src/Api/Controllers/GuestsController.php
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Guests REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for guests (admin only).
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guests Controller class.
|
||||||
|
*/
|
||||||
|
final class GuestsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'guests';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /guests - List guests (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_items' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => $this->get_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /guests/search - Search guests (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/search',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'search_guests' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'q' => array(
|
||||||
|
'description' => __( 'Search query (name, email, phone).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'description' => __( 'Maximum results.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 10,
|
||||||
|
'minimum' => 1,
|
||||||
|
'maximum' => 50,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /guests/{id} - Get single guest (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_item' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Guest ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /guests/{id}/bookings - Get guest's bookings (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/bookings',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_guest_bookings' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Guest ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of guests.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagination = $this->get_pagination_params( $request );
|
||||||
|
$sorting = $this->get_sorting_params( $request, array( 'title', 'date' ), 'title' );
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $pagination['per_page'],
|
||||||
|
'offset' => $pagination['offset'],
|
||||||
|
'orderby' => $sorting['orderby'],
|
||||||
|
'order' => $sorting['order'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Search filter.
|
||||||
|
$search = $request->get_param( 'search' );
|
||||||
|
if ( $search ) {
|
||||||
|
$args['s'] = $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$args['meta_query'] = array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_status',
|
||||||
|
'value' => $status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_guest_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search guests by name, email, or phone.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function search_guests( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $request->get_param( 'q' );
|
||||||
|
$limit = $request->get_param( 'limit' );
|
||||||
|
|
||||||
|
// Search by title (name) first.
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $limit,
|
||||||
|
's' => $query,
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = get_posts( $args );
|
||||||
|
|
||||||
|
// Also search by email and phone if we have room for more results.
|
||||||
|
if ( count( $results ) < $limit ) {
|
||||||
|
$existing_ids = wp_list_pluck( $results, 'ID' );
|
||||||
|
$remaining = $limit - count( $results );
|
||||||
|
|
||||||
|
// Search by email.
|
||||||
|
$email_args = array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $remaining,
|
||||||
|
'post__not_in' => $existing_ids,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_email',
|
||||||
|
'value' => $query,
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$email_results = get_posts( $email_args );
|
||||||
|
$results = array_merge( $results, $email_results );
|
||||||
|
|
||||||
|
// Search by phone if still room.
|
||||||
|
if ( count( $results ) < $limit ) {
|
||||||
|
$existing_ids = wp_list_pluck( $results, 'ID' );
|
||||||
|
$remaining = $limit - count( $results );
|
||||||
|
|
||||||
|
$phone_args = array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $remaining,
|
||||||
|
'post__not_in' => $existing_ids,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_phone',
|
||||||
|
'value' => $query,
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$phone_results = get_posts( $phone_args );
|
||||||
|
$results = array_merge( $results, $phone_results );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
foreach ( $results as $post ) {
|
||||||
|
$items[] = $this->prepare_guest_summary( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single guest.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Guest::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Guest', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_guest_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest's booking history.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_guest_bookings( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$guest_id = $request->get_param( 'id' );
|
||||||
|
$guest = get_post( $guest_id );
|
||||||
|
|
||||||
|
if ( ! $guest || Guest::POST_TYPE !== $guest->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Guest', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookings = Guest::get_bookings( $guest_id );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$room = $room_id ? get_post( $room_id ) : null;
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
|
||||||
|
$items[] = array(
|
||||||
|
'id' => $booking->ID,
|
||||||
|
'reference' => get_post_meta( $booking->ID, '_bnb_booking_reference', true ) ?: $booking->post_title,
|
||||||
|
'room' => $room ? array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
) : null,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'status' => get_post_meta( $booking->ID, '_bnb_booking_status', true ),
|
||||||
|
'total' => (float) get_post_meta( $booking->ID, '_bnb_booking_total_price', true ),
|
||||||
|
'created_at' => $booking->post_date_gmt,
|
||||||
|
'_links' => array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/bookings/' . $booking->ID ) ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare guest data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Guest post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Guest data.
|
||||||
|
*/
|
||||||
|
private function prepare_guest_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$data = array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'first_name' => get_post_meta( $post->ID, '_bnb_guest_first_name', true ),
|
||||||
|
'last_name' => get_post_meta( $post->ID, '_bnb_guest_last_name', true ),
|
||||||
|
'email' => get_post_meta( $post->ID, '_bnb_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $post->ID, '_bnb_guest_phone', true ),
|
||||||
|
'status' => get_post_meta( $post->ID, '_bnb_guest_status', true ) ?: 'active',
|
||||||
|
'created_at' => $post->post_date_gmt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Address.
|
||||||
|
$data['address'] = array(
|
||||||
|
'street' => get_post_meta( $post->ID, '_bnb_guest_street', true ),
|
||||||
|
'city' => get_post_meta( $post->ID, '_bnb_guest_city', true ),
|
||||||
|
'postal_code' => get_post_meta( $post->ID, '_bnb_guest_postal_code', true ),
|
||||||
|
'country' => get_post_meta( $post->ID, '_bnb_guest_country', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Statistics.
|
||||||
|
$booking_count = Guest::get_booking_count( $post->ID );
|
||||||
|
$total_spent = Guest::get_total_spent( $post->ID );
|
||||||
|
|
||||||
|
$data['statistics'] = array(
|
||||||
|
'total_bookings' => $booking_count,
|
||||||
|
'total_spent' => $total_spent,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $full ) {
|
||||||
|
$data['nationality'] = get_post_meta( $post->ID, '_bnb_guest_nationality', true );
|
||||||
|
$data['date_of_birth'] = get_post_meta( $post->ID, '_bnb_guest_date_of_birth', true );
|
||||||
|
$data['notes'] = get_post_meta( $post->ID, '_bnb_guest_notes', true );
|
||||||
|
$data['preferences'] = get_post_meta( $post->ID, '_bnb_guest_preferences', true );
|
||||||
|
|
||||||
|
// Get last stay date.
|
||||||
|
$bookings = Guest::get_bookings( $post->ID );
|
||||||
|
if ( ! empty( $bookings ) ) {
|
||||||
|
$last_booking = $bookings[0];
|
||||||
|
$data['statistics']['last_stay'] = get_post_meta( $last_booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatted address.
|
||||||
|
$data['address']['formatted'] = Guest::get_formatted_address( $post->ID );
|
||||||
|
|
||||||
|
// GDPR consent info.
|
||||||
|
$data['consent'] = array(
|
||||||
|
'data_processing' => (bool) get_post_meta( $post->ID, '_bnb_guest_consent_data', true ),
|
||||||
|
'marketing' => (bool) get_post_meta( $post->ID, '_bnb_guest_consent_marketing', true ),
|
||||||
|
'date' => get_post_meta( $post->ID, '_bnb_guest_consent_date', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: ID/passport numbers are NOT exposed via API for security.
|
||||||
|
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/guests/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'bookings' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/guests/' . $post->ID . '/bookings' ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare guest summary for search results.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Guest post object.
|
||||||
|
* @return array Guest summary.
|
||||||
|
*/
|
||||||
|
private function prepare_guest_summary( \WP_Post $post ): array {
|
||||||
|
return array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'first_name' => get_post_meta( $post->ID, '_bnb_guest_first_name', true ),
|
||||||
|
'last_name' => get_post_meta( $post->ID, '_bnb_guest_last_name', true ),
|
||||||
|
'email' => get_post_meta( $post->ID, '_bnb_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $post->ID, '_bnb_guest_phone', true ),
|
||||||
|
'_links' => array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/guests/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection parameters with status filter.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
public function get_collection_params(): array {
|
||||||
|
$params = parent::get_collection_params();
|
||||||
|
|
||||||
|
$params['status'] = array(
|
||||||
|
'description' => __( 'Filter by guest status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'active', 'inactive', 'blocked' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
}
|
||||||
278
src/Api/Controllers/PricingController.php
Normal file
278
src/Api/Controllers/PricingController.php
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pricing REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for price calculations.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pricing Controller class.
|
||||||
|
*/
|
||||||
|
final class PricingController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'pricing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// POST /pricing/calculate - Calculate full booking price.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/calculate',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'calculate_price' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'room_id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'services' => array(
|
||||||
|
'description' => __( 'Additional services.', 'wp-bnb' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'service_id' => array( 'type' => 'integer', 'required' => true ),
|
||||||
|
'quantity' => array( 'type' => 'integer', 'default' => 1 ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /pricing/seasons - Get active seasons.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/seasons',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_seasons' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate full booking price.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function calculate_price( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'room_id' );
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
$services = $request->get_param( 'services' ) ?? array();
|
||||||
|
|
||||||
|
// Validate room.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
|
||||||
|
// Calculate room price.
|
||||||
|
$price = Calculator::calculate( $room_id, $check_in, $check_out );
|
||||||
|
$room_total = $price['price'] ?? 0;
|
||||||
|
$breakdown = $price['breakdown'] ?? array();
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
// Build night-by-night breakdown.
|
||||||
|
$night_breakdown = array();
|
||||||
|
$current_date = $check_in_date;
|
||||||
|
$base_rate = $breakdown['base_price_per_night'] ?? 0;
|
||||||
|
|
||||||
|
while ( $current_date < $check_out_date ) {
|
||||||
|
$date_str = $current_date->format( 'Y-m-d' );
|
||||||
|
$day_of_week = (int) $current_date->format( 'w' );
|
||||||
|
$modifiers = array();
|
||||||
|
$rate = $base_rate;
|
||||||
|
|
||||||
|
// Check for weekend surcharge.
|
||||||
|
$weekend_days = explode( ',', get_option( 'wp_bnb_weekend_days', '5,6' ) );
|
||||||
|
if ( in_array( (string) $day_of_week, $weekend_days, true ) ) {
|
||||||
|
$weekend_surcharge = $breakdown['weekend_surcharge'] ?? 0;
|
||||||
|
if ( $weekend_surcharge > 0 ) {
|
||||||
|
$rate += $weekend_surcharge / max( 1, $breakdown['weekend_nights'] ?? 1 );
|
||||||
|
$modifiers[] = 'weekend_surcharge';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for seasonal modifier.
|
||||||
|
$season = Season::get_active_season( $date_str );
|
||||||
|
if ( $season && $season['modifier'] != 1.0 ) {
|
||||||
|
$modifiers[] = 'season:' . $season['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$night_breakdown[] = array(
|
||||||
|
'date' => $date_str,
|
||||||
|
'rate' => round( $rate, 2 ),
|
||||||
|
'modifiers' => $modifiers,
|
||||||
|
);
|
||||||
|
|
||||||
|
$current_date = $current_date->modify( '+1 day' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate services.
|
||||||
|
$services_items = array();
|
||||||
|
$services_total = 0;
|
||||||
|
|
||||||
|
foreach ( $services as $service_item ) {
|
||||||
|
$service_id = $service_item['service_id'] ?? 0;
|
||||||
|
$quantity = $service_item['quantity'] ?? 1;
|
||||||
|
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
if ( $service_data && 'active' === $service_data['status'] ) {
|
||||||
|
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
$services_total += $service_price;
|
||||||
|
|
||||||
|
$services_items[] = array(
|
||||||
|
'id' => $service_id,
|
||||||
|
'name' => $service_data['title'],
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'nights' => 'per_night' === $service_data['pricing_type'] ? $nights : null,
|
||||||
|
'subtotal' => $service_price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$grand_total = $room_total + $services_total;
|
||||||
|
|
||||||
|
// Build response.
|
||||||
|
$data = array(
|
||||||
|
'room' => array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
'pricing_tier' => $breakdown['tier']->value ?? 'short_term',
|
||||||
|
),
|
||||||
|
'dates' => array(
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
),
|
||||||
|
'room_pricing' => array(
|
||||||
|
'base_rate' => $breakdown['base_price_per_night'] ?? 0,
|
||||||
|
'subtotal' => $room_total,
|
||||||
|
'breakdown' => $night_breakdown,
|
||||||
|
),
|
||||||
|
'services_pricing' => array(
|
||||||
|
'items' => $services_items,
|
||||||
|
'subtotal' => $services_total,
|
||||||
|
),
|
||||||
|
'totals' => array(
|
||||||
|
'room' => $room_total,
|
||||||
|
'services' => $services_total,
|
||||||
|
'grand_total' => $grand_total,
|
||||||
|
'currency' => $currency,
|
||||||
|
'formatted' => Calculator::formatPrice( $grand_total ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active seasons.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_seasons( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seasons = Season::get_all();
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $seasons as $season ) {
|
||||||
|
$items[] = array(
|
||||||
|
'id' => $season['id'],
|
||||||
|
'name' => $season['name'],
|
||||||
|
'start_date' => $season['start_date'],
|
||||||
|
'end_date' => $season['end_date'],
|
||||||
|
'modifier' => (float) $season['modifier'],
|
||||||
|
'priority' => (int) $season['priority'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority (highest first).
|
||||||
|
usort(
|
||||||
|
$items,
|
||||||
|
function ( $a, $b ) {
|
||||||
|
return $b['priority'] - $a['priority'];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
}
|
||||||
768
src/Api/Controllers/RoomsController.php
Normal file
768
src/Api/Controllers/RoomsController.php
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Rooms REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for rooms, availability, and calendar.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms Controller class.
|
||||||
|
*/
|
||||||
|
final class RoomsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'rooms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /rooms - List all rooms.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_items' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => $this->get_rooms_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /rooms/{id} - Get single room.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_item' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /rooms/{id}/availability - Check room availability.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/availability',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_availability' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /rooms/{id}/calendar - Get room calendar.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/calendar',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_calendar' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'year' => array(
|
||||||
|
'description' => __( 'Year.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => (int) gmdate( 'Y' ),
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'month' => array(
|
||||||
|
'description' => __( 'Month (1-12).', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => (int) gmdate( 'n' ),
|
||||||
|
'minimum' => 1,
|
||||||
|
'maximum' => 12,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /availability/search - Search available rooms.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/availability/search',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'search_availability' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'guests' => array(
|
||||||
|
'description' => __( 'Number of guests.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'building_id' => array(
|
||||||
|
'description' => __( 'Building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'room_type' => array(
|
||||||
|
'description' => __( 'Room type term ID or slug.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'amenities' => array(
|
||||||
|
'description' => __( 'Comma-separated amenity slugs.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'price_min' => array(
|
||||||
|
'description' => __( 'Minimum price per night.', 'wp-bnb' ),
|
||||||
|
'type' => 'number',
|
||||||
|
),
|
||||||
|
'price_max' => array(
|
||||||
|
'description' => __( 'Maximum price per night.', 'wp-bnb' ),
|
||||||
|
'type' => 'number',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of rooms.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pagination = $this->get_pagination_params( $request );
|
||||||
|
$sorting = $this->get_sorting_params( $request, array( 'title', 'date', 'capacity', 'price' ), 'title' );
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $pagination['per_page'],
|
||||||
|
'offset' => $pagination['offset'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle special orderby values.
|
||||||
|
switch ( $sorting['orderby'] ) {
|
||||||
|
case 'capacity':
|
||||||
|
$args['meta_key'] = '_bnb_room_capacity';
|
||||||
|
$args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
case 'price':
|
||||||
|
$args['meta_key'] = '_bnb_room_price_short_term';
|
||||||
|
$args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$args['orderby'] = $sorting['orderby'];
|
||||||
|
}
|
||||||
|
$args['order'] = $sorting['order'];
|
||||||
|
|
||||||
|
// Search filter.
|
||||||
|
$search = $request->get_param( 'search' );
|
||||||
|
if ( $search ) {
|
||||||
|
$args['s'] = $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Building filter.
|
||||||
|
$building = $request->get_param( 'building' );
|
||||||
|
if ( $building ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => $building,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capacity filter.
|
||||||
|
$capacity_min = $request->get_param( 'capacity_min' );
|
||||||
|
if ( $capacity_min ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_capacity',
|
||||||
|
'value' => $capacity_min,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'NUMERIC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room type filter.
|
||||||
|
$room_type = $request->get_param( 'room_type' );
|
||||||
|
if ( $room_type ) {
|
||||||
|
$args['tax_query'][] = array(
|
||||||
|
'taxonomy' => RoomType::TAXONOMY,
|
||||||
|
'field' => is_numeric( $room_type ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $room_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amenities filter.
|
||||||
|
$amenities = $request->get_param( 'amenities' );
|
||||||
|
if ( $amenities ) {
|
||||||
|
$amenity_slugs = array_map( 'trim', explode( ',', $amenities ) );
|
||||||
|
$args['tax_query'][] = array(
|
||||||
|
'taxonomy' => Amenity::TAXONOMY,
|
||||||
|
'field' => 'slug',
|
||||||
|
'terms' => $amenity_slugs,
|
||||||
|
'operator' => 'AND',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $args['meta_query'] ) && count( $args['meta_query'] ) > 1 ) {
|
||||||
|
$args['meta_query']['relation'] = 'AND';
|
||||||
|
}
|
||||||
|
if ( isset( $args['tax_query'] ) && count( $args['tax_query'] ) > 1 ) {
|
||||||
|
$args['tax_query']['relation'] = 'AND';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_room_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single room.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Room::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_room_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room availability.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_availability( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'id' );
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
|
||||||
|
// Validate room exists.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability.
|
||||||
|
$is_available = Availability::is_available( $room_id, $check_in, $check_out );
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'available' => $is_available,
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $is_available ) {
|
||||||
|
// Calculate pricing.
|
||||||
|
$price = Calculator::calculate( $room_id, $check_in, $check_out );
|
||||||
|
$data['pricing'] = array(
|
||||||
|
'tier' => $price['breakdown']['tier']->value ?? 'short_term',
|
||||||
|
'base_rate' => $price['breakdown']['base_price_per_night'] ?? 0,
|
||||||
|
'total' => $price['price'] ?? 0,
|
||||||
|
'formatted' => $price['price_formatted'] ?? '',
|
||||||
|
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||||
|
'breakdown' => $price['breakdown'] ?? array(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Get conflicts.
|
||||||
|
$conflicts = $this->get_conflicts( $room_id, $check_in, $check_out );
|
||||||
|
$data['conflicts'] = $conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room calendar.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_calendar( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'id' );
|
||||||
|
$year = $request->get_param( 'year' );
|
||||||
|
$month = $request->get_param( 'month' );
|
||||||
|
|
||||||
|
// Validate room exists.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate month.
|
||||||
|
if ( $month < 1 || $month > 12 ) {
|
||||||
|
return $this->formatter->validation_error( 'month', __( 'Month must be between 1 and 12.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Availability::get_calendar_data( $room_id, $year, $month );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search available rooms.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function search_availability( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build search args.
|
||||||
|
$search_args = array(
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
);
|
||||||
|
|
||||||
|
$guests = $request->get_param( 'guests' );
|
||||||
|
if ( $guests ) {
|
||||||
|
$search_args['guests'] = $guests;
|
||||||
|
}
|
||||||
|
|
||||||
|
$building_id = $request->get_param( 'building_id' );
|
||||||
|
if ( $building_id ) {
|
||||||
|
$search_args['building_id'] = $building_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_type = $request->get_param( 'room_type' );
|
||||||
|
if ( $room_type ) {
|
||||||
|
$search_args['room_type'] = $room_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
$amenities = $request->get_param( 'amenities' );
|
||||||
|
if ( $amenities ) {
|
||||||
|
$search_args['amenities'] = array_map( 'trim', explode( ',', $amenities ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$price_min = $request->get_param( 'price_min' );
|
||||||
|
if ( $price_min ) {
|
||||||
|
$search_args['price_min'] = $price_min;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price_max = $request->get_param( 'price_max' );
|
||||||
|
if ( $price_max ) {
|
||||||
|
$search_args['price_max'] = $price_max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing Search class.
|
||||||
|
$results = Search::search( $search_args );
|
||||||
|
|
||||||
|
// Format response.
|
||||||
|
$items = array();
|
||||||
|
foreach ( $results['rooms'] as $room_data ) {
|
||||||
|
$items[] = array(
|
||||||
|
'room' => $room_data,
|
||||||
|
'availability' => array(
|
||||||
|
'available' => true,
|
||||||
|
'nights' => $room_data['nights'] ?? 0,
|
||||||
|
'total_price' => $room_data['stay_price'] ?? 0,
|
||||||
|
'formatted_price' => $room_data['stay_price_formatted'] ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'results' => $items,
|
||||||
|
'total' => $results['count'],
|
||||||
|
'filters_applied' => $search_args,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conflicting bookings for a date range.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room ID.
|
||||||
|
* @param string $check_in Check-in date.
|
||||||
|
* @param string $check_out Check-out date.
|
||||||
|
* @return array Conflicting bookings.
|
||||||
|
*/
|
||||||
|
private function get_conflicts( int $room_id, string $check_in, string $check_out ): array {
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'cancelled',
|
||||||
|
'compare' => '!=',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $check_out,
|
||||||
|
'compare' => '<',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $check_in,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$conflicts = array();
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$conflicts[] = array(
|
||||||
|
'booking_id' => $booking->ID,
|
||||||
|
'reference' => $booking->post_title,
|
||||||
|
'check_in' => get_post_meta( $booking->ID, '_bnb_booking_check_in', true ),
|
||||||
|
'check_out' => get_post_meta( $booking->ID, '_bnb_booking_check_out', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare room data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Room post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Room data.
|
||||||
|
*/
|
||||||
|
private function prepare_room_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$data = $this->format_post_base( $post );
|
||||||
|
|
||||||
|
$data['permalink'] = get_permalink( $post->ID );
|
||||||
|
$data['featured_image'] = $this->format_featured_image( $post->ID );
|
||||||
|
|
||||||
|
// Gallery.
|
||||||
|
$gallery_ids = get_post_meta( $post->ID, '_bnb_room_gallery', true );
|
||||||
|
$gallery = array();
|
||||||
|
if ( $gallery_ids ) {
|
||||||
|
foreach ( explode( ',', $gallery_ids ) as $image_id ) {
|
||||||
|
$image = $this->format_image( (int) $image_id );
|
||||||
|
if ( $image ) {
|
||||||
|
$gallery[] = $image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data['gallery'] = $gallery;
|
||||||
|
|
||||||
|
// Building reference.
|
||||||
|
$building_id = get_post_meta( $post->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
$data['building'] = $building ? array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => get_the_title( $building ),
|
||||||
|
'slug' => $building->post_name,
|
||||||
|
'permalink' => get_permalink( $building->ID ),
|
||||||
|
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
// Room details.
|
||||||
|
$data['room_number'] = get_post_meta( $post->ID, '_bnb_room_room_number', true );
|
||||||
|
$data['floor'] = (int) get_post_meta( $post->ID, '_bnb_room_floor', true );
|
||||||
|
$data['size_sqm'] = (float) get_post_meta( $post->ID, '_bnb_room_size', true );
|
||||||
|
|
||||||
|
// Capacity.
|
||||||
|
$data['capacity'] = array(
|
||||||
|
'max_guests' => (int) get_post_meta( $post->ID, '_bnb_room_capacity', true ),
|
||||||
|
'adults' => (int) get_post_meta( $post->ID, '_bnb_room_max_adults', true ),
|
||||||
|
'children' => (int) get_post_meta( $post->ID, '_bnb_room_max_children', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$data['beds'] = get_post_meta( $post->ID, '_bnb_room_beds', true );
|
||||||
|
$data['bathrooms'] = (float) get_post_meta( $post->ID, '_bnb_room_bathrooms', true );
|
||||||
|
$data['status'] = get_post_meta( $post->ID, '_bnb_room_status', true ) ?: 'available';
|
||||||
|
|
||||||
|
// Room types.
|
||||||
|
$room_types = wp_get_post_terms( $post->ID, RoomType::TAXONOMY );
|
||||||
|
$data['room_types'] = array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
is_array( $room_types ) ? $room_types : array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Amenities.
|
||||||
|
$amenities = wp_get_post_terms( $post->ID, Amenity::TAXONOMY );
|
||||||
|
$data['amenities'] = array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'icon' => get_term_meta( $term->term_id, '_bnb_amenity_icon', true ),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
is_array( $amenities ) ? $amenities : array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pricing.
|
||||||
|
$pricing = Calculator::getRoomPricing( $post->ID );
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
$data['pricing'] = array(
|
||||||
|
'currency' => $currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( PricingTier::cases() as $tier ) {
|
||||||
|
$price = $pricing[ $tier->value ]['price'] ?? null;
|
||||||
|
$data['pricing'][ $tier->value ] = array(
|
||||||
|
'price' => $price,
|
||||||
|
'formatted' => $price ? Calculator::formatPrice( $price ) : null,
|
||||||
|
'unit' => $tier->unit(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$weekend_surcharge = $pricing['weekend_surcharge']['price'] ?? null;
|
||||||
|
$data['pricing']['weekend_surcharge'] = array(
|
||||||
|
'price' => $weekend_surcharge,
|
||||||
|
'formatted' => $weekend_surcharge ? Calculator::formatPrice( $weekend_surcharge ) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add HATEOAS links.
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'building' => $building ? array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/buildings/' . $building->ID ) ),
|
||||||
|
) : array(),
|
||||||
|
'availability' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $post->ID . '/availability' ) ),
|
||||||
|
),
|
||||||
|
'calendar' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $post->ID . '/calendar' ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rooms collection parameters.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
private function get_rooms_collection_params(): array {
|
||||||
|
$params = $this->get_collection_params();
|
||||||
|
|
||||||
|
$params['building'] = array(
|
||||||
|
'description' => __( 'Filter by building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['room_type'] = array(
|
||||||
|
'description' => __( 'Filter by room type (term ID or slug).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['amenities'] = array(
|
||||||
|
'description' => __( 'Filter by amenities (comma-separated slugs).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['capacity_min'] = array(
|
||||||
|
'description' => __( 'Minimum guest capacity.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['status'] = array(
|
||||||
|
'description' => __( 'Filter by room status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'available', 'occupied', 'maintenance', 'blocked' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['orderby']['enum'] = array( 'title', 'date', 'capacity', 'price' );
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
}
|
||||||
375
src/Api/Controllers/ServicesController.php
Normal file
375
src/Api/Controllers/ServicesController.php
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Services REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for services.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Taxonomies\ServiceCategory;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services Controller class.
|
||||||
|
*/
|
||||||
|
final class ServicesController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /services - List active services (public).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_items' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => $this->get_services_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /services/{id} - Get single service (public).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_item' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Service ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /services/{id}/calculate - Calculate service price.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/calculate',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'calculate_price' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Service ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'quantity' => array(
|
||||||
|
'description' => __( 'Quantity.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'nights' => array(
|
||||||
|
'description' => __( 'Number of nights (for per-night services).', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of services.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Service::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => 100, // Services typically don't need pagination.
|
||||||
|
'orderby' => 'meta_value_num',
|
||||||
|
'meta_key' => '_bnb_service_sort_order',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$meta_query = array();
|
||||||
|
|
||||||
|
// Status filter (default: active only).
|
||||||
|
$status = $request->get_param( 'status' ) ?: 'active';
|
||||||
|
if ( 'all' !== $status ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_service_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pricing type filter.
|
||||||
|
$pricing_type = $request->get_param( 'pricing_type' );
|
||||||
|
if ( $pricing_type ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_service_pricing_type',
|
||||||
|
'value' => $pricing_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta_query ) ) {
|
||||||
|
$meta_query['relation'] = 'AND';
|
||||||
|
$args['meta_query'] = $meta_query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category filter.
|
||||||
|
$category = $request->get_param( 'category' );
|
||||||
|
if ( $category ) {
|
||||||
|
$args['tax_query'] = array(
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'bnb_service_category',
|
||||||
|
'field' => is_numeric( $category ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $category,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$services = get_posts( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$items[] = $this->prepare_service_response( $service );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single service.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
if ( ! $post || Service::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Service', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_service_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate service price.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function calculate_price( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service_id = $request->get_param( 'id' );
|
||||||
|
$quantity = $request->get_param( 'quantity' );
|
||||||
|
$nights = $request->get_param( 'nights' );
|
||||||
|
|
||||||
|
// Validate service.
|
||||||
|
$service = get_post( $service_id );
|
||||||
|
if ( ! $service || Service::POST_TYPE !== $service->post_type || 'publish' !== $service->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Service', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service is active.
|
||||||
|
$status = get_post_meta( $service_id, '_bnb_service_status', true );
|
||||||
|
if ( 'active' !== $status ) {
|
||||||
|
return $this->formatter->validation_error( 'id', __( 'Service is not available.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check max quantity.
|
||||||
|
$max_quantity = (int) get_post_meta( $service_id, '_bnb_service_max_quantity', true ) ?: 1;
|
||||||
|
if ( $quantity > $max_quantity ) {
|
||||||
|
return $this->formatter->validation_error(
|
||||||
|
'quantity',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %d: maximum quantity */
|
||||||
|
__( 'Maximum quantity is %d.', 'wp-bnb' ),
|
||||||
|
$max_quantity
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate price.
|
||||||
|
$total = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
$pricing_type = get_post_meta( $service_id, '_bnb_service_pricing_type', true );
|
||||||
|
$unit_price = (float) get_post_meta( $service_id, '_bnb_service_price', true );
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
// Build calculation string.
|
||||||
|
$calculation = '';
|
||||||
|
switch ( $pricing_type ) {
|
||||||
|
case 'included':
|
||||||
|
$calculation = __( 'Included', 'wp-bnb' );
|
||||||
|
break;
|
||||||
|
case 'per_booking':
|
||||||
|
$calculation = sprintf(
|
||||||
|
'%s x %d',
|
||||||
|
Calculator::formatPrice( $unit_price ),
|
||||||
|
$quantity
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'per_night':
|
||||||
|
$calculation = sprintf(
|
||||||
|
'%s x %d x %d %s',
|
||||||
|
Calculator::formatPrice( $unit_price ),
|
||||||
|
$quantity,
|
||||||
|
$nights,
|
||||||
|
_n( 'night', 'nights', $nights, 'wp-bnb' )
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'nights' => $nights,
|
||||||
|
'unit_price' => $unit_price,
|
||||||
|
'total' => $total,
|
||||||
|
'formatted' => Calculator::formatPrice( $total ),
|
||||||
|
'currency' => $currency,
|
||||||
|
'calculation' => $calculation,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare service data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Service post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Service data.
|
||||||
|
*/
|
||||||
|
private function prepare_service_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$pricing_type = get_post_meta( $post->ID, '_bnb_service_pricing_type', true );
|
||||||
|
$price = (float) get_post_meta( $post->ID, '_bnb_service_price', true );
|
||||||
|
$status = get_post_meta( $post->ID, '_bnb_service_status', true ) ?: 'active';
|
||||||
|
$max_quantity = (int) get_post_meta( $post->ID, '_bnb_service_max_quantity', true ) ?: 1;
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'title' => get_the_title( $post ),
|
||||||
|
'slug' => $post->post_name,
|
||||||
|
'description' => get_the_excerpt( $post ),
|
||||||
|
'pricing' => array(
|
||||||
|
'type' => $pricing_type,
|
||||||
|
'price' => $price,
|
||||||
|
'formatted' => Service::format_service_price( Service::get_service_data( $post->ID ) ),
|
||||||
|
'currency' => $currency,
|
||||||
|
),
|
||||||
|
'max_quantity' => $max_quantity,
|
||||||
|
'status' => $status,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Category.
|
||||||
|
$categories = wp_get_post_terms( $post->ID, 'bnb_service_category' );
|
||||||
|
if ( ! empty( $categories ) && ! is_wp_error( $categories ) ) {
|
||||||
|
$category = $categories[0];
|
||||||
|
$data['category'] = array(
|
||||||
|
'id' => $category->term_id,
|
||||||
|
'name' => $category->name,
|
||||||
|
'slug' => $category->slug,
|
||||||
|
'icon' => get_term_meta( $category->term_id, '_bnb_service_category_icon', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $full ) {
|
||||||
|
$data['content'] = apply_filters( 'the_content', $post->post_content );
|
||||||
|
$data['sort_order'] = (int) get_post_meta( $post->ID, '_bnb_service_sort_order', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/services/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get services collection parameters.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
private function get_services_collection_params(): array {
|
||||||
|
return array(
|
||||||
|
'status' => array(
|
||||||
|
'description' => __( 'Filter by status (default: active).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'active', 'inactive', 'all' ),
|
||||||
|
'default' => 'active',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'pricing_type' => array(
|
||||||
|
'description' => __( 'Filter by pricing type.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'included', 'per_booking', 'per_night' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'category' => array(
|
||||||
|
'description' => __( 'Filter by category (term ID or slug).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/Api/RateLimiter.php
Normal file
194
src/Api/RateLimiter.php
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Rate Limiter
|
||||||
|
*
|
||||||
|
* Provides transient-based rate limiting for API endpoints.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate Limiter class.
|
||||||
|
*/
|
||||||
|
final class RateLimiter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transient prefix for rate limit data.
|
||||||
|
*/
|
||||||
|
private const TRANSIENT_PREFIX = 'wp_bnb_rate_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limits per minute by endpoint type.
|
||||||
|
*
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private array $limits = array(
|
||||||
|
'public' => 60, // Public read endpoints.
|
||||||
|
'availability' => 30, // Availability checks.
|
||||||
|
'booking' => 10, // Booking creation.
|
||||||
|
'admin' => 120, // Admin endpoints.
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time window in seconds.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $window = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request is within rate limit.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier (user ID or IP).
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return bool True if within limit, false if exceeded.
|
||||||
|
*/
|
||||||
|
public function check( string $identifier, string $endpoint ): bool {
|
||||||
|
$type = $this->get_endpoint_type( $endpoint );
|
||||||
|
$limit = $this->limits[ $type ] ?? $this->limits['public'];
|
||||||
|
$key = $this->get_transient_key( $identifier, $type );
|
||||||
|
$data = get_transient( $key );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
// First request in window.
|
||||||
|
set_transient(
|
||||||
|
$key,
|
||||||
|
array(
|
||||||
|
'count' => 1,
|
||||||
|
'start' => time(),
|
||||||
|
),
|
||||||
|
$this->window
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if window expired.
|
||||||
|
if ( time() - $data['start'] >= $this->window ) {
|
||||||
|
set_transient(
|
||||||
|
$key,
|
||||||
|
array(
|
||||||
|
'count' => 1,
|
||||||
|
'start' => time(),
|
||||||
|
),
|
||||||
|
$this->window
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit exceeded.
|
||||||
|
if ( $data['count'] >= $limit ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment counter.
|
||||||
|
++$data['count'];
|
||||||
|
$remaining_window = $this->window - ( time() - $data['start'] );
|
||||||
|
set_transient( $key, $data, $remaining_window );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get seconds until rate limit resets.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier.
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return int Seconds until reset.
|
||||||
|
*/
|
||||||
|
public function get_retry_after( string $identifier, string $endpoint ): int {
|
||||||
|
$type = $this->get_endpoint_type( $endpoint );
|
||||||
|
$key = $this->get_transient_key( $identifier, $type );
|
||||||
|
$data = get_transient( $key );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max( 0, $this->window - ( time() - $data['start'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current rate limit info for headers.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier.
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return array{limit: int, remaining: int, reset: int}
|
||||||
|
*/
|
||||||
|
public function get_rate_limit_info( string $identifier, string $endpoint ): array {
|
||||||
|
$type = $this->get_endpoint_type( $endpoint );
|
||||||
|
$limit = $this->limits[ $type ] ?? $this->limits['public'];
|
||||||
|
$key = $this->get_transient_key( $identifier, $type );
|
||||||
|
$data = get_transient( $key );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
return array(
|
||||||
|
'limit' => $limit,
|
||||||
|
'remaining' => $limit,
|
||||||
|
'reset' => time() + $this->window,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$remaining = max( 0, $limit - $data['count'] );
|
||||||
|
$reset = $data['start'] + $this->window;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'limit' => $limit,
|
||||||
|
'remaining' => $remaining,
|
||||||
|
'reset' => $reset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine endpoint type from route.
|
||||||
|
*
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return string Endpoint type.
|
||||||
|
*/
|
||||||
|
private function get_endpoint_type( string $endpoint ): string {
|
||||||
|
if ( str_contains( $endpoint, '/availability' ) || str_contains( $endpoint, '/calendar' ) ) {
|
||||||
|
return 'availability';
|
||||||
|
}
|
||||||
|
if ( str_contains( $endpoint, '/bookings' ) ) {
|
||||||
|
return 'booking';
|
||||||
|
}
|
||||||
|
if ( str_contains( $endpoint, '/guests' ) ) {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
return 'public';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transient key for rate limit data.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier.
|
||||||
|
* @param string $type Endpoint type.
|
||||||
|
* @return string Transient key.
|
||||||
|
*/
|
||||||
|
private function get_transient_key( string $identifier, string $type ): string {
|
||||||
|
return self::TRANSIENT_PREFIX . md5( $identifier . '_' . $type );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom rate limits.
|
||||||
|
*
|
||||||
|
* @param array<string, int> $limits Rate limits by type.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_limits( array $limits ): void {
|
||||||
|
$this->limits = array_merge( $this->limits, $limits );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom time window.
|
||||||
|
*
|
||||||
|
* @param int $window Window in seconds.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_window( int $window ): void {
|
||||||
|
$this->window = $window;
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/Api/ResponseFormatter.php
Normal file
171
src/Api/ResponseFormatter.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Response Formatter
|
||||||
|
*
|
||||||
|
* Provides standardized response formatting for all API endpoints.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api;
|
||||||
|
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response Formatter class.
|
||||||
|
*/
|
||||||
|
final class ResponseFormatter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format successful response.
|
||||||
|
*
|
||||||
|
* @param mixed $data Response data.
|
||||||
|
* @param int $status HTTP status code.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function success( mixed $data, int $status = 200 ): WP_REST_Response {
|
||||||
|
return new WP_REST_Response( $data, $status );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format collection response with pagination headers.
|
||||||
|
*
|
||||||
|
* @param array $items Collection items.
|
||||||
|
* @param int $total Total number of items.
|
||||||
|
* @param int $page Current page number.
|
||||||
|
* @param int $per_page Items per page.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function collection( array $items, int $total, int $page, int $per_page ): WP_REST_Response {
|
||||||
|
$response = new WP_REST_Response( $items, 200 );
|
||||||
|
$max_pages = (int) ceil( $total / $per_page );
|
||||||
|
|
||||||
|
$response->header( 'X-WP-Total', (string) $total );
|
||||||
|
$response->header( 'X-WP-TotalPages', (string) $max_pages );
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format created response (201).
|
||||||
|
*
|
||||||
|
* @param mixed $data Response data.
|
||||||
|
* @param string $location Location header URL.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function created( mixed $data, string $location = '' ): WP_REST_Response {
|
||||||
|
$response = new WP_REST_Response( $data, 201 );
|
||||||
|
if ( $location ) {
|
||||||
|
$response->header( 'Location', $location );
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format no content response (204).
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function no_content(): WP_REST_Response {
|
||||||
|
return new WP_REST_Response( null, 204 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create validation error.
|
||||||
|
*
|
||||||
|
* @param string $param Parameter name.
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function validation_error( string $param, string $message ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_invalid_param',
|
||||||
|
$message,
|
||||||
|
array(
|
||||||
|
'status' => 400,
|
||||||
|
'param' => $param,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create not found error.
|
||||||
|
*
|
||||||
|
* @param string $resource Resource name.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function not_found( string $resource = 'Resource' ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_not_found',
|
||||||
|
/* translators: %s: Resource name */
|
||||||
|
sprintf( __( '%s not found.', 'wp-bnb' ), $resource ),
|
||||||
|
array( 'status' => 404 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create forbidden error.
|
||||||
|
*
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function forbidden( string $message = '' ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_forbidden',
|
||||||
|
$message ?: __( 'You do not have permission to access this resource.', 'wp-bnb' ),
|
||||||
|
array( 'status' => 403 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create rate limit error.
|
||||||
|
*
|
||||||
|
* @param int $retry_after Seconds until rate limit resets.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function rate_limit_error( int $retry_after = 60 ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_rate_limit_exceeded',
|
||||||
|
__( 'Rate limit exceeded. Please try again later.', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'status' => 429,
|
||||||
|
'retry_after' => $retry_after,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create conflict error (e.g., booking conflict).
|
||||||
|
*
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @param array $conflicts Conflicting resources.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function conflict( string $message, array $conflicts = array() ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_conflict',
|
||||||
|
$message,
|
||||||
|
array(
|
||||||
|
'status' => 409,
|
||||||
|
'conflicts' => $conflicts,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create internal server error.
|
||||||
|
*
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function server_error( string $message = '' ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_server_error',
|
||||||
|
$message ?: __( 'An internal server error occurred.', 'wp-bnb' ),
|
||||||
|
array( 'status' => 500 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Api/RestApi.php
Normal file
113
src/Api/RestApi.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Main Class
|
||||||
|
*
|
||||||
|
* Registers all REST API controllers and routes.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Api\Controllers\BuildingsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\RoomsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\BookingsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\GuestsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\ServicesController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\PricingController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API class.
|
||||||
|
*/
|
||||||
|
final class RestApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API namespace.
|
||||||
|
*/
|
||||||
|
public const NAMESPACE = 'wp-bnb/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API version.
|
||||||
|
*/
|
||||||
|
public const VERSION = '1.0.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller instances.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $controllers = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the REST API.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function init(): void {
|
||||||
|
// Check if API is enabled.
|
||||||
|
if ( 'yes' !== get_option( 'wp_bnb_api_enabled', 'yes' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all API routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
$this->controllers = array(
|
||||||
|
new BuildingsController(),
|
||||||
|
new RoomsController(),
|
||||||
|
new BookingsController(),
|
||||||
|
new GuestsController(),
|
||||||
|
new ServicesController(),
|
||||||
|
new PricingController(),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $this->controllers as $controller ) {
|
||||||
|
$controller->register_routes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register API info endpoint.
|
||||||
|
register_rest_route(
|
||||||
|
self::NAMESPACE,
|
||||||
|
'/info',
|
||||||
|
array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array( $this, 'get_api_info' ),
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API information.
|
||||||
|
*
|
||||||
|
* @return \WP_REST_Response API info response.
|
||||||
|
*/
|
||||||
|
public function get_api_info(): \WP_REST_Response {
|
||||||
|
return new \WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'name' => 'WP BnB REST API',
|
||||||
|
'version' => self::VERSION,
|
||||||
|
'namespace' => self::NAMESPACE,
|
||||||
|
'description' => __( 'REST API for WP BnB booking management.', 'wp-bnb' ),
|
||||||
|
'endpoints' => array(
|
||||||
|
'buildings' => rest_url( self::NAMESPACE . '/buildings' ),
|
||||||
|
'rooms' => rest_url( self::NAMESPACE . '/rooms' ),
|
||||||
|
'bookings' => rest_url( self::NAMESPACE . '/bookings' ),
|
||||||
|
'guests' => rest_url( self::NAMESPACE . '/guests' ),
|
||||||
|
'services' => rest_url( self::NAMESPACE . '/services' ),
|
||||||
|
'pricing' => rest_url( self::NAMESPACE . '/pricing' ),
|
||||||
|
'availability' => rest_url( self::NAMESPACE . '/availability' ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1654
src/Integration/CF7.php
Normal file
1654
src/Integration/CF7.php
Normal file
File diff suppressed because it is too large
Load Diff
877
src/Integration/Prometheus.php
Normal file
877
src/Integration/Prometheus.php
Normal file
@@ -0,0 +1,877 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Prometheus Metrics Integration.
|
||||||
|
*
|
||||||
|
* Provides meaningful metrics for monitoring BnB operations.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prometheus Metrics Integration class.
|
||||||
|
*
|
||||||
|
* Exposes BnB metrics via the wp-prometheus plugin.
|
||||||
|
*/
|
||||||
|
class Prometheus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for enabling metrics.
|
||||||
|
*/
|
||||||
|
public const OPTION_ENABLED = 'wp_bnb_metrics_enabled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Prometheus integration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Only hook if metrics are enabled.
|
||||||
|
if ( ! self::is_enabled() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook into wp-prometheus collector.
|
||||||
|
add_action( 'wp_prometheus_collect_metrics', array( self::class, 'collect_metrics' ) );
|
||||||
|
|
||||||
|
// Register Grafana dashboard.
|
||||||
|
add_action( 'wp_prometheus_register_dashboards', array( self::class, 'register_dashboards' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if metrics collection is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable metrics collection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function enable(): void {
|
||||||
|
update_option( self::OPTION_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable metrics collection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function disable(): void {
|
||||||
|
update_option( self::OPTION_ENABLED, 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect and register all BnB metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function collect_metrics( $collector ): void {
|
||||||
|
self::collect_inventory_metrics( $collector );
|
||||||
|
self::collect_booking_metrics( $collector );
|
||||||
|
self::collect_guest_metrics( $collector );
|
||||||
|
self::collect_occupancy_metrics( $collector );
|
||||||
|
self::collect_revenue_metrics( $collector );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect inventory metrics (buildings, rooms, services).
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_inventory_metrics( $collector ): void {
|
||||||
|
// Buildings total.
|
||||||
|
$buildings_total = wp_count_posts( Building::POST_TYPE );
|
||||||
|
$gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_buildings_total',
|
||||||
|
'Total number of buildings',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$gauge->set( (int) $buildings_total->publish, array() );
|
||||||
|
|
||||||
|
// Rooms by status.
|
||||||
|
$rooms_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_rooms_total',
|
||||||
|
'Total number of rooms by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$room_statuses = array( 'available', 'occupied', 'maintenance', 'inactive' );
|
||||||
|
foreach ( $room_statuses as $status ) {
|
||||||
|
$count = self::count_rooms_by_status( $status );
|
||||||
|
$rooms_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services by status.
|
||||||
|
$services_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_services_total',
|
||||||
|
'Total number of services by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$service_statuses = array( 'active', 'inactive' );
|
||||||
|
foreach ( $service_statuses as $status ) {
|
||||||
|
$count = self::count_services_by_status( $status );
|
||||||
|
$services_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect booking metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_booking_metrics( $collector ): void {
|
||||||
|
// Bookings by status.
|
||||||
|
$bookings_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_bookings_total',
|
||||||
|
'Total number of bookings by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$booking_statuses = array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' );
|
||||||
|
foreach ( $booking_statuses as $status ) {
|
||||||
|
$count = self::count_bookings_by_status( $status );
|
||||||
|
$bookings_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today's check-ins.
|
||||||
|
$checkins_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_checkins_today',
|
||||||
|
'Number of check-ins scheduled for today',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$checkins_gauge->set( self::count_todays_checkins(), array() );
|
||||||
|
|
||||||
|
// Today's check-outs.
|
||||||
|
$checkouts_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_checkouts_today',
|
||||||
|
'Number of check-outs scheduled for today',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$checkouts_gauge->set( self::count_todays_checkouts(), array() );
|
||||||
|
|
||||||
|
// Upcoming bookings (next 7 days).
|
||||||
|
$upcoming_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_bookings_upcoming_7days',
|
||||||
|
'Number of bookings starting in the next 7 days',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$upcoming_gauge->set( self::count_upcoming_bookings( 7 ), array() );
|
||||||
|
|
||||||
|
// Average booking duration (nights).
|
||||||
|
$avg_duration = $collector->register_gauge(
|
||||||
|
'wp_bnb_booking_avg_duration_nights',
|
||||||
|
'Average booking duration in nights',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$avg_duration->set( self::get_average_booking_duration(), array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect guest metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_guest_metrics( $collector ): void {
|
||||||
|
// Total guests.
|
||||||
|
$guests_total = wp_count_posts( Guest::POST_TYPE );
|
||||||
|
$guests_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_total',
|
||||||
|
'Total number of registered guests',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$guests_gauge->set( (int) $guests_total->publish, array() );
|
||||||
|
|
||||||
|
// Guests by status.
|
||||||
|
$guests_status_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_by_status',
|
||||||
|
'Number of guests by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$guest_statuses = array( 'active', 'blocked', 'vip' );
|
||||||
|
foreach ( $guest_statuses as $status ) {
|
||||||
|
$count = self::count_guests_by_status( $status );
|
||||||
|
$guests_status_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat guests (guests with more than one booking).
|
||||||
|
$repeat_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_repeat',
|
||||||
|
'Number of guests with more than one booking',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$repeat_gauge->set( self::count_repeat_guests(), array() );
|
||||||
|
|
||||||
|
// New guests this month.
|
||||||
|
$new_guests_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_new_this_month',
|
||||||
|
'Number of new guests registered this month',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$new_guests_gauge->set( self::count_new_guests_this_month(), array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect occupancy metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_occupancy_metrics( $collector ): void {
|
||||||
|
// Current occupancy rate (percentage).
|
||||||
|
$occupancy_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_occupancy_rate_current',
|
||||||
|
'Current room occupancy rate (percentage)',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$occupancy_gauge->set( self::get_current_occupancy_rate(), array() );
|
||||||
|
|
||||||
|
// Occupancy rate this month.
|
||||||
|
$occupancy_month_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_occupancy_rate_this_month',
|
||||||
|
'Room occupancy rate for the current month (percentage)',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$occupancy_month_gauge->set( self::get_monthly_occupancy_rate(), array() );
|
||||||
|
|
||||||
|
// Rooms currently occupied.
|
||||||
|
$occupied_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_rooms_currently_occupied',
|
||||||
|
'Number of rooms currently occupied',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$occupied_gauge->set( self::count_currently_occupied_rooms(), array() );
|
||||||
|
|
||||||
|
// Total room capacity (beds).
|
||||||
|
$capacity_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_total_capacity_beds',
|
||||||
|
'Total bed capacity across all rooms',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$capacity_gauge->set( self::get_total_bed_capacity(), array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect revenue metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_revenue_metrics( $collector ): void {
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
// Revenue this month.
|
||||||
|
$revenue_month_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_revenue_this_month',
|
||||||
|
'Total revenue for the current month',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$revenue_month_gauge->set( self::get_revenue_this_month(), array( $currency ) );
|
||||||
|
|
||||||
|
// Revenue year to date.
|
||||||
|
$revenue_ytd_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_revenue_ytd',
|
||||||
|
'Total revenue year to date',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$revenue_ytd_gauge->set( self::get_revenue_ytd(), array( $currency ) );
|
||||||
|
|
||||||
|
// Average booking value.
|
||||||
|
$avg_value_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_booking_avg_value',
|
||||||
|
'Average booking value',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$avg_value_gauge->set( self::get_average_booking_value(), array( $currency ) );
|
||||||
|
|
||||||
|
// Revenue from services this month.
|
||||||
|
$services_revenue_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_services_revenue_this_month',
|
||||||
|
'Revenue from additional services this month',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$services_revenue_gauge->set( self::get_services_revenue_this_month(), array( $currency ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Grafana dashboards.
|
||||||
|
*
|
||||||
|
* @param object $provider The wp-prometheus dashboard provider instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_dashboards( $provider ): void {
|
||||||
|
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
|
||||||
|
|
||||||
|
if ( file_exists( $dashboard_file ) ) {
|
||||||
|
$provider->register_dashboard(
|
||||||
|
'wp-bnb',
|
||||||
|
array(
|
||||||
|
'title' => __( 'WP BnB Dashboard', 'wp-bnb' ),
|
||||||
|
'description' => __( 'Monitor occupancy, bookings, revenue, and guest statistics for your B&B.', 'wp-bnb' ),
|
||||||
|
'icon' => 'dashicons-building',
|
||||||
|
'file' => $dashboard_file,
|
||||||
|
'plugin' => 'WP BnB Manager',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Inventory
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count rooms by status.
|
||||||
|
*
|
||||||
|
* @param string $status Room status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_rooms_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_room_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Room::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count services by status.
|
||||||
|
*
|
||||||
|
* @param string $status Service status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_services_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_service_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Service::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Bookings
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count bookings by status.
|
||||||
|
*
|
||||||
|
* @param string $status Booking status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_bookings_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count today's check-ins.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_todays_checkins(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_date.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_date.meta_value = %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'pending')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count today's check-outs.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_todays_checkouts(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_date.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_date.meta_value = %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value = 'checked_in'",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count upcoming bookings within given days.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to look ahead.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_upcoming_bookings( int $days ): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_date.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_date.meta_value >= %s
|
||||||
|
AND pm_date.meta_value <= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'pending')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today,
|
||||||
|
$end_date
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average booking duration in nights.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_average_booking_duration(): float {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT AVG(DATEDIFF(pm_out.meta_value, pm_in.meta_value))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value NOT IN ('cancelled')",
|
||||||
|
Booking::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Guests
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count guests by status.
|
||||||
|
*
|
||||||
|
* @param string $status Guest status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_guests_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_guest_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Guest::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count repeat guests (more than one booking).
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_repeat_guests(): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT pm.meta_value)
|
||||||
|
FROM {$wpdb->postmeta} pm
|
||||||
|
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_booking_guest_id'
|
||||||
|
AND pm.meta_value != ''
|
||||||
|
GROUP BY pm.meta_value
|
||||||
|
HAVING COUNT(*) > 1",
|
||||||
|
Booking::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count new guests registered this month.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_new_guests_this_month(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM {$wpdb->posts}
|
||||||
|
WHERE post_type = %s
|
||||||
|
AND post_status = 'publish'
|
||||||
|
AND post_date >= %s",
|
||||||
|
Guest::POST_TYPE,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Occupancy
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current occupancy rate (percentage of rooms occupied today).
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_current_occupancy_rate(): float {
|
||||||
|
$total_rooms = self::count_available_rooms();
|
||||||
|
if ( $total_rooms <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$occupied = self::count_currently_occupied_rooms();
|
||||||
|
return round( ( $occupied / $total_rooms ) * 100, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get monthly occupancy rate.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_monthly_occupancy_rate(): float {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$total_rooms = self::count_available_rooms();
|
||||||
|
if ( $total_rooms <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first_of_month = gmdate( 'Y-m-01' );
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
$days_so_far = (int) gmdate( 'd' );
|
||||||
|
$total_room_nights = $total_rooms * $days_so_far;
|
||||||
|
|
||||||
|
// Count booked nights this month (simplified: count bookings that overlap with this month).
|
||||||
|
$booked_nights = (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(
|
||||||
|
DATEDIFF(
|
||||||
|
LEAST(pm_out.meta_value, %s),
|
||||||
|
GREATEST(pm_in.meta_value, %s)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')
|
||||||
|
AND pm_in.meta_value <= %s
|
||||||
|
AND pm_out.meta_value >= %s",
|
||||||
|
$today,
|
||||||
|
$first_of_month,
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $total_room_nights <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round( ( $booked_nights / $total_room_nights ) * 100, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count currently occupied rooms.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_currently_occupied_rooms(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT pm_room.meta_value)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_room ON p.ID = pm_room.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_room.meta_key = '_bnb_booking_room_id'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in')
|
||||||
|
AND pm_in.meta_value <= %s
|
||||||
|
AND pm_out.meta_value > %s",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today,
|
||||||
|
$today
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count available rooms (not in maintenance/inactive).
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_available_rooms(): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_bnb_room_status'
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND (pm.meta_value IS NULL OR pm.meta_value IN ('available', 'occupied'))",
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total bed capacity.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function get_total_bed_capacity(): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm.meta_value AS UNSIGNED))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_room_beds'",
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (int) $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Revenue
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue for the current month.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_revenue_this_month(): float {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_month = gmdate( 'Y-m-01' );
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_in.meta_value >= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue year to date.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_revenue_ytd(): float {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_year = gmdate( 'Y-01-01' );
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_in.meta_value >= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$first_of_year
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average booking value.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_average_booking_value(): float {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT AVG(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue from services this month.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_services_revenue_this_month(): float {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_month = gmdate( 'Y-m-01' );
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm_services.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_services ON p.ID = pm_services.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_services.meta_key = '_bnb_booking_services_total'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_in.meta_value >= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
576
src/Plugin.php
576
src/Plugin.php
@@ -10,12 +10,17 @@ declare( strict_types=1 );
|
|||||||
namespace Magdev\WpBnb;
|
namespace Magdev\WpBnb;
|
||||||
|
|
||||||
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||||
|
use Magdev\WpBnb\Admin\Dashboard as DashboardAdmin;
|
||||||
|
use Magdev\WpBnb\Admin\Reports as ReportsAdmin;
|
||||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
use Magdev\WpBnb\Blocks\BlockRegistrar;
|
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\Search;
|
||||||
use Magdev\WpBnb\Frontend\Shortcodes;
|
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||||
|
use Magdev\WpBnb\Api\RestApi;
|
||||||
|
use Magdev\WpBnb\Integration\CF7;
|
||||||
|
use Magdev\WpBnb\Integration\Prometheus;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||||
@@ -133,6 +138,18 @@ final class Plugin {
|
|||||||
// Initialize auto-updater (requires license configuration).
|
// Initialize auto-updater (requires license configuration).
|
||||||
$this->init_updater();
|
$this->init_updater();
|
||||||
|
|
||||||
|
// Initialize Contact Form 7 integration if CF7 is active.
|
||||||
|
// This runs in both admin (for tag generators) and frontend (for form rendering).
|
||||||
|
if ( class_exists( 'WPCF7' ) ) {
|
||||||
|
CF7::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Prometheus metrics integration.
|
||||||
|
Prometheus::init();
|
||||||
|
|
||||||
|
// Initialize REST API.
|
||||||
|
$this->init_rest_api();
|
||||||
|
|
||||||
// Initialize admin components.
|
// Initialize admin components.
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
$this->init_admin();
|
$this->init_admin();
|
||||||
@@ -157,6 +174,16 @@ final class Plugin {
|
|||||||
$updater->init();
|
$updater->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the REST API.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_rest_api(): void {
|
||||||
|
$api = new RestApi();
|
||||||
|
$api->init();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize admin components.
|
* Initialize admin components.
|
||||||
*
|
*
|
||||||
@@ -241,6 +268,7 @@ final class Plugin {
|
|||||||
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
||||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
||||||
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
||||||
|
$is_dashboard = 'toplevel_page_wp-bnb' === $hook_suffix;
|
||||||
|
|
||||||
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
||||||
return;
|
return;
|
||||||
@@ -261,6 +289,18 @@ final class Plugin {
|
|||||||
$script_deps[] = 'jquery-ui-sortable';
|
$script_deps[] = 'jquery-ui-sortable';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Chart.js for dashboard.
|
||||||
|
if ( $is_dashboard ) {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'chartjs',
|
||||||
|
'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js',
|
||||||
|
array(),
|
||||||
|
'4.4.1',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$script_deps[] = 'chartjs';
|
||||||
|
}
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wp-bnb-admin',
|
'wp-bnb-admin',
|
||||||
WP_BNB_URL . 'assets/js/admin.js',
|
WP_BNB_URL . 'assets/js/admin.js',
|
||||||
@@ -269,43 +309,53 @@ final class Plugin {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_localize_script(
|
// Build localize data.
|
||||||
'wp-bnb-admin',
|
$localize_data = array(
|
||||||
'wpBnbAdmin',
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
array(
|
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
'postType' => $post_type,
|
||||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
'isDashboard' => $is_dashboard,
|
||||||
'postType' => $post_type,
|
'i18n' => array(
|
||||||
'i18n' => array(
|
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
'increase' => __( 'increase', 'wp-bnb' ),
|
||||||
'increase' => __( 'increase', 'wp-bnb' ),
|
'discount' => __( 'discount', 'wp-bnb' ),
|
||||||
'discount' => __( 'discount', 'wp-bnb' ),
|
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||||
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||||
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
'available' => __( 'Available', 'wp-bnb' ),
|
||||||
'available' => __( 'Available', 'wp-bnb' ),
|
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||||
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||||
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
'nights' => __( 'nights', 'wp-bnb' ),
|
'night' => __( 'night', 'wp-bnb' ),
|
||||||
'night' => __( 'night', 'wp-bnb' ),
|
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||||
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
|
||||||
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
|
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
||||||
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
||||||
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
||||||
'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' ),
|
||||||
'justNow' => __( 'Just now', 'wp-bnb' ),
|
'updateAvailable' => __( 'Update available!', 'wp-bnb' ),
|
||||||
'updateAvailable' => __( 'Update available!', 'wp-bnb' ),
|
'upToDate' => __( '(You are up to date)', 'wp-bnb' ),
|
||||||
'upToDate' => __( '(You are up to date)', 'wp-bnb' ),
|
'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ),
|
||||||
'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ),
|
'occupancy' => __( 'Occupancy %', 'wp-bnb' ),
|
||||||
),
|
'revenue' => __( 'Revenue', 'wp-bnb' ),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add chart data for dashboard.
|
||||||
|
if ( $is_dashboard ) {
|
||||||
|
$localize_data['chartData'] = array(
|
||||||
|
'occupancy' => DashboardAdmin::get_occupancy_trend_data( 30 ),
|
||||||
|
'revenue' => DashboardAdmin::get_revenue_trend_data( 6 ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_localize_script( 'wp-bnb-admin', 'wpBnbAdmin', $localize_data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,6 +412,43 @@ final class Plugin {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -404,6 +491,16 @@ final class Plugin {
|
|||||||
array( $this, 'render_dashboard_page' )
|
array( $this, 'render_dashboard_page' )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reports submenu.
|
||||||
|
add_submenu_page(
|
||||||
|
'wp-bnb',
|
||||||
|
__( 'Reports', 'wp-bnb' ),
|
||||||
|
__( 'Reports', 'wp-bnb' ),
|
||||||
|
'manage_options',
|
||||||
|
'wp-bnb-reports',
|
||||||
|
array( $this, 'render_reports_page' )
|
||||||
|
);
|
||||||
|
|
||||||
// Settings submenu.
|
// Settings submenu.
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'wp-bnb',
|
'wp-bnb',
|
||||||
@@ -432,15 +529,16 @@ final class Plugin {
|
|||||||
|
|
||||||
// Define the desired order of menu slugs.
|
// Define the desired order of menu slugs.
|
||||||
$desired_order = array(
|
$desired_order = array(
|
||||||
'wp-bnb', // Dashboard.
|
'wp-bnb', // Dashboard.
|
||||||
'edit.php?post_type=bnb_building', // Buildings.
|
'edit.php?post_type=bnb_building', // Buildings.
|
||||||
'edit.php?post_type=bnb_room', // Rooms.
|
'edit.php?post_type=bnb_room', // Rooms.
|
||||||
'edit.php?post_type=bnb_booking', // Bookings.
|
'edit.php?post_type=bnb_booking', // Bookings.
|
||||||
'edit.php?post_type=bnb_guest', // Guests.
|
'edit.php?post_type=bnb_guest', // Guests.
|
||||||
'edit.php?post_type=bnb_service', // Services.
|
'edit.php?post_type=bnb_service', // Services.
|
||||||
'wp-bnb-calendar', // Calendar.
|
'wp-bnb-calendar', // Calendar.
|
||||||
'wp-bnb-seasons', // Seasons.
|
'wp-bnb-reports', // Reports.
|
||||||
'wp-bnb-settings', // Settings (always last).
|
'wp-bnb-seasons', // Seasons.
|
||||||
|
'wp-bnb-settings', // Settings (always last).
|
||||||
);
|
);
|
||||||
|
|
||||||
$current_menu = $submenu['wp-bnb'];
|
$current_menu = $submenu['wp-bnb'];
|
||||||
@@ -484,39 +582,16 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function render_dashboard_page(): void {
|
public function render_dashboard_page(): void {
|
||||||
$license_valid = LicenseManager::is_license_valid();
|
DashboardAdmin::render();
|
||||||
$is_localhost = LicenseManager::is_localhost();
|
}
|
||||||
?>
|
|
||||||
<div class="wrap">
|
|
||||||
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
|
||||||
|
|
||||||
<?php if ( $is_localhost ) : ?>
|
/**
|
||||||
<div class="notice notice-info">
|
* Render reports page.
|
||||||
<p>
|
*
|
||||||
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
* @return void
|
||||||
<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' ); ?>
|
public function render_reports_page(): void {
|
||||||
</p>
|
ReportsAdmin::render();
|
||||||
</div>
|
|
||||||
<?php elseif ( ! $license_valid ) : ?>
|
|
||||||
<div class="notice notice-warning">
|
|
||||||
<p>
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %s: Link to settings page */
|
|
||||||
esc_html__( 'Your license is not active. Please %s to unlock all features.', 'wp-bnb' ),
|
|
||||||
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ) . '">' . esc_html__( 'activate your license', 'wp-bnb' ) . '</a>'
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="wp-bnb-dashboard">
|
|
||||||
<p><?php esc_html_e( 'Welcome to WP BnB Management. Use the menu on the left to manage your buildings, rooms, bookings, and guests.', 'wp-bnb' ); ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -553,6 +628,14 @@ final class Plugin {
|
|||||||
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=metrics' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'metrics' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'Metrics', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=api' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'api' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'API', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -567,6 +650,12 @@ final class Plugin {
|
|||||||
case 'updates':
|
case 'updates':
|
||||||
$this->render_updates_settings();
|
$this->render_updates_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'metrics':
|
||||||
|
$this->render_metrics_settings();
|
||||||
|
break;
|
||||||
|
case 'api':
|
||||||
|
$this->render_api_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->render_general_settings();
|
$this->render_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1208,6 +1297,314 @@ final class Plugin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render metrics settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_metrics_settings(): void {
|
||||||
|
$metrics_enabled = Prometheus::is_enabled();
|
||||||
|
$prometheus_active = class_exists( '\Magdev\WpPrometheus\Plugin' ) || defined( 'WP_PROMETHEUS_VERSION' );
|
||||||
|
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
|
||||||
|
$dashboard_available = file_exists( $dashboard_file );
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Prometheus Metrics', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( ! $prometheus_active ) : ?>
|
||||||
|
<div class="notice notice-warning inline" style="margin: 0 0 20px 0;">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
|
||||||
|
<strong><?php esc_html_e( 'WP Prometheus not detected', 'wp-bnb' ); ?></strong><br>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Plugin URL */
|
||||||
|
esc_html__( 'The WP Prometheus plugin is required to expose metrics. Please install and activate it from %s.', 'wp-bnb' ),
|
||||||
|
'<a href="https://src.bundespruefstelle.ch/magdev/wp-prometheus" target="_blank">wp-prometheus</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="notice notice-success inline" style="margin: 0 0 20px 0;">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
|
||||||
|
<strong><?php esc_html_e( 'WP Prometheus is active', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'Metrics will be exposed via the /metrics/ endpoint.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable Metrics', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_metrics_enabled"
|
||||||
|
value="yes" <?php checked( $metrics_enabled ); ?>>
|
||||||
|
<?php esc_html_e( 'Expose BnB metrics via Prometheus', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, occupancy, booking, revenue, and guest metrics will be available for Prometheus scraping.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Available Metrics', 'wp-bnb' ); ?></h2>
|
||||||
|
<p class="description"><?php esc_html_e( 'The following metrics are exposed when enabled:', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<table class="widefat striped" style="max-width: 800px; margin-top: 10px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Metric', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Type', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_buildings_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total number of buildings', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_rooms_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total rooms by status (available, occupied, maintenance, inactive)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_bookings_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total bookings by status (pending, confirmed, checked_in, checked_out, cancelled)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_occupancy_rate_current</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Current room occupancy rate (percentage)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_occupancy_rate_this_month</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Room occupancy rate for current month (percentage)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_checkins_today</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Number of check-ins scheduled for today', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_checkouts_today</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Number of check-outs scheduled for today', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_revenue_this_month</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total revenue for current month', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_revenue_ytd</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total revenue year to date', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_booking_avg_value</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Average booking value', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_guests_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total number of registered guests', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_guests_repeat</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Number of repeat guests (more than one booking)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Grafana Dashboard', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( $dashboard_available ) : ?>
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e( 'A pre-configured Grafana dashboard is available for visualizing WP BnB metrics.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'Dashboard file:', 'wp-bnb' ); ?></strong>
|
||||||
|
<code>assets/grafana/wp-bnb-dashboard.json</code>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'If WP Prometheus is installed, this dashboard will be automatically registered and available for export in the Prometheus settings.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="notice notice-warning inline">
|
||||||
|
<p><?php esc_html_e( 'Grafana dashboard file not found.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<?php submit_button( __( 'Save Metrics Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render API settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_api_settings(): void {
|
||||||
|
$api_enabled = get_option( 'wp_bnb_api_enabled', 'yes' );
|
||||||
|
$rate_limiting = get_option( 'wp_bnb_api_rate_limiting', 'yes' );
|
||||||
|
$api_base_url = rest_url( RestApi::NAMESPACE );
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'REST API Settings', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable API', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_api_enabled" value="yes" <?php checked( $api_enabled, 'yes' ); ?>>
|
||||||
|
<?php esc_html_e( 'Enable the REST API endpoints', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, external applications can access room, availability, and booking data via the API.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Rate Limiting', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_api_rate_limiting" value="yes" <?php checked( $rate_limiting, 'yes' ); ?>>
|
||||||
|
<?php esc_html_e( 'Enable rate limiting', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Limits API requests to prevent abuse. Recommended for production sites.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'API Information', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Base URL', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<code><?php echo esc_html( $api_base_url ); ?></code>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'All API endpoints are prefixed with this URL.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'API Version', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<code><?php echo esc_html( RestApi::VERSION ); ?></code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Info Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<code><?php echo esc_html( $api_base_url . '/info' ); ?></code>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Returns API information and available endpoints.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Available Endpoints', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e( 'Public Endpoints', 'wp-bnb' ); ?></h3>
|
||||||
|
<table class="widefat" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>GET</td><td><code>/buildings</code></td><td><?php esc_html_e( 'List all buildings', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/buildings/{id}</code></td><td><?php esc_html_e( 'Get building details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/buildings/{id}/rooms</code></td><td><?php esc_html_e( 'List rooms in building', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms</code></td><td><?php esc_html_e( 'List/search rooms', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms/{id}</code></td><td><?php esc_html_e( 'Get room details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms/{id}/availability</code></td><td><?php esc_html_e( 'Check room availability', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms/{id}/calendar</code></td><td><?php esc_html_e( 'Get room calendar', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/availability/search</code></td><td><?php esc_html_e( 'Search available rooms', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/services</code></td><td><?php esc_html_e( 'List services', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/pricing/calculate</code></td><td><?php esc_html_e( 'Calculate booking price', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings</code></td><td><?php esc_html_e( 'Create booking (pending status)', 'wp-bnb' ); ?></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e( 'Admin Endpoints (Requires Authentication)', 'wp-bnb' ); ?></h3>
|
||||||
|
<table class="widefat" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>GET</td><td><code>/bookings</code></td><td><?php esc_html_e( 'List all bookings', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Get booking details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>PATCH</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Update booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>DELETE</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Cancel booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings/{id}/confirm</code></td><td><?php esc_html_e( 'Confirm booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings/{id}/check-in</code></td><td><?php esc_html_e( 'Check in guest', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings/{id}/check-out</code></td><td><?php esc_html_e( 'Check out guest', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/guests</code></td><td><?php esc_html_e( 'List guests', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/guests/{id}</code></td><td><?php esc_html_e( 'Get guest details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/guests/search</code></td><td><?php esc_html_e( 'Search guests', 'wp-bnb' ); ?></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Authentication', 'wp-bnb' ); ?></h2>
|
||||||
|
<p><?php esc_html_e( 'Admin endpoints require authentication. Use one of the following methods:', 'wp-bnb' ); ?></p>
|
||||||
|
<ul style="list-style: disc; margin-left: 20px;">
|
||||||
|
<li><strong><?php esc_html_e( 'Application Passwords:', 'wp-bnb' ); ?></strong> <?php esc_html_e( 'Create one in Users > Your Profile. Use Basic Auth with username and app password.', 'wp-bnb' ); ?></li>
|
||||||
|
<li><strong><?php esc_html_e( 'Cookie + Nonce:', 'wp-bnb' ); ?></strong> <?php esc_html_e( 'For same-domain requests. Pass nonce in X-WP-Nonce header.', 'wp-bnb' ); ?></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Rate Limits', 'wp-bnb' ); ?></h2>
|
||||||
|
<table class="widefat" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Type', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Limit', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Applies To', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><?php esc_html_e( 'Public', 'wp-bnb' ); ?></td><td>60/min</td><td><?php esc_html_e( 'GET rooms, buildings, services', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><?php esc_html_e( 'Availability', 'wp-bnb' ); ?></td><td>30/min</td><td><?php esc_html_e( 'Availability checks, calendar', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><?php esc_html_e( 'Booking', 'wp-bnb' ); ?></td><td>10/min</td><td><?php esc_html_e( 'Booking creation', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><?php esc_html_e( 'Admin', 'wp-bnb' ); ?></td><td>120/min</td><td><?php esc_html_e( 'All admin endpoints', 'wp-bnb' ); ?></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<?php submit_button( __( 'Save API Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render license status badge.
|
* Render license status badge.
|
||||||
*
|
*
|
||||||
@@ -1279,6 +1676,12 @@ final class Plugin {
|
|||||||
case 'updates':
|
case 'updates':
|
||||||
$this->save_updates_settings();
|
$this->save_updates_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'metrics':
|
||||||
|
$this->save_metrics_settings();
|
||||||
|
break;
|
||||||
|
case 'api':
|
||||||
|
$this->save_api_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->save_general_settings();
|
$this->save_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1408,6 +1811,35 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save metrics settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_metrics_settings(): void {
|
||||||
|
$metrics_enabled = isset( $_POST['wp_bnb_metrics_enabled'] ) ? 'yes' : 'no';
|
||||||
|
update_option( Prometheus::OPTION_ENABLED, $metrics_enabled );
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Metrics settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save API settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_api_settings(): void {
|
||||||
|
$api_enabled = isset( $_POST['wp_bnb_api_enabled'] ) ? 'yes' : 'no';
|
||||||
|
$rate_limiting = isset( $_POST['wp_bnb_api_rate_limiting'] ) ? 'yes' : 'no';
|
||||||
|
|
||||||
|
update_option( 'wp_bnb_api_enabled', $api_enabled );
|
||||||
|
update_option( 'wp_bnb_api_rate_limiting', $rate_limiting );
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'API settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for checking room availability.
|
* AJAX handler for checking room availability.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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.6.1
|
* Version: 0.10.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 8.3
|
* Requires PHP: 8.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin version constant - MUST match Version in header above.
|
// Plugin version constant - MUST match Version in header above.
|
||||||
define( 'WP_BNB_VERSION', '0.6.1' );
|
define( 'WP_BNB_VERSION', '0.10.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user