Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6d7eeb5ec | |||
| b137fec4fb | |||
| 992d961066 | |||
| be6d9d68b5 | |||
| a784d92cc9 | |||
| f61dca5f45 | |||
| 28350aabfa | |||
| 3579904bad | |||
| 602549208f | |||
| 45a73e15aa | |||
| 13ba264431 | |||
| c17dd53c5a | |||
| be2735a3bd |
168
CHANGELOG.md
168
CHANGELOG.md
@@ -5,6 +5,173 @@ 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.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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Auto-Update System:
|
||||||
|
- New `src/License/Updater.php` class for WordPress update integration
|
||||||
|
- Hooks into `pre_set_site_transient_update_plugins` for update detection
|
||||||
|
- Plugin info modal via `plugins_api` filter
|
||||||
|
- Configurable update check frequency (1-168 hours)
|
||||||
|
- Option to enable/disable update notifications
|
||||||
|
- Option to enable/disable automatic updates
|
||||||
|
- AJAX endpoint for manual update check
|
||||||
|
- Automatic cache clearing when license settings change
|
||||||
|
- Updates Tab in Settings:
|
||||||
|
- Enable/disable update notifications toggle
|
||||||
|
- Enable/disable automatic updates toggle
|
||||||
|
- Update check frequency setting
|
||||||
|
- Manual "Check for Updates" button
|
||||||
|
- Display of last check timestamp and current version
|
||||||
|
- Localhost Development Mode:
|
||||||
|
- License bypass for local development environments
|
||||||
|
- Detects: localhost, 127.0.0.1, ::1, .local/.test/.localhost/.dev/.ddev.site domains
|
||||||
|
- Private IP range detection (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
|
||||||
|
- "Development Mode" notice on Dashboard and License settings page
|
||||||
|
- Extended General Settings:
|
||||||
|
- Business address fields (street, city, postal code, country)
|
||||||
|
- Contact fields (email, phone, website)
|
||||||
|
- Social media fields (Facebook, Instagram, X/Twitter, LinkedIn, TripAdvisor)
|
||||||
|
- Pricing Settings Subtabs:
|
||||||
|
- Split into three subtabs: Pricing Tiers, Weekend Days, Seasons
|
||||||
|
- Each subtab has its own save button
|
||||||
|
- Seasons subtab shows priority column and link to Seasons Manager
|
||||||
|
- Guest Data Encryption:
|
||||||
|
- AES-256-CBC encryption for sensitive data (ID/passport numbers)
|
||||||
|
- Uses WordPress AUTH_KEY for encryption key derivation
|
||||||
|
- `encrypt()` and `decrypt()` methods in Guest class
|
||||||
|
- Backward compatible with legacy unencrypted data
|
||||||
|
- Security notice displayed in Identification meta box
|
||||||
|
- Guest Auto-Creation from Booking:
|
||||||
|
- When new guest data is entered in booking form, guest record is automatically created
|
||||||
|
- Links booking to the new guest via guest_id meta
|
||||||
|
- Prevents duplicate guest entries
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Admin submenu reordered for better organization:
|
||||||
|
- Dashboard at top, Settings at bottom
|
||||||
|
- Logical grouping: Buildings, Rooms, Bookings, Guests, Services, Calendar, Seasons
|
||||||
|
- Booking title auto-generates with guest name and dates (room number removed)
|
||||||
|
- Disabled Gutenberg block editor for form-based post types:
|
||||||
|
- Service, Guest, and Booking now use classic editor
|
||||||
|
- Meta boxes display properly instead of being hidden at bottom
|
||||||
|
- Form-based interfaces more appropriate than block editor for data entry
|
||||||
|
- Settings tabs now flush with tab content (no gap)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed Booking admin issues with auto-draft status causing type errors
|
||||||
|
- Fixed guest dropdown to always load existing guests
|
||||||
|
- Fixed booking history display on Guest edit page
|
||||||
|
- Fixed service pricing meta box not displaying radio buttons (Gutenberg hiding meta boxes)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Guest ID/passport numbers encrypted at rest using AES-256-CBC
|
||||||
|
- Random IV generation for each encryption operation
|
||||||
|
- Secure key derivation from WordPress AUTH_KEY
|
||||||
|
|
||||||
## [0.6.0] - 2026-02-02
|
## [0.6.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -358,6 +525,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Input sanitization and output escaping
|
- Input sanitization and output escaping
|
||||||
- Server secret masking in license settings
|
- Server secret masking in license settings
|
||||||
|
|
||||||
|
[0.6.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.1
|
||||||
[0.6.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.0
|
[0.6.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.0
|
||||||
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
||||||
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
||||||
|
|||||||
383
CLAUDE.md
383
CLAUDE.md
@@ -1,4 +1,4 @@
|
|||||||
# WordPress BnB Management
|
# WordPress BnB Manager
|
||||||
|
|
||||||
**Author:** Marco Graetsch
|
**Author:** Marco Graetsch
|
||||||
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
|
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
|
||||||
@@ -38,6 +38,10 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||||
|
|
||||||
|
### Known Bugs
|
||||||
|
|
||||||
|
(none)
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **Language:** PHP 8.3.x
|
||||||
@@ -234,9 +238,18 @@ wp-bnb/
|
|||||||
│ ├── Admin/ # Admin pages
|
│ ├── Admin/ # Admin pages
|
||||||
│ │ ├── Calendar.php # Availability calendar page
|
│ │ ├── Calendar.php # Availability calendar page
|
||||||
│ │ └── Seasons.php # Seasons management page
|
│ │ └── Seasons.php # Seasons management page
|
||||||
|
│ ├── Blocks/ # Gutenberg blocks
|
||||||
|
│ │ └── BlockRegistrar.php # Block registration and rendering
|
||||||
│ ├── Booking/ # Booking system
|
│ ├── Booking/ # Booking system
|
||||||
│ │ ├── Availability.php # Availability checking
|
│ │ ├── Availability.php # Availability checking
|
||||||
│ │ └── EmailNotifier.php # Email notifications
|
│ │ └── EmailNotifier.php # Email notifications
|
||||||
|
│ ├── Frontend/ # Frontend components
|
||||||
|
│ │ ├── Search.php # Room search and AJAX handlers
|
||||||
|
│ │ ├── Shortcodes.php # All shortcode handlers
|
||||||
|
│ │ └── Widgets/ # WordPress widgets
|
||||||
|
│ │ ├── AvailabilityCalendar.php
|
||||||
|
│ │ ├── BuildingRooms.php
|
||||||
|
│ │ └── SimilarRooms.php
|
||||||
│ ├── License/
|
│ ├── License/
|
||||||
│ │ └── Manager.php # License management
|
│ │ └── Manager.php # License management
|
||||||
│ ├── PostTypes/ # Custom post types
|
│ ├── PostTypes/ # Custom post types
|
||||||
@@ -247,6 +260,8 @@ wp-bnb/
|
|||||||
│ │ ├── Calculator.php # Price calculation
|
│ │ ├── Calculator.php # Price calculation
|
||||||
│ │ ├── PricingTier.php # Pricing tier enum
|
│ │ ├── PricingTier.php # Pricing tier enum
|
||||||
│ │ └── Season.php # Seasonal pricing
|
│ │ └── Season.php # Seasonal pricing
|
||||||
|
│ ├── Integration/ # Third-party integrations
|
||||||
|
│ │ └── CF7.php # Contact Form 7 integration
|
||||||
│ └── Taxonomies/ # Custom taxonomies
|
│ └── Taxonomies/ # Custom taxonomies
|
||||||
│ ├── Amenity.php # Amenity taxonomy (tags)
|
│ ├── Amenity.php # Amenity taxonomy (tags)
|
||||||
│ └── RoomType.php # Room type taxonomy (categories)
|
│ └── RoomType.php # Room type taxonomy (categories)
|
||||||
@@ -256,10 +271,14 @@ wp-bnb/
|
|||||||
├── assets/
|
├── assets/
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
│ │ ├── admin.css # Admin styles
|
│ │ ├── admin.css # Admin styles
|
||||||
│ │ └── frontend.css # Frontend styles
|
│ │ ├── blocks-editor.css # Gutenberg editor styles
|
||||||
|
│ │ ├── cf7-integration.css # CF7 form styles
|
||||||
|
│ │ └── frontend.css # Frontend styles (~1250 lines)
|
||||||
│ └── js/
|
│ └── js/
|
||||||
│ ├── admin.js # Admin scripts
|
│ ├── admin.js # Admin scripts
|
||||||
│ └── frontend.js # Frontend scripts
|
│ ├── blocks-editor.js # Gutenberg editor scripts
|
||||||
|
│ ├── cf7-integration.js # CF7 form scripts
|
||||||
|
│ └── frontend.js # Frontend scripts (~825 lines)
|
||||||
├── templates/ # Twig templates (future)
|
├── templates/ # Twig templates (future)
|
||||||
├── languages/ # Translation files (future)
|
├── languages/ # Translation files (future)
|
||||||
└── releases/ # Release packages (git-ignored)
|
└── releases/ # Release packages (git-ignored)
|
||||||
@@ -323,7 +342,7 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Implemented license settings page with validation/activation buttons
|
- Implemented license settings page with validation/activation buttons
|
||||||
- Created admin CSS and JavaScript for license management
|
- Created admin CSS and JavaScript for license management
|
||||||
- Created Gitea CI/CD pipeline at `.gitea/workflows/release.yml`
|
- Created Gitea CI/CD pipeline at `.gitea/workflows/release.yml`
|
||||||
- Created `PLAN.md` with full implementation roadmap (8 phases)
|
- Created `PLAN.md` with full implementation roadmap (10 phases)
|
||||||
- Created `README.md` with user documentation
|
- Created `README.md` with user documentation
|
||||||
- Created `CHANGELOG.md` following Keep a Changelog format
|
- Created `CHANGELOG.md` following Keep a Changelog format
|
||||||
- Updated `CLAUDE.md` with architecture details
|
- Updated `CLAUDE.md` with architecture details
|
||||||
@@ -560,3 +579,359 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Same namespace classes can reference each other directly without use statements
|
- Same namespace classes can reference each other directly without use statements
|
||||||
- Services meta box renders before pricing meta box so services total is available
|
- Services meta box renders before pricing meta box so services total is available
|
||||||
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
|
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
|
||||||
|
|
||||||
|
### 2026-02-02 - Version 0.6.0 (Frontend Features)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Frontend/Search.php` class
|
||||||
|
- Room search with multiple filters: availability, capacity, room type, amenities, price range, building
|
||||||
|
- AJAX endpoints: `wp_bnb_search_rooms`, `wp_bnb_get_availability`, `wp_bnb_get_calendar`, `wp_bnb_calculate_price`
|
||||||
|
- Pagination support with configurable per_page
|
||||||
|
- Room data formatting for JSON responses with thumbnails, pricing, amenities
|
||||||
|
- Price range filtering using Calculator integration
|
||||||
|
- Availability filtering using Availability class
|
||||||
|
- Created `src/Frontend/Shortcodes.php` class
|
||||||
|
- `[bnb_buildings]` - Buildings list/grid with layout, columns, limit, orderby options
|
||||||
|
- `[bnb_rooms]` - Rooms list/grid with building, room_type, amenities filters
|
||||||
|
- `[bnb_room_search]` - Interactive search form with results container
|
||||||
|
- `[bnb_building id="X"]` - Single building display with rooms
|
||||||
|
- `[bnb_room id="X"]` - Single room display with availability form
|
||||||
|
- Grid system with 1-4 column support
|
||||||
|
- Sorting options: title, date, price, capacity
|
||||||
|
- Created `src/Frontend/Widgets/` directory with three widgets
|
||||||
|
- `SimilarRooms.php` - Shows rooms from same building/room type
|
||||||
|
- `BuildingRooms.php` - Lists all rooms in a building
|
||||||
|
- `AvailabilityCalendar.php` - Mini calendar with booking status
|
||||||
|
- All widgets extend `WP_Widget` with form/update/widget methods
|
||||||
|
- Auto-detection of current building/room from page context
|
||||||
|
- Created `src/Blocks/BlockRegistrar.php` class
|
||||||
|
- Five Gutenberg blocks: Building, Room, Room Search, Buildings List, Rooms List
|
||||||
|
- Server-side rendering using shortcode system
|
||||||
|
- Block editor assets (CSS/JS) enqueuing
|
||||||
|
- Block data localization with buildings, rooms, room types, amenities
|
||||||
|
- `render_callback` functions for each block type
|
||||||
|
- Created `assets/js/blocks-editor.js`
|
||||||
|
- Block registration using `wp.blocks.registerBlockType`
|
||||||
|
- InspectorControls for sidebar settings panels
|
||||||
|
- ServerSideRender for live preview in editor
|
||||||
|
- Attribute definitions matching shortcode parameters
|
||||||
|
- Created `assets/css/blocks-editor.css`
|
||||||
|
- Minimal editor styling for block placeholders
|
||||||
|
- Preview container styling
|
||||||
|
- Updated `assets/css/frontend.css` (~1250 lines)
|
||||||
|
- CSS custom properties for theming (colors, spacing, border-radius)
|
||||||
|
- Building and room card components
|
||||||
|
- Search form with field groups
|
||||||
|
- Results grid with responsive columns
|
||||||
|
- Calendar widget with availability states (available, booked, past, today)
|
||||||
|
- Legend styling
|
||||||
|
- Responsive breakpoints: 480px, 768px, 1024px
|
||||||
|
- Updated `assets/js/frontend.js` (~825 lines)
|
||||||
|
- `WpBnb` namespace with utility methods (ajax, formatDate, parseDate, debounce)
|
||||||
|
- `SearchForm` class: form submission, date validation, results rendering, load more
|
||||||
|
- `CalendarWidget` class: month navigation, AJAX calendar loading
|
||||||
|
- `AvailabilityForm` class: availability checking on single room pages
|
||||||
|
- `PriceCalculator` class: real-time price calculation with breakdown
|
||||||
|
- XSS-safe DOM construction using textContent instead of innerHTML
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added use statements for new frontend classes
|
||||||
|
- `init_frontend()` initializes Search, Shortcodes, BlockRegistrar
|
||||||
|
- `register_widgets()` method for widget registration
|
||||||
|
- `wp_localize_script()` adds AJAX URL, nonce, i18n strings to frontend
|
||||||
|
- Updated version to 0.6.0 in both plugin header and constant
|
||||||
|
- Updated CHANGELOG.md with comprehensive v0.6.0 release notes
|
||||||
|
- Updated PLAN.md to mark Phase 6 complete
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Server-side rendered Gutenberg blocks avoid complex build processes and ensure PHP/JS output consistency
|
||||||
|
- Shortcode system works well as render backend for blocks via `render_callback`
|
||||||
|
- Widget auto-detection from page context (`is_singular()`, `get_the_ID()`) reduces configuration
|
||||||
|
- CSS custom properties enable easy theming without modifying core styles
|
||||||
|
- AJAX nonce verification requires `wp_ajax_nopriv_` for non-logged-in users in frontend search
|
||||||
|
- Calendar data from `Availability::get_calendar_data()` provides consistent format for PHP and JS rendering
|
||||||
|
- XSS prevention in JS: use `textContent` for user data, `createElement` for structure
|
||||||
|
- Frontend components require license check (`LicenseManager::is_license_valid()`) before initialization
|
||||||
|
- Block editor requires separate script handle from frontend to avoid conflicts
|
||||||
|
|
||||||
|
**Released:**
|
||||||
|
|
||||||
|
- Committed: `864b8b2` on dev branch
|
||||||
|
- Merged to main (fast-forward)
|
||||||
|
- Tagged: `v0.6.0`
|
||||||
|
- Pushed to origin: dev, main, v0.6.0
|
||||||
|
|
||||||
|
### 2026-02-03 - Bug Fixes and Enhancements
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Fixed gap between settings page tabs and tab content
|
||||||
|
- Changed `.nav-tab-wrapper` margin-bottom from 20px to 0
|
||||||
|
- Added explicit border-bottom to create seamless connection with tab content
|
||||||
|
- Added license bypass for localhost development environments
|
||||||
|
- Created `LicenseManager::is_localhost()` method
|
||||||
|
- Detects: localhost, 127.0.0.1, ::1, .local/.test/.localhost/.dev/.ddev.site domains, private IP ranges
|
||||||
|
- `is_license_valid()` now returns true for localhost environments
|
||||||
|
- Added "Development Mode" notice on license settings page and dashboard when localhost detected
|
||||||
|
- Expanded General Settings with business owner fields
|
||||||
|
- Added Address section: street, city, postal code, country
|
||||||
|
- Added Contact section: email, phone, website
|
||||||
|
- Added Social Media section: Facebook, Instagram, X (Twitter), LinkedIn, TripAdvisor
|
||||||
|
- Updated `save_general_settings()` with proper sanitization for all new fields
|
||||||
|
- Created subtabs on Pricing settings tab
|
||||||
|
- Three subtabs: Pricing Tiers, Weekend Days, Seasons
|
||||||
|
- Each subtab has its own save button and focused content
|
||||||
|
- Added CSS for subtab navigation styling
|
||||||
|
- Seasons subtab now shows priority column and direct link to Seasons Manager
|
||||||
|
- Implemented auto-updates system
|
||||||
|
- Created `src/License/Updater.php` class
|
||||||
|
- Integrates with WordPress plugin update system via `pre_set_site_transient_update_plugins`
|
||||||
|
- Provides plugin info for "View details" modal via `plugins_api` filter
|
||||||
|
- Uses license client's `checkForUpdates()` method
|
||||||
|
- Configurable check frequency (1-168 hours)
|
||||||
|
- Options for notifications enabled and auto-install enabled
|
||||||
|
- Automatic cache clearing when license settings change or after updates
|
||||||
|
- Added Updates tab to settings page
|
||||||
|
- Enable/disable update notifications
|
||||||
|
- Enable/disable automatic updates
|
||||||
|
- Configurable update check frequency
|
||||||
|
- Manual "Check for Updates" button with AJAX
|
||||||
|
- Display of last check timestamp and current version
|
||||||
|
- Reordered admin submenu for better organization
|
||||||
|
- Dashboard at top, Settings at bottom
|
||||||
|
- Logical grouping: Buildings, Rooms, Bookings, Guests, Services, Calendar, Seasons
|
||||||
|
- Fixed Booking admin issues
|
||||||
|
- Fixed auto-draft status causing type errors (check for WP_Post object)
|
||||||
|
- Fixed guest dropdown to always load existing guests
|
||||||
|
- Booking title now auto-generates with guest name and dates (room removed per user request)
|
||||||
|
- Fixed booking history display on Guest edit page
|
||||||
|
- Implemented guest auto-creation from booking form
|
||||||
|
- When new guest data is entered in booking, guest record is automatically created
|
||||||
|
- Links booking to the new guest via guest_id meta
|
||||||
|
- Added encryption for sensitive guest data
|
||||||
|
- ID/passport numbers encrypted using AES-256-CBC
|
||||||
|
- Uses WordPress AUTH_KEY for encryption key derivation
|
||||||
|
- `encrypt()` and `decrypt()` methods in Guest class
|
||||||
|
- Backward compatible with legacy unencrypted data
|
||||||
|
- Security notice displayed in Identification meta box
|
||||||
|
- Disabled Gutenberg block editor for form-based post types
|
||||||
|
- Service, Guest, and Booking post types now use classic editor
|
||||||
|
- Added `disable_block_editor()` filter to each post type class
|
||||||
|
- Meta boxes now appear properly instead of being hidden at bottom
|
||||||
|
- Form-based interfaces are more appropriate than block editor for data entry
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `assets/css/admin.css` - Fixed tab gap, added subtab styles, booking form styles
|
||||||
|
- `assets/js/admin.js` - AJAX update check, booking form improvements, guest auto-creation
|
||||||
|
- `src/License/Manager.php` - Added `is_localhost()` method, updated `is_license_valid()`
|
||||||
|
- `src/License/Updater.php` - New file for auto-updates with configurable settings
|
||||||
|
- `src/Plugin.php` - Business owner settings, pricing subtabs, updates tab, menu reordering
|
||||||
|
- `src/PostTypes/Booking.php` - Auto-draft fixes, title generation, guest creation, disable Gutenberg
|
||||||
|
- `src/PostTypes/Guest.php` - AES-256-CBC encryption for ID numbers, disable Gutenberg
|
||||||
|
- `src/PostTypes/Service.php` - Disable Gutenberg for classic editor UI
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- WordPress nav-tab styling expects tabs and content to be flush (no margin/gap)
|
||||||
|
- Localhost detection should cover common development TLDs (.local, .test, .dev, .ddev.site)
|
||||||
|
- Private IP ranges can be detected using `FILTER_FLAG_NO_PRIV_RANGE`
|
||||||
|
- WordPress plugin updates require hooking into `pre_set_site_transient_update_plugins` and `plugins_api`
|
||||||
|
- Subtabs can be implemented with query parameters and conditional rendering within a single settings callback
|
||||||
|
- URL fields should use `esc_url_raw()` for sanitization, email fields use `sanitize_email()`
|
||||||
|
- Always check if post object is valid (`$post instanceof \WP_Post`) before accessing properties - auto-draft causes issues
|
||||||
|
- AES-256-CBC encryption with random IV provides secure storage for sensitive data
|
||||||
|
- Store IV concatenated with encrypted data (IV is not secret, just needs to be unique)
|
||||||
|
- `use_block_editor_for_post_type` filter disables Gutenberg per post type
|
||||||
|
- Post types with `show_in_rest => true` get Gutenberg by default, which hides traditional meta boxes
|
||||||
|
- Form-based admin interfaces (data entry) should use classic editor, not block editor
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.7.0 (Contact Form 7 Integration)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Integration/CF7.php` (~750 lines)
|
||||||
|
- Custom form tags: `[bnb_building_select]`, `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`, `[bnb_guests]`
|
||||||
|
- Server-side validation for all custom tags
|
||||||
|
- Availability validation in `wpcf7_before_send_mail` hook
|
||||||
|
- Automatic booking creation on form submission via `wpcf7_mail_sent`
|
||||||
|
- Guest record creation/linking using `find_or_create_guest()` pattern
|
||||||
|
- Custom mail tags: `[_bnb_room_name]`, `[_bnb_building_name]`, `[_bnb_calculated_price]`, `[_bnb_nights]`, `[_bnb_booking_reference]`
|
||||||
|
- Form type detection via CSS class `wp-bnb-booking-form`
|
||||||
|
- Created `assets/js/cf7-integration.js` (~230 lines)
|
||||||
|
- Building-based room filtering (rooms dropdown updates when building selected)
|
||||||
|
- Date validation (check-out after check-in, no past dates)
|
||||||
|
- Guest capacity validation against room limits
|
||||||
|
- AJAX availability checking with status display
|
||||||
|
- AJAX price calculation with formatted display
|
||||||
|
- Debounced updates to prevent excessive requests
|
||||||
|
- Created `assets/css/cf7-integration.css` (~200 lines)
|
||||||
|
- Two-column responsive form layout
|
||||||
|
- Availability status indicators (checking spinner, available checkmark, unavailable X)
|
||||||
|
- Price display formatting
|
||||||
|
- Capacity warning styling
|
||||||
|
- Dark mode support via `prefers-color-scheme`
|
||||||
|
- Print styles (hide interactive elements)
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added `use Magdev\WpBnb\Integration\CF7` import
|
||||||
|
- CF7 initialization in `init_frontend()` when WPCF7 class exists
|
||||||
|
- CF7 assets enqueuing with localized i18n strings
|
||||||
|
- Updated `README.md` with comprehensive CF7 documentation
|
||||||
|
- Custom form tags reference with options
|
||||||
|
- Example booking form template
|
||||||
|
- Example inquiry form template
|
||||||
|
- Custom mail tags documentation
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
|
||||||
|
- `src/Integration/CF7.php` - Main CF7 integration class
|
||||||
|
- `assets/js/cf7-integration.js` - Frontend JavaScript
|
||||||
|
- `assets/css/cf7-integration.css` - Form styling
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- CF7 custom tags registered via `wpcf7_add_form_tag()` with callback functions
|
||||||
|
- Validation filters follow pattern `wpcf7_validate_{tag_name}`
|
||||||
|
- `wpcf7_before_send_mail` can abort submission by setting `$abort` to true and adding validation error
|
||||||
|
- `wpcf7_mail_sent` fires after successful email, ideal for booking creation
|
||||||
|
- Custom mail tags via `wpcf7_special_mail_tags` filter receive submission data
|
||||||
|
- Form type detection by CSS class more reliable than checking for specific tags
|
||||||
|
- Room dropdown with `data-building` attributes enables client-side filtering
|
||||||
|
- AJAX endpoints reuse existing `wp_bnb_get_availability` and `wp_bnb_calculate_price` actions
|
||||||
|
- CF7 assets should depend on `contact-form-7` script/style handles
|
||||||
|
- Guest linking uses email as unique identifier for find-or-create pattern
|
||||||
|
|
||||||
|
**Released:**
|
||||||
|
|
||||||
|
- Committed: `28350aa` on dev branch
|
||||||
|
- Merged to main (fast-forward)
|
||||||
|
- Tagged: `v0.7.0`
|
||||||
|
- Pushed to origin: dev, main, v0.7.0
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|||||||
53
PLAN.md
53
PLAN.md
@@ -149,36 +149,47 @@ 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)
|
||||||
|
|
||||||
|
- [ ] Meanigful Metrics for this Plugin, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
|
||||||
|
- [ ] Example Grafana-Dashboard, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
|
||||||
|
- [ ] Update settings page to enable/disable metrics
|
||||||
|
|
||||||
|
## Phase 10: Security Audit (v0.10.0)
|
||||||
|
|
||||||
|
- [ ] Check for Wordpress best-practises
|
||||||
|
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
|
||||||
|
|
||||||
## Future Considerations (v1.0.0+)
|
## Future Considerations (v1.0.0+)
|
||||||
|
|
||||||
@@ -287,7 +298,7 @@ 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 |
|
||||||
@@ -295,6 +306,8 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| 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 |
|
||||||
|
| 0.9.0 | Prometheus Metrics | TBD |
|
||||||
|
| 0.10.0 | Security Audit | TBD |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
288
README.md
288
README.md
@@ -10,17 +10,24 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
|
|
||||||
- **Multi-Property Support**: Manage multiple buildings, each with multiple rooms
|
- **Multi-Property Support**: Manage multiple buildings, each with multiple rooms
|
||||||
- **Flexible Pricing**: Configure short-term (nights), mid-term (weeks), and long-term (months) pricing
|
- **Flexible Pricing**: Configure short-term (nights), mid-term (weeks), and long-term (months) pricing
|
||||||
|
- **Seasonal Pricing**: Set price modifiers for high/low seasons
|
||||||
- **Booking Management**: Track reservations from inquiry to checkout
|
- **Booking Management**: Track reservations from inquiry to checkout
|
||||||
- **Guest Management**: Store guest information securely with GDPR compliance
|
- **Guest Management**: Store guest information securely with GDPR compliance
|
||||||
|
- **Data Encryption**: Sensitive guest data (ID/passport) encrypted at rest
|
||||||
- **Additional Services**: Offer extras like breakfast, parking, or tours
|
- **Additional Services**: Offer extras like breakfast, parking, or tours
|
||||||
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
|
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
|
||||||
- **Contact Form 7 Integration**: Accept booking requests through forms
|
- **Auto-Updates**: Automatic update checks and installation from license server
|
||||||
|
- **Development Mode**: License bypass for local development environments
|
||||||
|
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
|
||||||
|
- **Dashboard**: Comprehensive admin dashboard with statistics and charts
|
||||||
|
- **Reports**: Detailed reports with CSV and PDF export
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- WordPress 6.0 or higher
|
- WordPress 6.0 or higher
|
||||||
- PHP 8.3 or higher
|
- PHP 8.3 or higher
|
||||||
- Valid license key
|
- Valid license key
|
||||||
|
- Contact Form 7 (optional, for booking forms)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -44,6 +51,23 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
|
|
||||||
- **Business Name**: Your B&B business name
|
- **Business Name**: Your B&B business name
|
||||||
- **Currency**: Select your preferred currency (CHF, EUR, USD, GBP)
|
- **Currency**: Select your preferred currency (CHF, EUR, USD, GBP)
|
||||||
|
- **Business Address**: Street, city, postal code, country
|
||||||
|
- **Contact Information**: Email, phone, website
|
||||||
|
- **Social Media**: Facebook, Instagram, X (Twitter), LinkedIn, TripAdvisor
|
||||||
|
|
||||||
|
### Update Settings
|
||||||
|
|
||||||
|
- **Update Notifications**: Enable/disable update notifications in WordPress
|
||||||
|
- **Automatic Updates**: Enable/disable automatic plugin updates
|
||||||
|
- **Check Frequency**: How often to check for updates (1-168 hours)
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
The plugin automatically detects local development environments and bypasses license validation. Supported environments:
|
||||||
|
|
||||||
|
- localhost, 127.0.0.1, ::1
|
||||||
|
- Domains ending in .local, .test, .localhost, .dev, .ddev.site
|
||||||
|
- Private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -76,31 +100,271 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
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:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
[wp_bnb_buildings]
|
[bnb_buildings] - List all buildings (grid/list layout)
|
||||||
[wp_bnb_rooms building="123"]
|
[bnb_rooms building="123"] - List rooms, optionally filtered by building
|
||||||
[wp_bnb_room_search]
|
[bnb_room_search] - Interactive room search form
|
||||||
|
[bnb_building id="123"] - Display a single building
|
||||||
|
[bnb_room id="456"] - Display a single room with availability
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Shortcode Attributes
|
||||||
|
|
||||||
|
**`[bnb_buildings]`** and **`[bnb_rooms]`**:
|
||||||
|
|
||||||
|
- `layout` - "grid" or "list" (default: grid)
|
||||||
|
- `columns` - 1-4 columns (default: 3)
|
||||||
|
- `limit` - Number of items (default: 12)
|
||||||
|
- `orderby` - title, date, price, capacity (default: title)
|
||||||
|
- `order` - ASC or DESC (default: ASC)
|
||||||
|
|
||||||
|
**`[bnb_rooms]`** additional attributes:
|
||||||
|
|
||||||
|
- `building` - Building ID to filter by
|
||||||
|
- `room_type` - Room type slug to filter by
|
||||||
|
- `amenities` - Comma-separated amenity slugs
|
||||||
|
|
||||||
## Gutenberg Blocks
|
## Gutenberg Blocks
|
||||||
|
|
||||||
The following blocks are available in the block editor:
|
The following blocks are available in the block editor:
|
||||||
|
|
||||||
- **Building** - Display a single building
|
- **Building** - Display a single building with details
|
||||||
- **Room** - Display a single room
|
- **Room** - Display a single room with availability form
|
||||||
- **Room Search** - Search and filter rooms
|
- **Room Search** - Interactive search form with filters
|
||||||
- **Booking Form** - Accept booking requests
|
- **Buildings List** - Display buildings grid/list
|
||||||
|
- **Rooms List** - Display rooms grid/list with filters
|
||||||
|
|
||||||
## Widgets
|
## Widgets
|
||||||
|
|
||||||
Available sidebar widgets:
|
Available sidebar widgets:
|
||||||
|
|
||||||
- **Similar Rooms** - Show rooms similar to the current one
|
- **Similar Rooms** - Show rooms from same building or room type
|
||||||
- **Building Rooms** - List all rooms in a building
|
- **Building Rooms** - List all rooms in a building
|
||||||
|
- **Availability Calendar** - Mini calendar showing booking status
|
||||||
|
|
||||||
|
## Contact Form 7 Integration
|
||||||
|
|
||||||
|
The plugin integrates with Contact Form 7 to accept booking requests and inquiries. Custom form tags are provided for room selection, date pickers, and guest counts.
|
||||||
|
|
||||||
|
### Custom Form Tags
|
||||||
|
|
||||||
|
Use these tags in your CF7 forms:
|
||||||
|
|
||||||
|
- `[bnb_building_select name]` - Building dropdown (optional filter for rooms)
|
||||||
|
- `[bnb_room_select* name]` - Room dropdown with capacity data
|
||||||
|
- `[bnb_date_checkin* name]` - Check-in date picker
|
||||||
|
- `[bnb_date_checkout* name]` - Check-out date picker
|
||||||
|
- `[bnb_guests* name]` - Guest count input
|
||||||
|
|
||||||
|
### Tag Options
|
||||||
|
|
||||||
|
**`[bnb_building_select]`**:
|
||||||
|
|
||||||
|
- `first_as_label:"text"` - Placeholder text (default: "All Locations")
|
||||||
|
|
||||||
|
**`[bnb_room_select]`**:
|
||||||
|
|
||||||
|
- `building_field:"name"` - Link to building field for filtering
|
||||||
|
- `first_as_label:"text"` - Placeholder text (default: "Select Room")
|
||||||
|
|
||||||
|
**`[bnb_guests]`**:
|
||||||
|
|
||||||
|
- `min:N` - Minimum guests (default: 1)
|
||||||
|
- `max:N` - Maximum guests (default: 10)
|
||||||
|
- `default:N` - Default value (default: 1)
|
||||||
|
|
||||||
|
### Example Booking Form
|
||||||
|
|
||||||
|
```txt
|
||||||
|
<div class="wp-bnb-booking-form">
|
||||||
|
<h3>Book Your Stay</h3>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
[bnb_building_select building first_as_label:"All Locations"]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
[bnb_room_select* room building_field:"building" first_as_label:"Select a Room"]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row-2col">
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>Check-in</label>
|
||||||
|
[bnb_date_checkin* check_in]
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>Check-out</label>
|
||||||
|
[bnb_date_checkout* check_out]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-availability-status"></div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Number of Guests</label>
|
||||||
|
[bnb_guests* guests min:1 max:10 default:2]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-price-display"></div>
|
||||||
|
|
||||||
|
<h4>Your Information</h4>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row-2col">
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>First Name</label>
|
||||||
|
[text* first_name]
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-form-field">
|
||||||
|
<label>Last Name</label>
|
||||||
|
[text* last_name]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Email</label>
|
||||||
|
[email* your_email]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Phone</label>
|
||||||
|
[tel your_phone]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Message</label>
|
||||||
|
[textarea your_message]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[submit "Request Booking"]
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Inquiry Form
|
||||||
|
|
||||||
|
For room-specific inquiries, add the `wp-bnb-inquiry-form` class:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
<div class="wp-bnb-inquiry-form">
|
||||||
|
<h3>Inquire About This Room</h3>
|
||||||
|
|
||||||
|
[hidden room default:123]
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Your Name</label>
|
||||||
|
[text* your_name]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Email</label>
|
||||||
|
[email* your_email]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-form-row">
|
||||||
|
<label>Your Question</label>
|
||||||
|
[textarea* your_message]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[submit "Send Inquiry"]
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Features
|
||||||
|
|
||||||
|
- **Availability Checking**: Real-time AJAX validation shows room availability
|
||||||
|
- **Price Display**: Estimated total calculated and displayed automatically
|
||||||
|
- **Room Filtering**: Rooms filter by building selection
|
||||||
|
- **Date Validation**: Check-out must be after check-in, no past dates
|
||||||
|
- **Capacity Validation**: Guest count validated against room capacity
|
||||||
|
- **Automatic Booking**: Booking record created with "pending" status on submission
|
||||||
|
- **Guest Linking**: Guest records created or linked by email address
|
||||||
|
|
||||||
|
### Custom Mail Tags
|
||||||
|
|
||||||
|
Use these in your CF7 mail templates:
|
||||||
|
|
||||||
|
- `[_bnb_room_name]` - Room title
|
||||||
|
- `[_bnb_building_name]` - Building name
|
||||||
|
- `[_bnb_calculated_price]` - Formatted price
|
||||||
|
- `[_bnb_nights]` - Number of nights
|
||||||
|
- `[_bnb_booking_reference]` - Booking reference (after creation)
|
||||||
|
|
||||||
## Hooks and Filters
|
## Hooks and Filters
|
||||||
|
|
||||||
@@ -123,7 +387,7 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) {
|
|||||||
|
|
||||||
### Do I need a license to use this plugin?
|
### Do I need a license to use this plugin?
|
||||||
|
|
||||||
Yes, a valid license is required to use the frontend features. The admin functionality works without a license for evaluation purposes.
|
Yes, a valid license is required to use the frontend features in production. The admin functionality works without a license for evaluation purposes. Local development environments (localhost, .local, .test, .dev domains) automatically bypass license validation.
|
||||||
|
|
||||||
### Can I manage multiple properties?
|
### Can I manage multiple properties?
|
||||||
|
|
||||||
@@ -137,6 +401,10 @@ Yes, guest data can be exported and deleted on request, and consent is tracked a
|
|||||||
|
|
||||||
WooCommerce integration for payments is planned for a future release.
|
WooCommerce integration for payments is planned for a future release.
|
||||||
|
|
||||||
|
### How is guest data secured?
|
||||||
|
|
||||||
|
Sensitive guest data like passport/ID numbers are encrypted using AES-256-CBC encryption before storage. The encryption key is derived from your WordPress AUTH_KEY, ensuring data is secure at rest.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes.
|
See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes.
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -54,7 +429,8 @@
|
|||||||
|
|
||||||
/* Settings Tabs */
|
/* Settings Tabs */
|
||||||
.nav-tab-wrapper {
|
.nav-tab-wrapper {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 0;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
@@ -64,6 +440,57 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings Subtabs */
|
||||||
|
.wp-bnb-subtabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #50575e;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab:last-child {
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #1d2327;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-subtab .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Form Tables */
|
/* Form Tables */
|
||||||
.form-table th {
|
.form-table th {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
@@ -1293,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,6 +91,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize update check functionality.
|
||||||
|
*/
|
||||||
|
function initUpdateCheck() {
|
||||||
|
var $checkBtn = $('#wp-bnb-check-updates');
|
||||||
|
var $spinner = $('#wp-bnb-update-spinner');
|
||||||
|
var $message = $('#wp-bnb-update-message');
|
||||||
|
var $latestVersion = $('#wp-bnb-latest-version');
|
||||||
|
var $lastCheck = $('#wp-bnb-update-last-check');
|
||||||
|
|
||||||
|
if (!$checkBtn.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkBtn.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Disable button and show spinner.
|
||||||
|
$checkBtn.prop('disabled', true);
|
||||||
|
$spinner.addClass('is-active');
|
||||||
|
$message.hide();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: wpBnbAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_check_updates',
|
||||||
|
nonce: wpBnbAdmin.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
$checkBtn.prop('disabled', false);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
|
||||||
|
// Update last check time.
|
||||||
|
$lastCheck.text(wpBnbAdmin.i18n.justNow || 'Just now');
|
||||||
|
|
||||||
|
// Update version display.
|
||||||
|
if (data.update_available) {
|
||||||
|
$latestVersion.html(
|
||||||
|
'<span style="color: #00a32a; font-weight: 600;">' +
|
||||||
|
data.latest_version +
|
||||||
|
'</span> ' +
|
||||||
|
'<span class="dashicons dashicons-yes" style="color: #00a32a;"></span> ' +
|
||||||
|
'<em>' + (wpBnbAdmin.i18n.updateAvailable || 'Update available!') + '</em>'
|
||||||
|
);
|
||||||
|
showUpdateMessage('success', data.message);
|
||||||
|
} else {
|
||||||
|
$latestVersion.html(
|
||||||
|
data.latest_version +
|
||||||
|
' <span style="color: #646970;">' +
|
||||||
|
(wpBnbAdmin.i18n.upToDate || '(You are up to date)') +
|
||||||
|
'</span>'
|
||||||
|
);
|
||||||
|
showUpdateMessage('success', data.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showUpdateMessage('error', response.data.message || wpBnbAdmin.i18n.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
$checkBtn.prop('disabled', false);
|
||||||
|
showUpdateMessage('error', wpBnbAdmin.i18n.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an update message.
|
||||||
|
*
|
||||||
|
* @param {string} type Message type (success or error).
|
||||||
|
* @param {string} message Message text.
|
||||||
|
*/
|
||||||
|
function showUpdateMessage(type, message) {
|
||||||
|
$message
|
||||||
|
.removeClass('success error')
|
||||||
|
.addClass(type)
|
||||||
|
.text(message)
|
||||||
|
.fadeIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize room gallery functionality.
|
* Initialize room gallery functionality.
|
||||||
*/
|
*/
|
||||||
@@ -768,7 +853,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pricingTypeInputs.on('change', function() {
|
function updatePriceRowVisibility() {
|
||||||
var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val();
|
var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val();
|
||||||
|
|
||||||
if (pricingType === 'included') {
|
if (pricingType === 'included') {
|
||||||
@@ -784,7 +869,12 @@
|
|||||||
$priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.');
|
$priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
$pricingTypeInputs.on('change', updatePriceRowVisibility);
|
||||||
|
|
||||||
|
// Set initial visibility state on page load.
|
||||||
|
updatePriceRowVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -934,9 +1024,185 @@
|
|||||||
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();
|
||||||
|
initUpdateCheck();
|
||||||
initRoomGallery();
|
initRoomGallery();
|
||||||
initPricingSettings();
|
initPricingSettings();
|
||||||
initSeasonForm();
|
initSeasonForm();
|
||||||
@@ -946,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
1654
src/Integration/CF7.php
Normal file
1654
src/Integration/CF7.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -126,13 +126,60 @@ final class Manager {
|
|||||||
/**
|
/**
|
||||||
* Check if license is valid.
|
* Check if license is valid.
|
||||||
*
|
*
|
||||||
|
* Localhost environments bypass the license check to allow
|
||||||
|
* full functionality during development.
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function is_license_valid(): bool {
|
public static function is_license_valid(): bool {
|
||||||
|
// Bypass license check for localhost environments.
|
||||||
|
if ( self::is_localhost() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||||
return 'valid' === $status;
|
return 'valid' === $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on localhost.
|
||||||
|
*
|
||||||
|
* Detects common local development environments:
|
||||||
|
* - localhost / 127.0.0.1 / ::1
|
||||||
|
* - .local, .test, .localhost domains
|
||||||
|
* - Private IP ranges (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_localhost(): bool {
|
||||||
|
$site_url = get_site_url();
|
||||||
|
$parsed = wp_parse_url( $site_url );
|
||||||
|
$host = $parsed['host'] ?? '';
|
||||||
|
|
||||||
|
// Check for localhost variations.
|
||||||
|
if ( in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common local development TLDs.
|
||||||
|
$local_tlds = array( '.local', '.test', '.localhost', '.dev', '.ddev.site' );
|
||||||
|
foreach ( $local_tlds as $tld ) {
|
||||||
|
if ( str_ends_with( $host, $tld ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for private IP ranges.
|
||||||
|
if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
|
||||||
|
// 10.x.x.x, 172.16-31.x.x, 192.168.x.x.
|
||||||
|
if ( ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get license key.
|
* Get license key.
|
||||||
*
|
*
|
||||||
|
|||||||
473
src/License/Updater.php
Normal file
473
src/License/Updater.php
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Updater class.
|
||||||
|
*
|
||||||
|
* Integrates with WordPress plugin update system to check for and install
|
||||||
|
* updates from the license server.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\License
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\License;
|
||||||
|
|
||||||
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||||||
|
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles plugin auto-updates from the license server.
|
||||||
|
*/
|
||||||
|
final class Updater {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance.
|
||||||
|
*
|
||||||
|
* @var Updater|null
|
||||||
|
*/
|
||||||
|
private static ?Updater $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin basename (e.g., wp-bnb/wp-bnb.php).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $plugin_basename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $plugin_slug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current plugin version.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $current_version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for update info.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const CACHE_KEY = 'wp_bnb_update_info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for last check timestamp.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const LAST_CHECK_KEY = 'wp_bnb_update_last_check';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default cache duration in seconds (12 hours).
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private const DEFAULT_CHECK_FREQUENCY = 12;
|
||||||
|
|
||||||
|
// Option keys for update settings.
|
||||||
|
public const OPTION_NOTIFICATIONS_ENABLED = 'wp_bnb_update_notifications_enabled';
|
||||||
|
public const OPTION_AUTO_INSTALL_ENABLED = 'wp_bnb_auto_install_enabled';
|
||||||
|
public const OPTION_CHECK_FREQUENCY = 'wp_bnb_update_check_frequency';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License client instance.
|
||||||
|
*
|
||||||
|
* @var SecureLicenseClient|LicenseClient|null
|
||||||
|
*/
|
||||||
|
private SecureLicenseClient|LicenseClient|null $client = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param string $plugin_file Full path to the main plugin file.
|
||||||
|
* @param string $current_version Current plugin version.
|
||||||
|
*/
|
||||||
|
public function __construct( string $plugin_file, string $current_version ) {
|
||||||
|
$this->plugin_basename = plugin_basename( $plugin_file );
|
||||||
|
$this->plugin_slug = dirname( $this->plugin_basename );
|
||||||
|
$this->current_version = $current_version;
|
||||||
|
self::$instance = $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance.
|
||||||
|
*
|
||||||
|
* @return Updater|null
|
||||||
|
*/
|
||||||
|
public static function get_instance(): ?Updater {
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize update hooks.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function init(): void {
|
||||||
|
// Allow complete disable via constant.
|
||||||
|
if ( defined( 'WP_BNB_DISABLE_AUTO_UPDATE' ) && WP_BNB_DISABLE_AUTO_UPDATE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook into WordPress update system.
|
||||||
|
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_updates' ) );
|
||||||
|
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
|
||||||
|
add_action( 'upgrader_process_complete', array( $this, 'after_update' ), 10, 2 );
|
||||||
|
|
||||||
|
// Auto-install filter for WordPress background updates.
|
||||||
|
add_filter( 'auto_update_plugin', array( $this, 'auto_update_plugin' ), 10, 2 );
|
||||||
|
|
||||||
|
// Clear update cache when license settings change.
|
||||||
|
add_action( 'update_option_' . Manager::OPTION_LICENSE_KEY, array( $this, 'clear_cache' ) );
|
||||||
|
add_action( 'update_option_' . Manager::OPTION_SERVER_URL, array( $this, 'clear_cache' ) );
|
||||||
|
|
||||||
|
// AJAX handler for manual update check.
|
||||||
|
add_action( 'wp_ajax_wp_bnb_check_updates', array( $this, 'ajax_check_updates' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if update notifications are enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_notifications_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_NOTIFICATIONS_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if auto-install is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_auto_install_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_AUTO_INSTALL_ENABLED, 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the update check frequency in hours.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get_check_frequency(): int {
|
||||||
|
$frequency = (int) get_option( self::OPTION_CHECK_FREQUENCY, self::DEFAULT_CHECK_FREQUENCY );
|
||||||
|
// Clamp between 1 and 168 hours (1 week).
|
||||||
|
return max( 1, min( 168, $frequency ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache duration in seconds based on check frequency.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function get_cache_duration(): int {
|
||||||
|
return self::get_check_frequency() * 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter for WordPress auto-update system.
|
||||||
|
*
|
||||||
|
* @param bool|null $update Whether to update the plugin.
|
||||||
|
* @param object $item The plugin update object.
|
||||||
|
* @return bool|null
|
||||||
|
*/
|
||||||
|
public function auto_update_plugin( $update, object $item ) {
|
||||||
|
// Only affect our plugin.
|
||||||
|
if ( ! isset( $item->plugin ) || $item->plugin !== $this->plugin_basename ) {
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if auto-install is enabled and license is valid.
|
||||||
|
if ( self::is_auto_install_enabled() && Manager::is_license_valid() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current plugin version.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_current_version(): string {
|
||||||
|
return $this->current_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last update check timestamp.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get_last_check(): int {
|
||||||
|
return (int) get_option( self::LAST_CHECK_KEY, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler: Check for updates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_check_updates(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'update_plugins' ) ) {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => __( 'You do not have permission to check for updates.', 'wp-bnb' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_cached_update_info( true );
|
||||||
|
|
||||||
|
if ( null === $update_info ) {
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'update_available' => false,
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'message' => __( 'Could not check for updates. Please verify your license configuration.', 'wp-bnb' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = array(
|
||||||
|
'update_available' => $update_info->updateAvailable && version_compare( $this->current_version, $update_info->version ?? '', '<' ),
|
||||||
|
'current_version' => $this->current_version,
|
||||||
|
'latest_version' => $update_info->version ?? $this->current_version,
|
||||||
|
'last_check' => time(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $response['update_available'] ) {
|
||||||
|
$response['message'] = sprintf(
|
||||||
|
/* translators: %s: New version number */
|
||||||
|
__( 'A new version (%s) is available.', 'wp-bnb' ),
|
||||||
|
$update_info->version
|
||||||
|
);
|
||||||
|
$response['changelog'] = $update_info->changelog ?? '';
|
||||||
|
} else {
|
||||||
|
$response['message'] = __( 'You are running the latest version.', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $response );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the license client.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function init_client(): bool {
|
||||||
|
if ( null !== $this->client ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$server_url = Manager::get_server_url();
|
||||||
|
$server_secret = Manager::get_server_secret();
|
||||||
|
|
||||||
|
if ( empty( $server_url ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! empty( $server_secret ) ) {
|
||||||
|
$this->client = new SecureLicenseClient(
|
||||||
|
httpClient: HttpClient::create(),
|
||||||
|
baseUrl: $server_url,
|
||||||
|
serverSecret: $server_secret,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->client = new LicenseClient(
|
||||||
|
httpClient: HttpClient::create(),
|
||||||
|
baseUrl: $server_url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch ( \Throwable $e ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for plugin updates.
|
||||||
|
*
|
||||||
|
* @param object $transient The update_plugins transient.
|
||||||
|
* @return object Modified transient.
|
||||||
|
*/
|
||||||
|
public function check_for_updates( object $transient ): object {
|
||||||
|
if ( empty( $transient->checked ) ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respect notifications enabled setting.
|
||||||
|
if ( ! self::is_notifications_enabled() ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_update_info();
|
||||||
|
|
||||||
|
if ( null === $update_info || ! $update_info->updateAvailable ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare versions.
|
||||||
|
if ( version_compare( $this->current_version, $update_info->version ?? '', '>=' ) ) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to update response.
|
||||||
|
$transient->response[ $this->plugin_basename ] = (object) array(
|
||||||
|
'slug' => $update_info->slug ?? $this->plugin_slug,
|
||||||
|
'plugin' => $this->plugin_basename,
|
||||||
|
'new_version' => $update_info->version,
|
||||||
|
'url' => $update_info->homepage ?? '',
|
||||||
|
'package' => $update_info->downloadUrl,
|
||||||
|
'icons' => $update_info->icons ?? array(),
|
||||||
|
'tested' => $update_info->tested ?? '',
|
||||||
|
'requires' => $update_info->requires ?? '',
|
||||||
|
'requires_php' => $update_info->requiresPhp ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide plugin information for the details modal.
|
||||||
|
*
|
||||||
|
* @param false|object|array $result The result object or array.
|
||||||
|
* @param string $action The API action being performed.
|
||||||
|
* @param object $args Plugin API arguments.
|
||||||
|
* @return false|object
|
||||||
|
*/
|
||||||
|
public function plugin_info( $result, string $action, object $args ) {
|
||||||
|
if ( 'plugin_information' !== $action ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $args->slug ) || $args->slug !== $this->plugin_slug ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_info = $this->get_update_info();
|
||||||
|
|
||||||
|
if ( null === $update_info ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_info = (object) array(
|
||||||
|
'name' => $update_info->name ?? 'WP BnB Manager',
|
||||||
|
'slug' => $update_info->slug ?? $this->plugin_slug,
|
||||||
|
'version' => $update_info->version ?? $this->current_version,
|
||||||
|
'author' => '<a href="https://src.bundespruefstelle.ch/magdev">Marco Graetsch</a>',
|
||||||
|
'homepage' => $update_info->homepage ?? 'https://src.bundespruefstelle.ch/magdev/wp-bnb',
|
||||||
|
'requires' => $update_info->requires ?? '6.0',
|
||||||
|
'tested' => $update_info->tested ?? '',
|
||||||
|
'requires_php' => $update_info->requiresPhp ?? '8.3',
|
||||||
|
'last_updated' => $update_info->lastUpdated?->format( 'Y-m-d' ) ?? '',
|
||||||
|
'download_link' => $update_info->downloadUrl ?? '',
|
||||||
|
'sections' => $update_info->sections ?? array(
|
||||||
|
'description' => __( 'A comprehensive Bed & Breakfast management plugin for WordPress.', 'wp-bnb' ),
|
||||||
|
'changelog' => $update_info->changelog ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $update_info->icons ) ) {
|
||||||
|
$plugin_info->icons = $update_info->icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plugin_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear update cache after upgrade.
|
||||||
|
*
|
||||||
|
* @param \WP_Upgrader $upgrader WP_Upgrader instance.
|
||||||
|
* @param array $hook_extra Extra arguments passed to hooked filters.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function after_update( \WP_Upgrader $upgrader, array $hook_extra ): void {
|
||||||
|
if ( ! isset( $hook_extra['plugins'] ) || ! is_array( $hook_extra['plugins'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( in_array( $this->plugin_basename, $hook_extra['plugins'], true ) ) {
|
||||||
|
$this->clear_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update info from cache or server.
|
||||||
|
*
|
||||||
|
* @param bool $force_refresh Force refresh from server.
|
||||||
|
* @return UpdateInfo|null
|
||||||
|
*/
|
||||||
|
public function get_cached_update_info( bool $force_refresh = false ): ?UpdateInfo {
|
||||||
|
if ( ! $force_refresh ) {
|
||||||
|
$cached = get_transient( self::CACHE_KEY );
|
||||||
|
if ( false !== $cached && $cached instanceof UpdateInfo ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if license is configured.
|
||||||
|
$license_key = Manager::get_license_key();
|
||||||
|
if ( empty( $license_key ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $this->init_client() ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$domain = $this->get_current_domain();
|
||||||
|
$update_info = $this->client->checkForUpdates(
|
||||||
|
licenseKey: $license_key,
|
||||||
|
domain: $domain,
|
||||||
|
pluginSlug: $this->plugin_slug,
|
||||||
|
currentVersion: $this->current_version,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache the result and update last check timestamp.
|
||||||
|
set_transient( self::CACHE_KEY, $update_info, $this->get_cache_duration() );
|
||||||
|
update_option( self::LAST_CHECK_KEY, time() );
|
||||||
|
|
||||||
|
return $update_info;
|
||||||
|
} catch ( \Throwable $e ) {
|
||||||
|
// Silently fail and return null - don't break WordPress.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update info from cache or server (alias for WordPress update system).
|
||||||
|
*
|
||||||
|
* @param bool $force_refresh Force refresh from server.
|
||||||
|
* @return UpdateInfo|null
|
||||||
|
*/
|
||||||
|
private function get_update_info( bool $force_refresh = false ): ?UpdateInfo {
|
||||||
|
return $this->get_cached_update_info( $force_refresh );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current domain.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_current_domain(): string {
|
||||||
|
$site_url = get_site_url();
|
||||||
|
$parsed = wp_parse_url( $site_url );
|
||||||
|
return $parsed['host'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the update cache.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function clear_cache(): void {
|
||||||
|
delete_transient( self::CACHE_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
635
src/Plugin.php
635
src/Plugin.php
@@ -10,16 +10,21 @@ 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\Integration\CF7;
|
||||||
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;
|
||||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
|
use Magdev\WpBnb\License\Updater as LicenseUpdater;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
use Magdev\WpBnb\PostTypes\Booking;
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
use Magdev\WpBnb\PostTypes\Building;
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
use Magdev\WpBnb\PostTypes\Guest;
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
@@ -128,6 +133,15 @@ final class Plugin {
|
|||||||
// Initialize License Manager (always active for admin).
|
// Initialize License Manager (always active for admin).
|
||||||
LicenseManager::get_instance();
|
LicenseManager::get_instance();
|
||||||
|
|
||||||
|
// Initialize auto-updater (requires license configuration).
|
||||||
|
$this->init_updater();
|
||||||
|
|
||||||
|
// Initialize 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 admin components.
|
// Initialize admin components.
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
$this->init_admin();
|
$this->init_admin();
|
||||||
@@ -139,6 +153,19 @@ final class Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the plugin auto-updater.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_updater(): void {
|
||||||
|
$updater = new LicenseUpdater(
|
||||||
|
plugin_file: WP_BNB_PATH . 'wp-bnb.php',
|
||||||
|
current_version: WP_BNB_VERSION,
|
||||||
|
);
|
||||||
|
$updater->init();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize admin components.
|
* Initialize admin components.
|
||||||
*
|
*
|
||||||
@@ -147,6 +174,7 @@ final class Plugin {
|
|||||||
private function init_admin(): void {
|
private function init_admin(): void {
|
||||||
// Admin menu and settings will be added here.
|
// Admin menu and settings will be added here.
|
||||||
add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
|
add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
|
||||||
|
add_action( 'admin_menu', array( $this, 'reorder_admin_menu' ), 99 );
|
||||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||||
|
|
||||||
// Initialize seasons admin page.
|
// Initialize seasons admin page.
|
||||||
@@ -222,6 +250,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;
|
||||||
@@ -242,6 +271,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',
|
||||||
@@ -250,13 +291,12 @@ final class Plugin {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_localize_script(
|
// Build localize data.
|
||||||
'wp-bnb-admin',
|
$localize_data = array(
|
||||||
'wpBnbAdmin',
|
|
||||||
array(
|
|
||||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||||
'postType' => $post_type,
|
'postType' => $post_type,
|
||||||
|
'isDashboard' => $is_dashboard,
|
||||||
'i18n' => array(
|
'i18n' => array(
|
||||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||||
@@ -280,9 +320,24 @@ final class Plugin {
|
|||||||
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
||||||
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
||||||
|
'justNow' => __( 'Just now', 'wp-bnb' ),
|
||||||
|
'updateAvailable' => __( 'Update available!', 'wp-bnb' ),
|
||||||
|
'upToDate' => __( '(You are up to date)', 'wp-bnb' ),
|
||||||
|
'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ),
|
||||||
|
'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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,6 +394,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' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -381,6 +473,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',
|
||||||
@@ -392,6 +494,60 @@ final class Plugin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorder the admin submenu items.
|
||||||
|
*
|
||||||
|
* Places Dashboard at top, Settings at bottom, and organizes
|
||||||
|
* the remaining items in logical order.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function reorder_admin_menu(): void {
|
||||||
|
global $submenu;
|
||||||
|
|
||||||
|
if ( ! isset( $submenu['wp-bnb'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the desired order of menu slugs.
|
||||||
|
$desired_order = array(
|
||||||
|
'wp-bnb', // Dashboard.
|
||||||
|
'edit.php?post_type=bnb_building', // Buildings.
|
||||||
|
'edit.php?post_type=bnb_room', // Rooms.
|
||||||
|
'edit.php?post_type=bnb_booking', // Bookings.
|
||||||
|
'edit.php?post_type=bnb_guest', // Guests.
|
||||||
|
'edit.php?post_type=bnb_service', // Services.
|
||||||
|
'wp-bnb-calendar', // Calendar.
|
||||||
|
'wp-bnb-reports', // Reports.
|
||||||
|
'wp-bnb-seasons', // Seasons.
|
||||||
|
'wp-bnb-settings', // Settings (always last).
|
||||||
|
);
|
||||||
|
|
||||||
|
$current_menu = $submenu['wp-bnb'];
|
||||||
|
$ordered_menu = array();
|
||||||
|
$index = 0;
|
||||||
|
|
||||||
|
// Add items in the desired order.
|
||||||
|
foreach ( $desired_order as $slug ) {
|
||||||
|
foreach ( $current_menu as $key => $item ) {
|
||||||
|
if ( $item[2] === $slug ) {
|
||||||
|
$ordered_menu[ $index ] = $item;
|
||||||
|
unset( $current_menu[ $key ] );
|
||||||
|
++$index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any remaining items not in the desired order.
|
||||||
|
foreach ( $current_menu as $item ) {
|
||||||
|
$ordered_menu[ $index ] = $item;
|
||||||
|
++$index;
|
||||||
|
}
|
||||||
|
|
||||||
|
$submenu['wp-bnb'] = $ordered_menu;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register plugin settings.
|
* Register plugin settings.
|
||||||
*
|
*
|
||||||
@@ -408,30 +564,16 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function render_dashboard_page(): void {
|
public function render_dashboard_page(): void {
|
||||||
$license_status = LicenseManager::get_cached_status();
|
DashboardAdmin::render();
|
||||||
?>
|
}
|
||||||
<div class="wrap">
|
|
||||||
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
|
||||||
|
|
||||||
<?php if ( 'valid' !== $license_status ) : ?>
|
/**
|
||||||
<div class="notice notice-warning">
|
* Render reports page.
|
||||||
<p>
|
*
|
||||||
<?php
|
* @return void
|
||||||
printf(
|
*/
|
||||||
/* translators: %s: Link to settings page */
|
public function render_reports_page(): void {
|
||||||
esc_html__( 'Your license is not active. Please %s to unlock all features.', 'wp-bnb' ),
|
ReportsAdmin::render();
|
||||||
'<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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -464,6 +606,10 @@ final class Plugin {
|
|||||||
class="nav-tab <?php echo 'license' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'license' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'License', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'License', 'wp-bnb' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=updates' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -475,6 +621,9 @@ final class Plugin {
|
|||||||
case 'license':
|
case 'license':
|
||||||
$this->render_license_settings();
|
$this->render_license_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'updates':
|
||||||
|
$this->render_updates_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->render_general_settings();
|
$this->render_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -495,6 +644,8 @@ final class Plugin {
|
|||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Business Information', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
<table class="form-table" role="presentation">
|
<table class="form-table" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
@@ -532,6 +683,143 @@ final class Plugin {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Address', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_street"><?php esc_html_e( 'Street Address', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_street" id="wp_bnb_address_street"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_street', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_city"><?php esc_html_e( 'City', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_city" id="wp_bnb_address_city"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_city', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_postal"><?php esc_html_e( 'Postal Code', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_postal" id="wp_bnb_address_postal"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_postal', '' ) ); ?>"
|
||||||
|
class="small-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_address_country"><?php esc_html_e( 'Country', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_address_country" id="wp_bnb_address_country"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_address_country', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Contact Information', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_contact_email"><?php esc_html_e( 'Email Address', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="email" name="wp_bnb_contact_email" id="wp_bnb_contact_email"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_email', '' ) ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
<p class="description"><?php esc_html_e( 'Primary contact email for bookings and inquiries.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_contact_phone"><?php esc_html_e( 'Phone Number', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="tel" name="wp_bnb_contact_phone" id="wp_bnb_contact_phone"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_phone', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="+41 12 345 67 89">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_contact_website"><?php esc_html_e( 'Website', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_contact_website" id="wp_bnb_contact_website"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_website', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Social Media', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_facebook"><?php esc_html_e( 'Facebook', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_facebook" id="wp_bnb_social_facebook"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_facebook', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://facebook.com/yourpage">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_instagram"><?php esc_html_e( 'Instagram', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_instagram" id="wp_bnb_social_instagram"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_instagram', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://instagram.com/yourprofile">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_x"><?php esc_html_e( 'X (Twitter)', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_x" id="wp_bnb_social_x"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_x', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://x.com/yourhandle">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_linkedin"><?php esc_html_e( 'LinkedIn', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_linkedin" id="wp_bnb_social_linkedin"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_linkedin', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://linkedin.com/company/yourcompany">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_social_tripadvisor"><?php esc_html_e( 'TripAdvisor', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="url" name="wp_bnb_social_tripadvisor" id="wp_bnb_social_tripadvisor"
|
||||||
|
value="<?php echo esc_attr( get_option( 'wp_bnb_social_tripadvisor', '' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="https://tripadvisor.com/...">
|
||||||
|
<p class="description"><?php esc_html_e( 'Link to your TripAdvisor listing.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<?php submit_button( __( 'Save Settings', 'wp-bnb' ) ); ?>
|
<?php submit_button( __( 'Save Settings', 'wp-bnb' ) ); ?>
|
||||||
</form>
|
</form>
|
||||||
<?php
|
<?php
|
||||||
@@ -543,6 +831,8 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function render_pricing_settings(): void {
|
private function render_pricing_settings(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Subtab switching only.
|
||||||
|
$active_subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'tiers';
|
||||||
$short_term_max = get_option( 'wp_bnb_short_term_max_nights', 6 );
|
$short_term_max = get_option( 'wp_bnb_short_term_max_nights', 6 );
|
||||||
$mid_term_max = get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
$mid_term_max = get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
||||||
$weekend_days = get_option( 'wp_bnb_weekend_days', '5,6' );
|
$weekend_days = get_option( 'wp_bnb_weekend_days', '5,6' );
|
||||||
@@ -558,10 +848,34 @@ final class Plugin {
|
|||||||
7 => __( 'Sunday', 'wp-bnb' ),
|
7 => __( 'Sunday', 'wp-bnb' ),
|
||||||
);
|
);
|
||||||
$selected_days = array_map( 'intval', explode( ',', $weekend_days ) );
|
$selected_days = array_map( 'intval', explode( ',', $weekend_days ) );
|
||||||
|
|
||||||
|
$base_url = admin_url( 'admin.php?page=wp-bnb-settings&tab=pricing' );
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<!-- Pricing Subtabs -->
|
||||||
|
<div class="wp-bnb-subtabs">
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=tiers' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'tiers' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-chart-bar"></span>
|
||||||
|
<?php esc_html_e( 'Pricing Tiers', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=weekend' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'weekend' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=seasons' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'seasons' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-image-filter"></span>
|
||||||
|
<?php esc_html_e( 'Seasons', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<?php if ( 'tiers' === $active_subtab ) : ?>
|
||||||
|
<!-- Pricing Tiers Subtab -->
|
||||||
<h2><?php esc_html_e( 'Pricing Tier Thresholds', 'wp-bnb' ); ?></h2>
|
<h2><?php esc_html_e( 'Pricing Tier Thresholds', 'wp-bnb' ); ?></h2>
|
||||||
<p class="description"><?php esc_html_e( 'Define the number of nights that determine which pricing tier applies.', 'wp-bnb' ); ?></p>
|
<p class="description"><?php esc_html_e( 'Define the number of nights that determine which pricing tier applies.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
@@ -606,6 +920,10 @@ final class Plugin {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Pricing Tiers', 'wp-bnb' ) ); ?>
|
||||||
|
|
||||||
|
<?php elseif ( 'weekend' === $active_subtab ) : ?>
|
||||||
|
<!-- Weekend Days Subtab -->
|
||||||
<h2><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></h2>
|
<h2><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></h2>
|
||||||
<p class="description"><?php esc_html_e( 'Select which days are considered weekend days for weekend surcharges.', 'wp-bnb' ); ?></p>
|
<p class="description"><?php esc_html_e( 'Select which days are considered weekend days for weekend surcharges.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
@@ -627,38 +945,45 @@ final class Plugin {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Weekend Days', 'wp-bnb' ) ); ?>
|
||||||
|
|
||||||
|
<?php else : ?>
|
||||||
|
<!-- Seasons Subtab -->
|
||||||
<h2><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h2>
|
<h2><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h2>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<?php
|
<?php esc_html_e( 'Seasonal pricing allows you to adjust room rates based on time of year. Prices are multiplied by the season modifier.', 'wp-bnb' ); ?>
|
||||||
printf(
|
</p>
|
||||||
/* translators: %s: Link to seasons page */
|
|
||||||
esc_html__( 'Manage seasonal pricing periods in the %s.', 'wp-bnb' ),
|
<p>
|
||||||
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ) . '">' . esc_html__( 'Seasons Manager', 'wp-bnb' ) . '</a>'
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ); ?>" class="button button-primary">
|
||||||
);
|
<span class="dashicons dashicons-admin-settings" style="vertical-align: text-top;"></span>
|
||||||
?>
|
<?php esc_html_e( 'Manage Seasons', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php if ( ! empty( $seasons ) ) : ?>
|
<?php if ( ! empty( $seasons ) ) : ?>
|
||||||
<table class="widefat striped" style="max-width: 600px;">
|
<table class="widefat striped" style="max-width: 700px; margin-top: 20px;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><?php esc_html_e( 'Season', 'wp-bnb' ); ?></th>
|
<th><?php esc_html_e( 'Season', 'wp-bnb' ); ?></th>
|
||||||
<th><?php esc_html_e( 'Period', 'wp-bnb' ); ?></th>
|
<th><?php esc_html_e( 'Period', 'wp-bnb' ); ?></th>
|
||||||
<th><?php esc_html_e( 'Modifier', 'wp-bnb' ); ?></th>
|
<th><?php esc_html_e( 'Modifier', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Priority', 'wp-bnb' ); ?></th>
|
||||||
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ( $seasons as $season ) : ?>
|
<?php foreach ( $seasons as $season ) : ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo esc_html( $season->name ); ?></td>
|
<td><strong><?php echo esc_html( $season->name ); ?></strong></td>
|
||||||
<td><?php echo esc_html( $season->start_date . ' - ' . $season->end_date ); ?></td>
|
<td><?php echo esc_html( $season->start_date . ' - ' . $season->end_date ); ?></td>
|
||||||
<td><?php echo esc_html( $season->getModifierLabel() ); ?></td>
|
<td><?php echo esc_html( $season->getModifierLabel() ); ?></td>
|
||||||
|
<td><?php echo esc_html( $season->priority ); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ( $season->active ) : ?>
|
<?php if ( $season->active ) : ?>
|
||||||
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
|
<span class="bnb-status-active"><?php esc_html_e( 'Active', 'wp-bnb' ); ?></span>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<span class="dashicons dashicons-marker" style="color: #646970;"></span>
|
<span class="bnb-status-inactive"><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -666,10 +991,12 @@ final class Plugin {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<p><?php esc_html_e( 'No seasons configured yet.', 'wp-bnb' ); ?></p>
|
<div class="notice notice-info inline" style="margin: 20px 0;">
|
||||||
|
<p><?php esc_html_e( 'No seasons configured yet. Create seasons to apply price modifiers during specific periods.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php submit_button( __( 'Save Pricing Settings', 'wp-bnb' ) ); ?>
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -685,10 +1012,21 @@ final class Plugin {
|
|||||||
$license_status = LicenseManager::get_cached_status();
|
$license_status = LicenseManager::get_cached_status();
|
||||||
$license_data = LicenseManager::get_cached_data();
|
$license_data = LicenseManager::get_cached_data();
|
||||||
$last_check = LicenseManager::get_last_check();
|
$last_check = LicenseManager::get_last_check();
|
||||||
|
$is_localhost = LicenseManager::is_localhost();
|
||||||
?>
|
?>
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<?php if ( $is_localhost ) : ?>
|
||||||
|
<div class="notice notice-info inline" style="margin: 0 0 20px 0;">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
||||||
|
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'You are running on a local development environment. License validation is bypassed and all features are enabled.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<h2><?php esc_html_e( 'License Status', 'wp-bnb' ); ?></h2>
|
<h2><?php esc_html_e( 'License Status', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
<table class="form-table" role="presentation">
|
<table class="form-table" role="presentation">
|
||||||
@@ -773,6 +1111,160 @@ final class Plugin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render updates settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_updates_settings(): void {
|
||||||
|
$updater = LicenseUpdater::get_instance();
|
||||||
|
|
||||||
|
if ( null === $updater ) {
|
||||||
|
?>
|
||||||
|
<p><?php esc_html_e( 'Update checker is not available.', 'wp-bnb' ); ?></p>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_version = $updater->get_current_version();
|
||||||
|
$last_check = LicenseUpdater::get_last_check();
|
||||||
|
$update_info = $updater->get_cached_update_info();
|
||||||
|
$update_available = false;
|
||||||
|
$latest_version = $current_version;
|
||||||
|
$notifications_enabled = LicenseUpdater::is_notifications_enabled();
|
||||||
|
$auto_install_enabled = LicenseUpdater::is_auto_install_enabled();
|
||||||
|
$check_frequency = LicenseUpdater::get_check_frequency();
|
||||||
|
$license_valid = LicenseManager::is_license_valid();
|
||||||
|
|
||||||
|
if ( $update_info instanceof UpdateInfo && $update_info->updateAvailable ) {
|
||||||
|
$latest_version = $update_info->version ?? $current_version;
|
||||||
|
$update_available = version_compare( $current_version, $latest_version, '<' );
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Update Status', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Current Version', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<strong><?php echo esc_html( $current_version ); ?></strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Latest Version', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<span id="wp-bnb-latest-version">
|
||||||
|
<?php if ( $update_available ) : ?>
|
||||||
|
<span style="color: #00a32a; font-weight: 600;">
|
||||||
|
<?php echo esc_html( $latest_version ); ?>
|
||||||
|
</span>
|
||||||
|
<span class="dashicons dashicons-yes" style="color: #00a32a;"></span>
|
||||||
|
<em><?php esc_html_e( 'Update available!', 'wp-bnb' ); ?></em>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php echo esc_html( $latest_version ); ?>
|
||||||
|
<span style="color: #646970;">
|
||||||
|
<?php esc_html_e( '(You are up to date)', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Last Check', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<span id="wp-bnb-update-last-check">
|
||||||
|
<?php if ( $last_check > 0 ) : ?>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Time ago string */
|
||||||
|
esc_html__( '%s ago', 'wp-bnb' ),
|
||||||
|
esc_html( human_time_diff( $last_check, time() ) )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php esc_html_e( 'Never', 'wp-bnb' ); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 30px;">
|
||||||
|
<button type="button" id="wp-bnb-check-updates" class="button button-secondary">
|
||||||
|
<span class="dashicons dashicons-update" style="vertical-align: text-top;"></span>
|
||||||
|
<?php esc_html_e( 'Check for Updates', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
<span class="spinner" id="wp-bnb-update-spinner"></span>
|
||||||
|
<?php if ( $update_available ) : ?>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'plugins.php' ) ); ?>" class="button button-primary">
|
||||||
|
<?php esc_html_e( 'Go to Plugins Page', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="wp-bnb-update-message" style="display: none; margin-bottom: 20px;"></div>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Update Settings', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable Update Notifications', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_update_notifications_enabled"
|
||||||
|
value="yes" <?php checked( $notifications_enabled ); ?>>
|
||||||
|
<?php esc_html_e( 'Check for updates from the license server', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, the plugin will check for updates and show notifications in the WordPress admin.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Automatic Updates', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_auto_install_enabled"
|
||||||
|
value="yes" <?php checked( $auto_install_enabled ); ?>
|
||||||
|
<?php disabled( ! $license_valid ); ?>>
|
||||||
|
<?php esc_html_e( 'Automatically install updates', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<?php if ( ! $license_valid ) : ?>
|
||||||
|
<span style="color: #d63638; margin-left: 10px;">
|
||||||
|
<?php esc_html_e( '(Requires valid license)', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, updates will be automatically installed during WordPress background updates.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_update_check_frequency"><?php esc_html_e( 'Check Frequency', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_update_check_frequency" id="wp_bnb_update_check_frequency"
|
||||||
|
value="<?php echo esc_attr( $check_frequency ); ?>"
|
||||||
|
min="1" max="168" class="small-text">
|
||||||
|
<?php esc_html_e( 'hours', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'How often to check for updates (1-168 hours). Default: 12 hours.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<?php submit_button( __( 'Save Update Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render license status badge.
|
* Render license status badge.
|
||||||
*
|
*
|
||||||
@@ -841,6 +1333,9 @@ final class Plugin {
|
|||||||
case 'license':
|
case 'license':
|
||||||
$this->save_license_settings();
|
$this->save_license_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'updates':
|
||||||
|
$this->save_updates_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->save_general_settings();
|
$this->save_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -853,6 +1348,7 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function save_general_settings(): void {
|
private function save_general_settings(): void {
|
||||||
|
// Business Information.
|
||||||
if ( isset( $_POST['wp_bnb_business_name'] ) ) {
|
if ( isset( $_POST['wp_bnb_business_name'] ) ) {
|
||||||
update_option( 'wp_bnb_business_name', sanitize_text_field( wp_unslash( $_POST['wp_bnb_business_name'] ) ) );
|
update_option( 'wp_bnb_business_name', sanitize_text_field( wp_unslash( $_POST['wp_bnb_business_name'] ) ) );
|
||||||
}
|
}
|
||||||
@@ -860,6 +1356,35 @@ final class Plugin {
|
|||||||
update_option( 'wp_bnb_currency', sanitize_text_field( wp_unslash( $_POST['wp_bnb_currency'] ) ) );
|
update_option( 'wp_bnb_currency', sanitize_text_field( wp_unslash( $_POST['wp_bnb_currency'] ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Address fields.
|
||||||
|
$address_fields = array( 'street', 'city', 'postal', 'country' );
|
||||||
|
foreach ( $address_fields as $field ) {
|
||||||
|
$key = 'wp_bnb_address_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_option( $key, sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact fields.
|
||||||
|
if ( isset( $_POST['wp_bnb_contact_email'] ) ) {
|
||||||
|
update_option( 'wp_bnb_contact_email', sanitize_email( wp_unslash( $_POST['wp_bnb_contact_email'] ) ) );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['wp_bnb_contact_phone'] ) ) {
|
||||||
|
update_option( 'wp_bnb_contact_phone', sanitize_text_field( wp_unslash( $_POST['wp_bnb_contact_phone'] ) ) );
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['wp_bnb_contact_website'] ) ) {
|
||||||
|
update_option( 'wp_bnb_contact_website', esc_url_raw( wp_unslash( $_POST['wp_bnb_contact_website'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Social media fields.
|
||||||
|
$social_fields = array( 'facebook', 'instagram', 'x', 'linkedin', 'tripadvisor' );
|
||||||
|
foreach ( $social_fields as $field ) {
|
||||||
|
$key = 'wp_bnb_social_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
update_option( $key, esc_url_raw( wp_unslash( $_POST[ $key ] ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Settings saved.', 'wp-bnb' ), 'success' );
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Settings saved.', 'wp-bnb' ), 'success' );
|
||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
@@ -912,6 +1437,34 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save updates settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_updates_settings(): void {
|
||||||
|
$notifications_enabled = isset( $_POST['wp_bnb_update_notifications_enabled'] ) ? 'yes' : 'no';
|
||||||
|
update_option( LicenseUpdater::OPTION_NOTIFICATIONS_ENABLED, $notifications_enabled );
|
||||||
|
|
||||||
|
$auto_install_enabled = isset( $_POST['wp_bnb_auto_install_enabled'] ) ? 'yes' : 'no';
|
||||||
|
update_option( LicenseUpdater::OPTION_AUTO_INSTALL_ENABLED, $auto_install_enabled );
|
||||||
|
|
||||||
|
if ( isset( $_POST['wp_bnb_update_check_frequency'] ) ) {
|
||||||
|
$frequency = absint( $_POST['wp_bnb_update_check_frequency'] );
|
||||||
|
$frequency = max( 1, min( 168, $frequency ) ); // Clamp between 1-168 hours.
|
||||||
|
update_option( LicenseUpdater::OPTION_CHECK_FREQUENCY, $frequency );
|
||||||
|
|
||||||
|
// Clear update cache when frequency changes so new frequency takes effect.
|
||||||
|
$updater = LicenseUpdater::get_instance();
|
||||||
|
if ( null !== $updater ) {
|
||||||
|
$updater->clear_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Update settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for checking room availability.
|
* AJAX handler for checking room availability.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -54,8 +54,24 @@ final class Booking {
|
|||||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
add_filter( 'wp_insert_post_data', array( self::class, 'auto_generate_title' ), 10, 2 );
|
|
||||||
add_action( 'admin_notices', array( self::class, 'show_conflict_notice' ) );
|
add_action( 'admin_notices', array( self::class, 'show_conflict_notice' ) );
|
||||||
|
|
||||||
|
// Disable Gutenberg block editor for Bookings - use classic editor for form-based UI.
|
||||||
|
add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable block editor for Bookings post type.
|
||||||
|
*
|
||||||
|
* @param bool $use_block_editor Whether to use block editor.
|
||||||
|
* @param string $post_type Post type.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool {
|
||||||
|
if ( self::POST_TYPE === $post_type ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $use_block_editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,7 +312,7 @@ final class Booking {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function render_guest_meta_box( \WP_Post $post ): void {
|
public static function render_guest_meta_box( \WP_Post $post ): void {
|
||||||
$guest_id = get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true );
|
$guest_id = (int) get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true );
|
||||||
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
|
$guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true );
|
||||||
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
|
$guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true );
|
||||||
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
|
$guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true );
|
||||||
@@ -314,7 +330,7 @@ final class Booking {
|
|||||||
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
|
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
|
||||||
} else {
|
} else {
|
||||||
$linked_guest = null;
|
$linked_guest = null;
|
||||||
$guest_id = '';
|
$guest_id = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -757,23 +773,26 @@ final class Booking {
|
|||||||
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
// No guest_id selected - get guest data from form fields.
|
||||||
|
$guest_name = isset( $_POST['bnb_booking_guest_name'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_guest_name'] ) ) : '';
|
||||||
|
$guest_email = isset( $_POST['bnb_booking_guest_email'] ) ? sanitize_email( wp_unslash( $_POST['bnb_booking_guest_email'] ) ) : '';
|
||||||
|
$guest_phone = isset( $_POST['bnb_booking_guest_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_guest_phone'] ) ) : '';
|
||||||
|
|
||||||
// Guest text fields (only save if no guest_id).
|
// Try to find or create a Guest post.
|
||||||
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
|
$linked_guest_id = self::find_or_create_guest( $guest_name, $guest_email, $guest_phone );
|
||||||
foreach ( $guest_fields as $field ) {
|
|
||||||
$key = 'bnb_booking_' . $field;
|
if ( $linked_guest_id ) {
|
||||||
if ( isset( $_POST[ $key ] ) ) {
|
// Link to the guest and sync data.
|
||||||
$value = wp_unslash( $_POST[ $key ] );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $linked_guest_id );
|
||||||
if ( 'guest_email' === $field ) {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $linked_guest_id ) );
|
||||||
$value = sanitize_email( $value );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $linked_guest_id, '_bnb_guest_email', true ) );
|
||||||
} elseif ( 'guest_notes' === $field ) {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $linked_guest_id, '_bnb_guest_phone', true ) );
|
||||||
$value = sanitize_textarea_field( $value );
|
|
||||||
} else {
|
} else {
|
||||||
$value = sanitize_text_field( $value );
|
// Fallback: save guest data directly to booking meta if guest creation failed.
|
||||||
}
|
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
||||||
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', $guest_name );
|
||||||
}
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', $guest_email );
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', $guest_phone );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,6 +896,130 @@ final class Booking {
|
|||||||
*/
|
*/
|
||||||
do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status );
|
do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate comprehensive title with guest name, room, and dates.
|
||||||
|
self::generate_comprehensive_title( $post_id, $room_id, $check_in, $check_out );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a comprehensive title for a booking.
|
||||||
|
*
|
||||||
|
* Format: "Guest Name (DD.MM - DD.MM.YYYY)"
|
||||||
|
*
|
||||||
|
* @param int $post_id Booking post ID.
|
||||||
|
* @param int $room_id Room post ID (unused, kept for signature compatibility).
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function generate_comprehensive_title( int $post_id, int $room_id, string $check_in, string $check_out ): void {
|
||||||
|
// Get guest name.
|
||||||
|
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
||||||
|
if ( empty( $guest_name ) ) {
|
||||||
|
$guest_name = __( 'Unknown Guest', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format dates.
|
||||||
|
$date_part = '';
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
|
||||||
|
if ( $check_in_date && $check_out_date ) {
|
||||||
|
// Same year: "01.02 - 05.02.2026"
|
||||||
|
// Different year: "28.12.2025 - 02.01.2026"
|
||||||
|
if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) {
|
||||||
|
$date_part = sprintf(
|
||||||
|
'%s - %s',
|
||||||
|
$check_in_date->format( 'd.m' ),
|
||||||
|
$check_out_date->format( 'd.m.Y' )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$date_part = sprintf(
|
||||||
|
'%s - %s',
|
||||||
|
$check_in_date->format( 'd.m.Y' ),
|
||||||
|
$check_out_date->format( 'd.m.Y' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build title: "Guest Name (dates)".
|
||||||
|
$title = $guest_name;
|
||||||
|
if ( $date_part ) {
|
||||||
|
$title .= sprintf( ' (%s)', $date_part );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the post title directly in the database to avoid infinite loop.
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->posts,
|
||||||
|
array( 'post_title' => $title ),
|
||||||
|
array( 'ID' => $post_id ),
|
||||||
|
array( '%s' ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear post cache.
|
||||||
|
clean_post_cache( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an existing guest by email or create a new one.
|
||||||
|
*
|
||||||
|
* @param string $name Guest full name.
|
||||||
|
* @param string $email Guest email.
|
||||||
|
* @param string $phone Guest phone (optional).
|
||||||
|
* @return int|null Guest post ID or null on failure.
|
||||||
|
*/
|
||||||
|
private static function find_or_create_guest( string $name, string $email, string $phone = '' ): ?int {
|
||||||
|
// Need at least a name to create a guest.
|
||||||
|
if ( empty( $name ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find existing guest by email.
|
||||||
|
if ( ! empty( $email ) ) {
|
||||||
|
$existing_guest = Guest::get_by_email( $email );
|
||||||
|
if ( $existing_guest ) {
|
||||||
|
return $existing_guest->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse name into first/last name.
|
||||||
|
$name_parts = explode( ' ', trim( $name ), 2 );
|
||||||
|
$first_name = $name_parts[0] ?? '';
|
||||||
|
$last_name = $name_parts[1] ?? '';
|
||||||
|
|
||||||
|
// Create new guest post.
|
||||||
|
$guest_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $name,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $guest_id ) || ! $guest_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save guest meta.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name );
|
||||||
|
|
||||||
|
if ( ! empty( $email ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_email', $email );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $phone ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_phone', $phone );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default status.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
|
||||||
|
|
||||||
|
return $guest_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -913,7 +1056,7 @@ final class Booking {
|
|||||||
public static function render_column( string $column, int $post_id ): void {
|
public static function render_column( string $column, int $post_id ): void {
|
||||||
switch ( $column ) {
|
switch ( $column ) {
|
||||||
case 'room':
|
case 'room':
|
||||||
$room_id = get_post_meta( $post_id, self::META_PREFIX . 'room_id', true );
|
$room_id = (int) get_post_meta( $post_id, self::META_PREFIX . 'room_id', true );
|
||||||
if ( $room_id ) {
|
if ( $room_id ) {
|
||||||
$room = get_post( $room_id );
|
$room = get_post( $room_id );
|
||||||
if ( $room ) {
|
if ( $room ) {
|
||||||
@@ -935,7 +1078,7 @@ final class Booking {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'guest':
|
case 'guest':
|
||||||
$guest_id = get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true );
|
$guest_id = (int) get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true );
|
||||||
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
$guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true );
|
||||||
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
|
$guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true );
|
||||||
if ( $guest_name ) {
|
if ( $guest_name ) {
|
||||||
@@ -1096,6 +1239,13 @@ final class Booking {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exclude auto-drafts from the list - they're not real bookings.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
$post_status = isset( $_GET['post_status'] ) ? sanitize_key( $_GET['post_status'] ) : '';
|
||||||
|
if ( empty( $post_status ) || 'all' === $post_status ) {
|
||||||
|
$query->set( 'post_status', array( 'publish', 'pending', 'draft', 'private' ) );
|
||||||
|
}
|
||||||
|
|
||||||
$meta_query = array();
|
$meta_query = array();
|
||||||
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
@@ -1142,31 +1292,11 @@ final class Booking {
|
|||||||
*/
|
*/
|
||||||
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
||||||
if ( self::POST_TYPE === $post->post_type ) {
|
if ( self::POST_TYPE === $post->post_type ) {
|
||||||
return __( 'Booking reference (auto-generated)', 'wp-bnb' );
|
return __( 'Title auto-generated from guest name and dates', 'wp-bnb' );
|
||||||
}
|
}
|
||||||
return $placeholder;
|
return $placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-generate booking reference as title.
|
|
||||||
*
|
|
||||||
* @param array $data Post data.
|
|
||||||
* @param array $postarr Post array.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function auto_generate_title( array $data, array $postarr ): array {
|
|
||||||
if ( self::POST_TYPE !== $data['post_type'] ) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only generate if title is empty or matches auto-generated pattern.
|
|
||||||
if ( empty( $data['post_title'] ) || preg_match( '/^BNB-\d{4}-\d{5}$/', $data['post_title'] ) ) {
|
|
||||||
$data['post_title'] = self::generate_reference();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show conflict notice in admin.
|
* Show conflict notice in admin.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,6 +30,75 @@ final class Guest {
|
|||||||
*/
|
*/
|
||||||
private const META_PREFIX = '_bnb_guest_';
|
private const META_PREFIX = '_bnb_guest_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encryption method.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const ENCRYPTION_METHOD = 'aes-256-cbc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt sensitive data.
|
||||||
|
*
|
||||||
|
* Uses WordPress AUTH_KEY for encryption key derivation.
|
||||||
|
*
|
||||||
|
* @param string $data Plain text data to encrypt.
|
||||||
|
* @return string Encrypted data (base64 encoded).
|
||||||
|
*/
|
||||||
|
private static function encrypt( string $data ): string {
|
||||||
|
if ( empty( $data ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = hash( 'sha256', AUTH_KEY . 'wp_bnb_guest_encryption', true );
|
||||||
|
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::ENCRYPTION_METHOD ) );
|
||||||
|
|
||||||
|
$encrypted = openssl_encrypt( $data, self::ENCRYPTION_METHOD, $key, 0, $iv );
|
||||||
|
|
||||||
|
if ( false === $encrypted ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store IV with encrypted data (IV is not secret, just needs to be unique).
|
||||||
|
return base64_encode( $iv . '::' . $encrypted );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt sensitive data.
|
||||||
|
*
|
||||||
|
* @param string $data Encrypted data (base64 encoded).
|
||||||
|
* @return string Decrypted plain text.
|
||||||
|
*/
|
||||||
|
private static function decrypt( string $data ): string {
|
||||||
|
if ( empty( $data ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = base64_decode( $data, true );
|
||||||
|
if ( false === $decoded ) {
|
||||||
|
// Data might be stored unencrypted (legacy), return as-is.
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode( '::', $decoded, 2 );
|
||||||
|
if ( count( $parts ) !== 2 ) {
|
||||||
|
// Data might be stored unencrypted (legacy), return as-is.
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
list( $iv, $encrypted ) = $parts;
|
||||||
|
$key = hash( 'sha256', AUTH_KEY . 'wp_bnb_guest_encryption', true );
|
||||||
|
|
||||||
|
$decrypted = openssl_decrypt( $encrypted, self::ENCRYPTION_METHOD, $key, 0, $iv );
|
||||||
|
|
||||||
|
if ( false === $decrypted ) {
|
||||||
|
// Decryption failed, might be legacy unencrypted data.
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the post type.
|
* Initialize the post type.
|
||||||
*
|
*
|
||||||
@@ -45,6 +114,23 @@ final class Guest {
|
|||||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
|
|
||||||
|
// Disable Gutenberg block editor for Guests - use classic editor for simpler UI.
|
||||||
|
add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable block editor for Guests post type.
|
||||||
|
*
|
||||||
|
* @param bool $use_block_editor Whether to use block editor.
|
||||||
|
* @param string $post_type Post type.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool {
|
||||||
|
if ( self::POST_TYPE === $post_type ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $use_block_editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,12 +397,12 @@ final class Guest {
|
|||||||
*/
|
*/
|
||||||
public static function render_identification_meta_box( \WP_Post $post ): void {
|
public static function render_identification_meta_box( \WP_Post $post ): void {
|
||||||
$id_type = get_post_meta( $post->ID, self::META_PREFIX . 'id_type', true );
|
$id_type = get_post_meta( $post->ID, self::META_PREFIX . 'id_type', true );
|
||||||
$id_number = get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true );
|
$id_number = self::decrypt( get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true ) );
|
||||||
$id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true );
|
$id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true );
|
||||||
?>
|
?>
|
||||||
<p class="description" style="margin-bottom: 15px;">
|
<p class="description" style="margin-bottom: 15px;">
|
||||||
<span class="dashicons dashicons-shield" style="color: #d63638;"></span>
|
<span class="dashicons dashicons-shield" style="color: #00a32a;"></span>
|
||||||
<?php esc_html_e( 'This information is sensitive. Handle with care according to privacy regulations.', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'This information is encrypted and stored securely.', 'wp-bnb' ); ?>
|
||||||
</p>
|
</p>
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -443,9 +529,9 @@ final class Guest {
|
|||||||
$room = get_post( $room_id );
|
$room = get_post( $room_id );
|
||||||
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
$price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true );
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
$statuses = Booking::get_statuses();
|
$statuses = Booking::get_booking_statuses();
|
||||||
$colors = Booking::get_status_colors();
|
$colors = Booking::get_status_colors();
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -563,7 +649,7 @@ final class Guest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text fields.
|
// Text fields (non-sensitive).
|
||||||
$text_fields = array(
|
$text_fields = array(
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
@@ -574,7 +660,6 @@ final class Guest {
|
|||||||
'country',
|
'country',
|
||||||
'nationality',
|
'nationality',
|
||||||
'id_type',
|
'id_type',
|
||||||
'id_number',
|
|
||||||
'status',
|
'status',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -589,6 +674,16 @@ final class Guest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sensitive field: ID number (encrypted).
|
||||||
|
if ( isset( $_POST['bnb_guest_id_number'] ) ) {
|
||||||
|
$id_number = sanitize_text_field( wp_unslash( $_POST['bnb_guest_id_number'] ) );
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . 'id_number',
|
||||||
|
self::encrypt( $id_number )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Email field (special sanitization).
|
// Email field (special sanitization).
|
||||||
if ( isset( $_POST['bnb_guest_email'] ) ) {
|
if ( isset( $_POST['bnb_guest_email'] ) ) {
|
||||||
update_post_meta(
|
update_post_meta(
|
||||||
@@ -1035,7 +1130,7 @@ final class Guest {
|
|||||||
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
// Only count completed bookings (checked_out) or confirmed ones.
|
// Only count completed bookings (checked_out) or confirmed ones.
|
||||||
if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) {
|
if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) {
|
||||||
$price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true );
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
$total += floatval( $price );
|
$total += floatval( $price );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1083,4 +1178,15 @@ final class Guest {
|
|||||||
|
|
||||||
return trim( $first_name . ' ' . $last_name );
|
return trim( $first_name . ' ' . $last_name );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest's ID number (decrypted).
|
||||||
|
*
|
||||||
|
* @param int $guest_id Guest post ID.
|
||||||
|
* @return string Decrypted ID number.
|
||||||
|
*/
|
||||||
|
public static function get_id_number( int $guest_id ): string {
|
||||||
|
$encrypted = get_post_meta( $guest_id, self::META_PREFIX . 'id_number', true );
|
||||||
|
return self::decrypt( $encrypted );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,23 @@ final class Service {
|
|||||||
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
|
|
||||||
|
// Disable Gutenberg block editor for Services - use classic editor for simpler UI.
|
||||||
|
add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable block editor for Services post type.
|
||||||
|
*
|
||||||
|
* @param bool $use_block_editor Whether to use block editor.
|
||||||
|
* @param string $post_type Post type.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool {
|
||||||
|
if ( self::POST_TYPE === $post_type ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $use_block_editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP BnB Management
|
* Plugin Name: WP BnB Management
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||||
* Version: 0.6.0
|
* Version: 0.8.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.0' );
|
define( 'WP_BNB_VERSION', '0.8.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