Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 864b8b2869 | |||
| 05f24fdec7 | |||
| aab3a4d1aa | |||
| c66af8e299 |
170
CHANGELOG.md
170
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.6.0] - 2026-02-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Frontend Features System:
|
||||||
|
- Room search with multiple filters (availability, capacity, room type, amenities, price range, building)
|
||||||
|
- AJAX-powered search with pagination and "Load More" functionality
|
||||||
|
- Date validation (check-out after check-in, minimum today)
|
||||||
|
- Real-time availability checking on single room pages
|
||||||
|
- Price calculator with breakdown display
|
||||||
|
- Shortcodes:
|
||||||
|
- `[bnb_buildings]` - Display buildings list/grid with filtering and sorting
|
||||||
|
- `[bnb_rooms]` - Display rooms list/grid with multiple filter options
|
||||||
|
- `[bnb_room_search]` - Interactive room search form with results
|
||||||
|
- `[bnb_building id="X"]` - Display single building details
|
||||||
|
- `[bnb_room id="X"]` - Display single room details with availability form
|
||||||
|
- WordPress Widgets:
|
||||||
|
- Similar Rooms widget (shows rooms from same building/type)
|
||||||
|
- Building Rooms widget (lists all rooms in a building)
|
||||||
|
- Availability Calendar widget (mini calendar with booking status)
|
||||||
|
- Gutenberg Blocks:
|
||||||
|
- Building block with ID selector
|
||||||
|
- Room block with ID selector
|
||||||
|
- Room Search block with filter presets
|
||||||
|
- Buildings List block with layout options
|
||||||
|
- Rooms List block with filter options
|
||||||
|
- Server-side rendered blocks for consistent output
|
||||||
|
- Frontend Search Class (`src/Frontend/Search.php`):
|
||||||
|
- Core search functionality with availability filtering
|
||||||
|
- Price range filtering with Calculator integration
|
||||||
|
- Pagination support
|
||||||
|
- AJAX endpoints: search_rooms, get_availability, get_calendar, calculate_price
|
||||||
|
- Room data formatting for JSON responses
|
||||||
|
- Frontend Shortcodes Class (`src/Frontend/Shortcodes.php`):
|
||||||
|
- All shortcode registration and handlers
|
||||||
|
- Grid/list layout support
|
||||||
|
- Column configuration (1-4 columns)
|
||||||
|
- Sorting options (title, date, price, capacity)
|
||||||
|
- Limit and offset support
|
||||||
|
- Block Registrar Class (`src/Blocks/BlockRegistrar.php`):
|
||||||
|
- Gutenberg block registration
|
||||||
|
- Block editor assets (CSS/JS)
|
||||||
|
- Server-side render callbacks
|
||||||
|
- Block data localization for editor
|
||||||
|
- Frontend Assets:
|
||||||
|
- Comprehensive CSS with CSS custom properties for theming
|
||||||
|
- Building and room card styles
|
||||||
|
- Search form and results styling
|
||||||
|
- Calendar widget styling with availability states
|
||||||
|
- Responsive design (breakpoints: 480px, 768px, 1024px)
|
||||||
|
- JavaScript with SearchForm, CalendarWidget, AvailabilityForm, PriceCalculator classes
|
||||||
|
- AJAX integration with proper error handling
|
||||||
|
- XSS-safe DOM construction (no innerHTML with user data)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated with frontend component initialization
|
||||||
|
- Frontend assets now include localized script data with AJAX URL, nonce, and i18n strings
|
||||||
|
- Widget registration added to init_frontend() method
|
||||||
|
- Search, Shortcodes, and BlockRegistrar initialized when license is valid
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- AJAX nonce verification on all frontend requests
|
||||||
|
- Input sanitization on all search parameters
|
||||||
|
- Output escaping in shortcode and widget templates
|
||||||
|
- XSS prevention in JavaScript (textContent instead of innerHTML)
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-01-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Additional Services System:
|
||||||
|
- Custom Post Type: Services (`bnb_service`)
|
||||||
|
- Service pricing types: Included (free), Per Booking (one-time), Per Night
|
||||||
|
- Service configuration: price, status, sort order, max quantity
|
||||||
|
- Custom admin columns with pricing type icons and status badges
|
||||||
|
- Filters by status and pricing type
|
||||||
|
- Service data helper methods for pricing calculations
|
||||||
|
- Service Categories Taxonomy (`bnb_service_category`)
|
||||||
|
- Non-hierarchical (tag-like) structure
|
||||||
|
- Icon selection per category
|
||||||
|
- Sort order for custom ordering
|
||||||
|
- Default categories: Food & Dining, Transportation, Wellness & Spa, Activities, Housekeeping
|
||||||
|
- Booking-Services Integration:
|
||||||
|
- Services meta box in Booking edit screen
|
||||||
|
- Checkbox-based service selection
|
||||||
|
- Quantity input for services with max_quantity > 1
|
||||||
|
- Real-time price calculation per service based on nights
|
||||||
|
- Services total display
|
||||||
|
- Price breakdown shows services cost
|
||||||
|
- Grand total (room + services) in admin list and pricing meta box
|
||||||
|
- Admin UI Enhancements:
|
||||||
|
- Service selector with pricing type indicators
|
||||||
|
- Included services badge
|
||||||
|
- Per-night price suffix display
|
||||||
|
- Service line totals with quantity support
|
||||||
|
- Services total summary in booking
|
||||||
|
- CSS styles for all service-related components
|
||||||
|
- JavaScript for dynamic service pricing calculations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to register Service CPT and ServiceCategory taxonomy
|
||||||
|
- Admin assets enqueued for Service post type screens
|
||||||
|
- Booking admin list shows total price including services
|
||||||
|
- Booking pricing meta box displays services breakdown and grand total
|
||||||
|
- Admin JavaScript extended with service pricing and selection logic
|
||||||
|
- Admin CSS includes comprehensive service styling
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-01-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Guest Management System with dedicated CPT:
|
||||||
|
- Custom Post Type: Guests (`bnb_guest`)
|
||||||
|
- Personal information fields (name, email, phone, DOB, nationality)
|
||||||
|
- Address fields (street, city, postal code, country)
|
||||||
|
- Identification fields (ID type, number, expiry date)
|
||||||
|
- Guest status tracking (active, inactive, blocked)
|
||||||
|
- Internal notes and preferences
|
||||||
|
- GDPR consent tracking (marketing, data processing, consent date)
|
||||||
|
- Booking history display with statistics
|
||||||
|
- Helper methods: `get_by_email()`, `get_bookings()`, `get_booking_count()`, `get_total_spent()`, `get_full_name()`, `get_formatted_address()`
|
||||||
|
- Guest-Booking Integration:
|
||||||
|
- Guest search by email/name with AJAX autocomplete
|
||||||
|
- Link existing guests to bookings
|
||||||
|
- Create new guests from booking form
|
||||||
|
- Guest profile link in booking admin
|
||||||
|
- Automatic guest data sync when linked
|
||||||
|
- Backward compatibility for legacy bookings without guest_id
|
||||||
|
- GDPR/Privacy Compliance (`src/Privacy/Manager.php`):
|
||||||
|
- WordPress Privacy API integration
|
||||||
|
- Personal data exporter (guest profile + booking history)
|
||||||
|
- Personal data eraser with anonymization option
|
||||||
|
- Privacy policy content suggestion
|
||||||
|
- Support for WordPress Tools > Export/Erase Personal Data
|
||||||
|
- Guest anonymization (replaces PII with placeholder data)
|
||||||
|
- Booking anonymization for connected bookings
|
||||||
|
- Email Notifier Enhancements:
|
||||||
|
- Guest data retrieval from Guest CPT when available
|
||||||
|
- Fallback to booking meta for legacy bookings
|
||||||
|
- New placeholders: `{guest_first_name}`, `{guest_last_name}`, `{guest_full_address}`
|
||||||
|
- Admin UI Styles:
|
||||||
|
- Guest search container and results styling
|
||||||
|
- Linked guest display card
|
||||||
|
- Booking history table in Guest
|
||||||
|
- Consent status indicators
|
||||||
|
- Guest status badges
|
||||||
|
- Privacy action buttons
|
||||||
|
- Anonymized data display
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Booking meta box updated with guest search/link functionality
|
||||||
|
- Plugin.php now initializes Guest CPT and Privacy Manager
|
||||||
|
- Admin JavaScript includes guest search with debounce
|
||||||
|
- Admin CSS extended with Guest and Privacy styles
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Guest email used as unique identifier for deduplication
|
||||||
|
- GDPR-compliant data export and erasure
|
||||||
|
- Consent tracking with timestamps
|
||||||
|
- Anonymization preserves booking records while removing PII
|
||||||
|
- AJAX endpoints secured with nonce verification
|
||||||
|
|
||||||
## [0.3.0] - 2026-01-31
|
## [0.3.0] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -191,6 +358,9 @@ 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.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.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
||||||
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
||||||
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.2.0
|
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.2.0
|
||||||
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.1.0
|
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.1.0
|
||||||
|
|||||||
92
CLAUDE.md
92
CLAUDE.md
@@ -128,6 +128,31 @@ for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
|||||||
- Commit messages should follow the established format with Claude Code attribution
|
- Commit messages should follow the established format with Claude Code attribution
|
||||||
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
|
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
|
||||||
|
|
||||||
|
**CRITICAL - Release Workflow:**
|
||||||
|
|
||||||
|
On every new version, ALWAYS execute this complete workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit changes to dev branch
|
||||||
|
git add <files>
|
||||||
|
git commit -m "Description of changes (vX.X.X)"
|
||||||
|
|
||||||
|
# 2. Merge dev to main
|
||||||
|
git checkout main
|
||||||
|
git merge dev --no-edit
|
||||||
|
|
||||||
|
# 3. Create annotated tag
|
||||||
|
git tag -a vX.X.X -m "Version X.X.X - Brief description"
|
||||||
|
|
||||||
|
# 4. Push everything to origin
|
||||||
|
git push origin dev main vX.X.X
|
||||||
|
|
||||||
|
# 5. Switch back to dev for continued development
|
||||||
|
git checkout dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Never skip any of these steps. The release is not complete until all branches and the tag are pushed to origin.
|
||||||
|
|
||||||
#### What Gets Released
|
#### What Gets Released
|
||||||
|
|
||||||
- All plugin source files
|
- All plugin source files
|
||||||
@@ -468,3 +493,70 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Calendar displays bookings color-coded by status for quick visual overview
|
- Calendar displays bookings color-coded by status for quick visual overview
|
||||||
- HTML email templates with inline CSS for better email client compatibility
|
- HTML email templates with inline CSS for better email client compatibility
|
||||||
- Status transitions can trigger different email notifications via hooks
|
- Status transitions can trigger different email notifications via hooks
|
||||||
|
- **Release workflow** must always include: commit to dev → merge to main → create tag → push all to origin
|
||||||
|
- Git fast-forward merge works well when dev is ahead of main with no conflicts
|
||||||
|
|
||||||
|
**Released:**
|
||||||
|
|
||||||
|
- Committed: `0c601df` on dev branch
|
||||||
|
- Merged to main (fast-forward)
|
||||||
|
- Tagged: `v0.3.0`
|
||||||
|
- Pushed to origin: dev, main, v0.3.0
|
||||||
|
|
||||||
|
### 2026-01-31 - Version 0.5.0 (Additional Services)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created Custom Taxonomy: Service Categories (`bnb_service_category`)
|
||||||
|
- Non-hierarchical (tag-like) structure
|
||||||
|
- Dashicon selection for visual display
|
||||||
|
- Sort order meta field for custom ordering
|
||||||
|
- Default categories: Food & Dining, Transportation, Wellness & Spa, Activities, Housekeeping
|
||||||
|
- Created Custom Post Type: Services (`bnb_service`)
|
||||||
|
- Three pricing types: Included (free), Per Booking, Per Night
|
||||||
|
- Price configuration per service
|
||||||
|
- Service status (active/inactive)
|
||||||
|
- Sort order for display ordering
|
||||||
|
- Maximum quantity setting per service
|
||||||
|
- Custom admin columns: pricing type, price, status
|
||||||
|
- Filters by status and pricing type
|
||||||
|
- Helper methods: `get_service_data()`, `calculate_service_price()`, `get_services_for_booking()`, `format_service_price()`
|
||||||
|
- Updated Booking post type with services integration
|
||||||
|
- Added `SERVICES_META_KEY` constant for services storage
|
||||||
|
- New meta box: Additional Services with checkbox selection
|
||||||
|
- Quantity input for services with max_quantity > 1
|
||||||
|
- Real-time per-service line total calculation
|
||||||
|
- Services total display
|
||||||
|
- Price breakdown now shows services cost
|
||||||
|
- Grand total (room + services) in pricing meta box
|
||||||
|
- Admin list price column shows total including services
|
||||||
|
- Helper methods: `calculate_booking_services_total()`, `get_booking_services()`
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Registered ServiceCategory taxonomy
|
||||||
|
- Registered Service post type
|
||||||
|
- Added Service post type to asset enqueuing
|
||||||
|
- Added i18n strings for service pricing descriptions
|
||||||
|
- Updated `assets/css/admin.css`
|
||||||
|
- Service status badges
|
||||||
|
- Service pricing meta box styles
|
||||||
|
- Booking services selector styles
|
||||||
|
- Service item with selected state
|
||||||
|
- Quantity inputs and line totals
|
||||||
|
- Services total summary
|
||||||
|
- Grand total display
|
||||||
|
- Updated `assets/js/admin.js`
|
||||||
|
- `initServicePricing()`: Toggle price row based on pricing type
|
||||||
|
- `initBookingServices()`: Service selection with real-time price calculation
|
||||||
|
- Quantity change handlers with min/max enforcement
|
||||||
|
- Automatic recalculation when booking dates change
|
||||||
|
- Updated version to 0.5.0
|
||||||
|
- Updated CHANGELOG.md with Phase 5 changes
|
||||||
|
- Updated PLAN.md to mark Phase 5 complete
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Service pricing calculation depends on pricing_type: included=0, per_booking=price*qty, per_night=price*qty*nights
|
||||||
|
- Services are stored as JSON array in booking meta with service_id, quantity, price, pricing_type
|
||||||
|
- Same namespace classes can reference each other directly without use statements
|
||||||
|
- Services meta box renders before pricing meta box so services total is available
|
||||||
|
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
|
||||||
|
|||||||
69
PLAN.md
69
PLAN.md
@@ -84,43 +84,43 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Email notifications
|
- [x] Email notifications
|
||||||
- [x] Booking confirmation
|
- [x] Booking confirmation
|
||||||
|
|
||||||
## Phase 4: Guest Management (v0.4.0)
|
## Phase 4: Guest Management (v0.4.0) - Complete
|
||||||
|
|
||||||
### Custom Post Type: Guests
|
### Custom Post Type: Guests
|
||||||
|
|
||||||
- [ ] Personal information (name, email, phone)
|
- [x] Personal information (name, email, phone)
|
||||||
- [ ] Address fields
|
- [x] Address fields
|
||||||
- [ ] ID/Passport information
|
- [x] ID/Passport information
|
||||||
- [ ] Booking history reference
|
- [x] Booking history reference
|
||||||
- [ ] Notes and preferences
|
- [x] Notes and preferences
|
||||||
|
|
||||||
### Privacy & Compliance
|
### Privacy & Compliance
|
||||||
|
|
||||||
- [ ] GDPR compliance features
|
- [x] GDPR compliance features
|
||||||
- [ ] Data export functionality
|
- [x] Data export functionality
|
||||||
- [ ] Data deletion on request
|
- [x] Data deletion on request
|
||||||
- [ ] Consent tracking
|
- [x] Consent tracking
|
||||||
|
|
||||||
## Phase 5: Additional Services (v0.5.0)
|
## Phase 5: Additional Services (v0.5.0) - Complete
|
||||||
|
|
||||||
### Service Options
|
### Service Options
|
||||||
|
|
||||||
- [ ] Custom Post Type: Services
|
- [x] Custom Post Type: Services
|
||||||
- [ ] Price per service (or included)
|
- [x] Price per service (or included)
|
||||||
- [ ] Per-booking or per-night pricing
|
- [x] Per-booking or per-night pricing
|
||||||
- [ ] Service categories
|
- [x] Service categories
|
||||||
|
|
||||||
### Booking Services
|
### Booking Services
|
||||||
|
|
||||||
- [ ] Service selection during booking
|
- [x] Service selection during booking
|
||||||
- [ ] Automatic price calculation
|
- [x] Automatic price calculation
|
||||||
- [ ] Service summary display
|
- [x] Service summary display
|
||||||
|
|
||||||
## Phase 6: Frontend Features (v0.6.0)
|
## Phase 6: Frontend Features (v0.6.0) - Complete
|
||||||
|
|
||||||
### Search & Filtering
|
### Search & Filtering
|
||||||
|
|
||||||
- [ ] Room search with filters
|
- [x] Room search with filters
|
||||||
- Date range (availability)
|
- Date range (availability)
|
||||||
- Capacity
|
- Capacity
|
||||||
- Room type
|
- Room type
|
||||||
@@ -130,23 +130,24 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
|
|
||||||
### Display Components
|
### Display Components
|
||||||
|
|
||||||
- [ ] Building list/grid shortcode
|
- [x] Building list/grid shortcode
|
||||||
- [ ] Room list/grid shortcode
|
- [x] Room list/grid shortcode
|
||||||
- [ ] Room detail template
|
- [x] Room detail template
|
||||||
- [ ] Availability widget
|
- [x] Availability widget
|
||||||
|
|
||||||
### Gutenberg Blocks
|
### Gutenberg Blocks
|
||||||
|
|
||||||
- [ ] Building block
|
- [x] Building block
|
||||||
- [ ] Room block
|
- [x] Room block
|
||||||
- [ ] Room search block
|
- [x] Room search block
|
||||||
- [ ] Booking form block
|
- [x] Buildings list block
|
||||||
|
- [x] Rooms list block
|
||||||
|
|
||||||
### Widgets
|
### Widgets
|
||||||
|
|
||||||
- [ ] Similar rooms widget
|
- [x] Similar rooms widget
|
||||||
- [ ] Building rooms widget
|
- [x] Building rooms widget
|
||||||
- [ ] Availability calendar widget
|
- [x] Availability calendar widget
|
||||||
|
|
||||||
## Phase 7: Contact Form 7 Integration (v0.7.0)
|
## Phase 7: Contact Form 7 Integration (v0.7.0)
|
||||||
|
|
||||||
@@ -291,9 +292,9 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| 0.1.0 | Data structures | Complete |
|
| 0.1.0 | Data structures | Complete |
|
||||||
| 0.2.0 | Pricing | Complete |
|
| 0.2.0 | Pricing | Complete |
|
||||||
| 0.3.0 | Bookings | Complete |
|
| 0.3.0 | Bookings | Complete |
|
||||||
| 0.4.0 | Guests | TBD |
|
| 0.4.0 | Guests | Complete |
|
||||||
| 0.5.0 | Services | TBD |
|
| 0.5.0 | Services | Complete |
|
||||||
| 0.6.0 | Frontend | TBD |
|
| 0.6.0 | Frontend | Complete |
|
||||||
| 0.7.0 | CF7 Integration | TBD |
|
| 0.7.0 | CF7 Integration | TBD |
|
||||||
| 0.8.0 | Dashboard | TBD |
|
| 0.8.0 | Dashboard | TBD |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
@@ -688,3 +688,608 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Guest Management Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Guest Search Container */
|
||||||
|
.bnb-guest-search-container {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-search-container label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-search-container input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Guest Search Results */
|
||||||
|
.bnb-guest-results {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-result {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-result:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-result:hover {
|
||||||
|
background: #f0f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-result-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-result-email {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-result-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #a7aaad;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-no-results {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-create-new {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border-top: 1px solid #c3c4c7;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-create-new:hover {
|
||||||
|
background: #d4e4f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-create-new .dashicons {
|
||||||
|
color: #2271b1;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Guest Linked Display */
|
||||||
|
.bnb-guest-linked {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-linked-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-linked-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-linked-name .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-linked-details {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-linked-details div {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-linked-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Guest Admin Columns */
|
||||||
|
.column-email,
|
||||||
|
.column-phone,
|
||||||
|
.column-country,
|
||||||
|
.column-bookings {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-bookings a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Guest Status Badges */
|
||||||
|
.bnb-guest-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-status-active {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-status-inactive {
|
||||||
|
background: #f6f7f7;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-guest-status-blocked {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking History Table in Guest */
|
||||||
|
.bnb-booking-history {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-history th,
|
||||||
|
.bnb-booking-history td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-history th {
|
||||||
|
background: #f6f7f7;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-history tr:hover td {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-history-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
GDPR / Privacy Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Consent Status */
|
||||||
|
.bnb-consent-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-item .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-granted {
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-not-granted {
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consent Checkboxes in Guest Form */
|
||||||
|
.bnb-consent-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-checkbox input[type="checkbox"] {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-checkbox label {
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-consent-checkbox-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Privacy Settings Section */
|
||||||
|
.bnb-privacy-settings {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-section-header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-section-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border-left: 4px solid #72aee6;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-notice .dashicons {
|
||||||
|
color: #72aee6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-notice p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Privacy Actions in Guest Profile */
|
||||||
|
.bnb-privacy-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #f0f0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-actions h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-actions-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-action-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-privacy-action-button .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anonymized Data Display */
|
||||||
|
.bnb-anonymized {
|
||||||
|
font-style: italic;
|
||||||
|
color: #a7aaad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-anonymized-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #f0f0f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-anonymized-badge .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data Retention Settings */
|
||||||
|
.bnb-retention-settings {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-retention-settings input[type="number"] {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-retention-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Services Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Services List in Admin */
|
||||||
|
.column-pricing_type,
|
||||||
|
.column-service_status {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Service Status Badges */
|
||||||
|
.bnb-service-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-status-active {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-status-inactive {
|
||||||
|
background: #f6f7f7;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-included {
|
||||||
|
color: #00a32a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Service Pricing Meta Box */
|
||||||
|
.bnb-service-pricing-type fieldset label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-pricing-type fieldset label input {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-pricing-type fieldset p.description {
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Booking Services Selector
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.bnb-services-selector {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-services-list {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-item:hover {
|
||||||
|
background: #f0f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-item.selected {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-checkbox input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-price-label {
|
||||||
|
color: #135e96;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-included-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-quantity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-quantity label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-qty-input {
|
||||||
|
width: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-line-total {
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-service-total-value {
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services Total */
|
||||||
|
.bnb-services-total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-services-total strong {
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bnb-services-total-amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Services Message */
|
||||||
|
.bnb-no-services-message {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-services-message a {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Pricing with Services */
|
||||||
|
.bnb-booking-services-summary {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-grand-total {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-grand-total strong {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|||||||
86
assets/css/blocks-editor.css
Normal file
86
assets/css/blocks-editor.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* WP BnB Block Editor Styles
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Block placeholder styling */
|
||||||
|
.wp-bnb-block-placeholder {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server-side render container */
|
||||||
|
.wp-block-wp-bnb-building,
|
||||||
|
.wp-block-wp-bnb-room,
|
||||||
|
.wp-block-wp-bnb-room-search,
|
||||||
|
.wp-block-wp-bnb-buildings,
|
||||||
|
.wp-block-wp-bnb-rooms {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder in editor */
|
||||||
|
.wp-block-wp-bnb-building .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-room .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-room-search .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-buildings .components-placeholder,
|
||||||
|
.wp-block-wp-bnb-rooms .components-placeholder {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner container */
|
||||||
|
.wp-block-wp-bnb-building .components-spinner,
|
||||||
|
.wp-block-wp-bnb-room .components-spinner,
|
||||||
|
.wp-block-wp-bnb-room-search .components-spinner,
|
||||||
|
.wp-block-wp-bnb-buildings .components-spinner,
|
||||||
|
.wp-block-wp-bnb-rooms .components-spinner {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inspector control sections */
|
||||||
|
.wp-block-wp-bnb-building .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-room .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-room-search .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-buildings .components-panel__body,
|
||||||
|
.wp-block-wp-bnb-rooms .components-panel__body {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select control styling */
|
||||||
|
.wp-block-wp-bnb-building .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-room .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-room-search .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-buildings .components-select-control__input,
|
||||||
|
.wp-block-wp-bnb-rooms .components-select-control__input {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview container in editor */
|
||||||
|
.wp-bnb-editor-preview {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable interactive elements in preview */
|
||||||
|
.wp-bnb-editor-preview a,
|
||||||
|
.wp-bnb-editor-preview button,
|
||||||
|
.wp-bnb-editor-preview input,
|
||||||
|
.wp-bnb-editor-preview select {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add visual indicator that this is a preview */
|
||||||
|
.wp-bnb-editor-preview::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -577,6 +577,363 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize guest search functionality for booking form.
|
||||||
|
*/
|
||||||
|
function initGuestSearch() {
|
||||||
|
var $searchInput = $('#bnb_booking_guest_search');
|
||||||
|
var $searchResults = $('#bnb-guest-search-results');
|
||||||
|
var $guestIdInput = $('#bnb_booking_guest_id');
|
||||||
|
var $linkedGuestInfo = $('#bnb-linked-guest-info');
|
||||||
|
var $searchContainer = $('#bnb-guest-search-container');
|
||||||
|
var $fieldsContainer = $('#bnb-guest-fields-container');
|
||||||
|
var $unlinkBtn = $('#bnb-unlink-guest');
|
||||||
|
var $guestNameInput = $('#bnb_booking_guest_name');
|
||||||
|
var $guestEmailInput = $('#bnb_booking_guest_email');
|
||||||
|
var $guestPhoneInput = $('#bnb_booking_guest_phone');
|
||||||
|
|
||||||
|
// Exit if not on booking form.
|
||||||
|
if (!$searchInput.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchTimer = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform guest search via AJAX.
|
||||||
|
*/
|
||||||
|
function searchGuests() {
|
||||||
|
var query = $searchInput.val().trim();
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
$searchResults.hide().empty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$searchResults.html('<div class="bnb-search-loading">' + wpBnbAdmin.i18n.searchingGuests + '</div>').show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: wpBnbAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_search_guest',
|
||||||
|
nonce: wpBnbAdmin.nonce,
|
||||||
|
search: query
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success && response.data.guests.length > 0) {
|
||||||
|
var html = '<div class="bnb-guest-search-list">';
|
||||||
|
|
||||||
|
$.each(response.data.guests, function(i, guest) {
|
||||||
|
var isBlocked = guest.status === 'blocked';
|
||||||
|
var statusClass = isBlocked ? 'bnb-guest-blocked' : '';
|
||||||
|
var statusLabel = isBlocked ? ' <span class="bnb-blocked-label">' + wpBnbAdmin.i18n.guestBlocked + '</span>' : '';
|
||||||
|
|
||||||
|
html += '<div class="bnb-guest-search-item ' + statusClass + '" data-guest=\'' + JSON.stringify(guest) + '\'>';
|
||||||
|
html += '<div class="bnb-guest-item-info">';
|
||||||
|
html += '<strong>' + escapeHtml(guest.name) + '</strong>' + statusLabel + '<br>';
|
||||||
|
html += '<small>' + escapeHtml(guest.email || '') + '</small>';
|
||||||
|
if (guest.phone) {
|
||||||
|
html += ' <small>(' + escapeHtml(guest.phone) + ')</small>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
if (!isBlocked) {
|
||||||
|
html += '<button type="button" class="button button-small bnb-select-guest">' + wpBnbAdmin.i18n.selectGuest + '</button>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
$searchResults.html(html);
|
||||||
|
} else {
|
||||||
|
$searchResults.html('<div class="bnb-no-guests">' + wpBnbAdmin.i18n.noGuestsFound + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$searchResults.html('<div class="bnb-search-error">' + wpBnbAdmin.i18n.error + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML entities.
|
||||||
|
*
|
||||||
|
* @param {string} text Text to escape.
|
||||||
|
* @return {string} Escaped text.
|
||||||
|
*/
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a guest from search results.
|
||||||
|
*
|
||||||
|
* @param {Object} guest Guest data.
|
||||||
|
*/
|
||||||
|
function selectGuest(guest) {
|
||||||
|
// Set hidden guest ID.
|
||||||
|
$guestIdInput.val(guest.id);
|
||||||
|
|
||||||
|
// Populate guest fields (for display/fallback).
|
||||||
|
$guestNameInput.val(guest.name).prop('readonly', true);
|
||||||
|
$guestEmailInput.val(guest.email).prop('readonly', true);
|
||||||
|
$guestPhoneInput.val(guest.phone).prop('readonly', true);
|
||||||
|
|
||||||
|
// Update linked guest display.
|
||||||
|
var infoHtml = '<p>';
|
||||||
|
infoHtml += '<span class="dashicons dashicons-admin-users"></span> ';
|
||||||
|
infoHtml += '<strong>' + escapeHtml(guest.name) + '</strong> ';
|
||||||
|
infoHtml += '<a href="' + wpBnbAdmin.ajaxUrl.replace('admin-ajax.php', 'post.php?post=' + guest.id + '&action=edit') + '" target="_blank" class="button button-small">View Guest Profile</a> ';
|
||||||
|
infoHtml += '<button type="button" id="bnb-unlink-guest" class="button button-small button-link-delete">Unlink</button>';
|
||||||
|
infoHtml += '</p>';
|
||||||
|
if (guest.email) {
|
||||||
|
infoHtml += '<p><small>' + escapeHtml(guest.email) + '</small></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$linkedGuestInfo.html(infoHtml).show();
|
||||||
|
$searchContainer.hide();
|
||||||
|
$fieldsContainer.hide();
|
||||||
|
$searchResults.hide().empty();
|
||||||
|
$searchInput.val('');
|
||||||
|
|
||||||
|
// Re-bind unlink button.
|
||||||
|
bindUnlinkButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlink guest from booking.
|
||||||
|
*/
|
||||||
|
function unlinkGuest() {
|
||||||
|
$guestIdInput.val('');
|
||||||
|
$guestNameInput.val('').prop('readonly', false);
|
||||||
|
$guestEmailInput.val('').prop('readonly', false);
|
||||||
|
$guestPhoneInput.val('').prop('readonly', false);
|
||||||
|
|
||||||
|
$linkedGuestInfo.hide();
|
||||||
|
$searchContainer.show();
|
||||||
|
$fieldsContainer.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind unlink button event.
|
||||||
|
*/
|
||||||
|
function bindUnlinkButton() {
|
||||||
|
$('#bnb-unlink-guest').off('click').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
unlinkGuest();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search input with debounce.
|
||||||
|
$searchInput.on('input', function() {
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
}
|
||||||
|
searchTimer = setTimeout(searchGuests, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select guest from results.
|
||||||
|
$searchResults.on('click', '.bnb-select-guest', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var guest = $(this).closest('.bnb-guest-search-item').data('guest');
|
||||||
|
if (guest) {
|
||||||
|
selectGuest(guest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial unlink button binding.
|
||||||
|
bindUnlinkButton();
|
||||||
|
|
||||||
|
// Close search results when clicking outside.
|
||||||
|
$(document).on('click', function(e) {
|
||||||
|
if (!$(e.target).closest('#bnb_booking_guest_search, #bnb-guest-search-results').length) {
|
||||||
|
$searchResults.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize service pricing type toggle.
|
||||||
|
*/
|
||||||
|
function initServicePricing() {
|
||||||
|
var $pricingTypeInputs = $('input[name="bnb_service_pricing_type"]');
|
||||||
|
var $priceRow = $('#bnb-service-price-row');
|
||||||
|
var $priceSuffix = $('#bnb-service-price-suffix');
|
||||||
|
var $priceDescription = $('#bnb-service-price-description');
|
||||||
|
|
||||||
|
if (!$pricingTypeInputs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pricingTypeInputs.on('change', function() {
|
||||||
|
var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val();
|
||||||
|
|
||||||
|
if (pricingType === 'included') {
|
||||||
|
$priceRow.hide();
|
||||||
|
} else {
|
||||||
|
$priceRow.show();
|
||||||
|
|
||||||
|
if (pricingType === 'per_night') {
|
||||||
|
$priceSuffix.text(' / ' + (wpBnbAdmin.i18n.night || 'night'));
|
||||||
|
$priceDescription.text(wpBnbAdmin.i18n.perNightDescription || 'This price will be charged per night of the stay.');
|
||||||
|
} else {
|
||||||
|
$priceSuffix.text('');
|
||||||
|
$priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize booking services selector.
|
||||||
|
*/
|
||||||
|
function initBookingServices() {
|
||||||
|
var $servicesSelector = $('.bnb-services-selector');
|
||||||
|
var $servicesList = $servicesSelector.find('.bnb-services-list');
|
||||||
|
var $totalDisplay = $('#bnb-services-total-amount');
|
||||||
|
|
||||||
|
if (!$servicesSelector.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current number of nights from booking form.
|
||||||
|
*
|
||||||
|
* @return {number} Number of nights.
|
||||||
|
*/
|
||||||
|
function getNights() {
|
||||||
|
var checkIn = $('#bnb_booking_check_in').val();
|
||||||
|
var checkOut = $('#bnb_booking_check_out').val();
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
var startDate = new Date(checkIn);
|
||||||
|
var endDate = new Date(checkOut);
|
||||||
|
var nights = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
||||||
|
return Math.max(1, nights);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt($servicesSelector.data('nights'), 10) || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate service line total.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $item Service item element.
|
||||||
|
* @param {number} nights Number of nights.
|
||||||
|
* @return {number} Calculated price.
|
||||||
|
*/
|
||||||
|
function calculateServiceTotal($item, nights) {
|
||||||
|
var price = parseFloat($item.data('price')) || 0;
|
||||||
|
var pricingType = $item.data('pricing-type');
|
||||||
|
var quantity = parseInt($item.find('.bnb-service-qty-input').val(), 10) || 1;
|
||||||
|
|
||||||
|
if (pricingType === 'included') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingType === 'per_night') {
|
||||||
|
return price * quantity * nights;
|
||||||
|
}
|
||||||
|
|
||||||
|
return price * quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update service line total display.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $item Service item element.
|
||||||
|
*/
|
||||||
|
function updateServiceLineTotal($item) {
|
||||||
|
var nights = getNights();
|
||||||
|
var total = calculateServiceTotal($item, nights);
|
||||||
|
var $lineTotal = $item.find('.bnb-service-line-total');
|
||||||
|
var $totalValue = $item.find('.bnb-service-total-value');
|
||||||
|
var isSelected = $item.find('input[type="checkbox"]').is(':checked');
|
||||||
|
var pricingType = $item.data('pricing-type');
|
||||||
|
|
||||||
|
if (isSelected && pricingType !== 'included' && total > 0) {
|
||||||
|
$totalValue.text(formatPrice(total));
|
||||||
|
$lineTotal.show();
|
||||||
|
} else {
|
||||||
|
$lineTotal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update total services amount.
|
||||||
|
*/
|
||||||
|
function updateServicesTotal() {
|
||||||
|
var nights = getNights();
|
||||||
|
var total = 0;
|
||||||
|
|
||||||
|
$servicesList.find('.bnb-service-item').each(function() {
|
||||||
|
var $item = $(this);
|
||||||
|
var isSelected = $item.find('input[type="checkbox"]').is(':checked');
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
total += calculateServiceTotal($item, nights);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$totalDisplay.text(formatPrice(total));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price for display (simple formatting).
|
||||||
|
*
|
||||||
|
* @param {number} price Price value.
|
||||||
|
* @return {string} Formatted price.
|
||||||
|
*/
|
||||||
|
function formatPrice(price) {
|
||||||
|
return parseFloat(price).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle service checkbox change.
|
||||||
|
$servicesList.on('change', 'input[type="checkbox"]', function() {
|
||||||
|
var $item = $(this).closest('.bnb-service-item');
|
||||||
|
var isSelected = $(this).is(':checked');
|
||||||
|
|
||||||
|
$item.toggleClass('selected', isSelected);
|
||||||
|
|
||||||
|
// Show/hide quantity input.
|
||||||
|
var $quantity = $item.find('.bnb-service-quantity');
|
||||||
|
if ($quantity.length) {
|
||||||
|
$quantity.toggle(isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateServiceLineTotal($item);
|
||||||
|
updateServicesTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle quantity change.
|
||||||
|
$servicesList.on('change input', '.bnb-service-qty-input', function() {
|
||||||
|
var $item = $(this).closest('.bnb-service-item');
|
||||||
|
var maxQty = parseInt($item.data('max-quantity'), 10) || 1;
|
||||||
|
var value = parseInt($(this).val(), 10) || 1;
|
||||||
|
|
||||||
|
// Enforce min/max.
|
||||||
|
value = Math.max(1, Math.min(value, maxQty));
|
||||||
|
$(this).val(value);
|
||||||
|
|
||||||
|
updateServiceLineTotal($item);
|
||||||
|
updateServicesTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update when booking dates change.
|
||||||
|
$('#bnb_booking_check_in, #bnb_booking_check_out').on('change', function() {
|
||||||
|
$servicesList.find('.bnb-service-item.selected').each(function() {
|
||||||
|
updateServiceLineTotal($(this));
|
||||||
|
});
|
||||||
|
updateServicesTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial calculation.
|
||||||
|
updateServicesTotal();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
@@ -586,6 +943,9 @@
|
|||||||
initPricingMetaBox();
|
initPricingMetaBox();
|
||||||
initBookingForm();
|
initBookingForm();
|
||||||
initCalendarPage();
|
initCalendarPage();
|
||||||
|
initGuestSearch();
|
||||||
|
initServicePricing();
|
||||||
|
initBookingServices();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
489
assets/js/blocks-editor.js
Normal file
489
assets/js/blocks-editor.js
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
/**
|
||||||
|
* WP BnB Gutenberg Blocks
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function(wp) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { registerBlockType } = wp.blocks;
|
||||||
|
const { createElement, Fragment } = wp.element;
|
||||||
|
const { InspectorControls, useBlockProps } = wp.blockEditor;
|
||||||
|
const { PanelBody, SelectControl, ToggleControl, RangeControl, Placeholder, Spinner } = wp.components;
|
||||||
|
const { ServerSideRender } = wp.editor || wp.serverSideRender;
|
||||||
|
const { __ } = wp.i18n;
|
||||||
|
const el = createElement;
|
||||||
|
|
||||||
|
// Get localized data
|
||||||
|
const { buildings, rooms, roomTypes, i18n } = wpBnbBlocks;
|
||||||
|
|
||||||
|
// Building options for select
|
||||||
|
const buildingOptions = [
|
||||||
|
{ value: 0, label: i18n.selectBuilding },
|
||||||
|
...buildings
|
||||||
|
];
|
||||||
|
|
||||||
|
// Room options for select
|
||||||
|
const roomOptions = [
|
||||||
|
{ value: 0, label: i18n.selectRoom },
|
||||||
|
...rooms.map(r => ({
|
||||||
|
value: r.value,
|
||||||
|
label: r.building ? `${r.label} (${r.building})` : r.label
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
// Room type options
|
||||||
|
const roomTypeOptions = [
|
||||||
|
{ value: '', label: i18n.allTypes },
|
||||||
|
...roomTypes.map(t => ({
|
||||||
|
value: t.slug,
|
||||||
|
label: t.name
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
// Building filter options for rooms block
|
||||||
|
const buildingFilterOptions = [
|
||||||
|
{ value: 0, label: i18n.allBuildings },
|
||||||
|
...buildings
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/building', {
|
||||||
|
title: i18n.buildingBlock,
|
||||||
|
icon: 'building',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
buildingId: { type: 'number', default: 0 },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showAddress: { type: 'boolean', default: true },
|
||||||
|
showRooms: { type: 'boolean', default: true },
|
||||||
|
showContact: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.buildingBlock,
|
||||||
|
value: attributes.buildingId,
|
||||||
|
options: buildingOptions,
|
||||||
|
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAddress,
|
||||||
|
checked: attributes.showAddress,
|
||||||
|
onChange: (value) => setAttributes({ showAddress: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showRooms,
|
||||||
|
checked: attributes.showRooms,
|
||||||
|
onChange: (value) => setAttributes({ showRooms: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showContact,
|
||||||
|
checked: attributes.showContact,
|
||||||
|
onChange: (value) => setAttributes({ showContact: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
attributes.buildingId ?
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/building',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'building', label: i18n.buildingBlock }, el(Spinner))
|
||||||
|
}) :
|
||||||
|
el(Placeholder, { icon: 'building', label: i18n.buildingBlock },
|
||||||
|
buildings.length === 0 ?
|
||||||
|
el('p', {}, i18n.noBuildings) :
|
||||||
|
el(SelectControl, {
|
||||||
|
value: attributes.buildingId,
|
||||||
|
options: buildingOptions,
|
||||||
|
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null; // Server-side rendered
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/room', {
|
||||||
|
title: i18n.roomBlock,
|
||||||
|
icon: 'admin-home',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
roomId: { type: 'number', default: 0 },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showGallery: { type: 'boolean', default: true },
|
||||||
|
showPrice: { type: 'boolean', default: true },
|
||||||
|
showAmenities: { type: 'boolean', default: true },
|
||||||
|
showAvailability: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.roomBlock,
|
||||||
|
value: attributes.roomId,
|
||||||
|
options: roomOptions,
|
||||||
|
onChange: (value) => setAttributes({ roomId: parseInt(value, 10) })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showGallery,
|
||||||
|
checked: attributes.showGallery,
|
||||||
|
onChange: (value) => setAttributes({ showGallery: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showPrice,
|
||||||
|
checked: attributes.showPrice,
|
||||||
|
onChange: (value) => setAttributes({ showPrice: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAmenities,
|
||||||
|
checked: attributes.showAmenities,
|
||||||
|
onChange: (value) => setAttributes({ showAmenities: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAvailability,
|
||||||
|
checked: attributes.showAvailability,
|
||||||
|
onChange: (value) => setAttributes({ showAvailability: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
attributes.roomId ?
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/room',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'admin-home', label: i18n.roomBlock }, el(Spinner))
|
||||||
|
}) :
|
||||||
|
el(Placeholder, { icon: 'admin-home', label: i18n.roomBlock },
|
||||||
|
rooms.length === 0 ?
|
||||||
|
el('p', {}, i18n.noRooms) :
|
||||||
|
el(SelectControl, {
|
||||||
|
value: attributes.roomId,
|
||||||
|
options: roomOptions,
|
||||||
|
onChange: (value) => setAttributes({ roomId: parseInt(value, 10) })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room Search Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/room-search', {
|
||||||
|
title: i18n.roomSearchBlock,
|
||||||
|
icon: 'search',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
layout: { type: 'string', default: 'grid' },
|
||||||
|
columns: { type: 'number', default: 3 },
|
||||||
|
showDates: { type: 'boolean', default: true },
|
||||||
|
showGuests: { type: 'boolean', default: true },
|
||||||
|
showRoomType: { type: 'boolean', default: true },
|
||||||
|
showAmenities: { type: 'boolean', default: true },
|
||||||
|
showPriceRange: { type: 'boolean', default: true },
|
||||||
|
showBuilding: { type: 'boolean', default: true },
|
||||||
|
resultsPerPage: { type: 'number', default: 12 }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.layout,
|
||||||
|
value: attributes.layout,
|
||||||
|
options: [
|
||||||
|
{ value: 'grid', label: i18n.grid },
|
||||||
|
{ value: 'list', label: i18n.list }
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value })
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.columns,
|
||||||
|
value: attributes.columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 4
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.resultsPerPage,
|
||||||
|
value: attributes.resultsPerPage,
|
||||||
|
onChange: (value) => setAttributes({ resultsPerPage: value }),
|
||||||
|
min: 4,
|
||||||
|
max: 48
|
||||||
|
})
|
||||||
|
),
|
||||||
|
el(PanelBody, { title: i18n.filterSettings, initialOpen: false },
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showDates,
|
||||||
|
checked: attributes.showDates,
|
||||||
|
onChange: (value) => setAttributes({ showDates: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showGuests,
|
||||||
|
checked: attributes.showGuests,
|
||||||
|
onChange: (value) => setAttributes({ showGuests: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showRoomType,
|
||||||
|
checked: attributes.showRoomType,
|
||||||
|
onChange: (value) => setAttributes({ showRoomType: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAmenities,
|
||||||
|
checked: attributes.showAmenities,
|
||||||
|
onChange: (value) => setAttributes({ showAmenities: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showPriceRange,
|
||||||
|
checked: attributes.showPriceRange,
|
||||||
|
onChange: (value) => setAttributes({ showPriceRange: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showBuilding,
|
||||||
|
checked: attributes.showBuilding,
|
||||||
|
onChange: (value) => setAttributes({ showBuilding: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/room-search',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'search', label: i18n.roomSearchBlock }, el(Spinner))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buildings List Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/buildings', {
|
||||||
|
title: i18n.buildingsBlock,
|
||||||
|
icon: 'building',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
layout: { type: 'string', default: 'grid' },
|
||||||
|
columns: { type: 'number', default: 3 },
|
||||||
|
limit: { type: 'number', default: -1 },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showAddress: { type: 'boolean', default: true },
|
||||||
|
showRoomsCount: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.layout,
|
||||||
|
value: attributes.layout,
|
||||||
|
options: [
|
||||||
|
{ value: 'grid', label: i18n.grid },
|
||||||
|
{ value: 'list', label: i18n.list }
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value })
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.columns,
|
||||||
|
value: attributes.columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 4
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.limit,
|
||||||
|
value: attributes.limit,
|
||||||
|
onChange: (value) => setAttributes({ limit: value }),
|
||||||
|
min: -1,
|
||||||
|
max: 20
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAddress,
|
||||||
|
checked: attributes.showAddress,
|
||||||
|
onChange: (value) => setAttributes({ showAddress: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showRoomsCount,
|
||||||
|
checked: attributes.showRoomsCount,
|
||||||
|
onChange: (value) => setAttributes({ showRoomsCount: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/buildings',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'building', label: i18n.buildingsBlock }, el(Spinner))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms List Block
|
||||||
|
*/
|
||||||
|
registerBlockType('wp-bnb/rooms', {
|
||||||
|
title: i18n.roomsBlock,
|
||||||
|
icon: 'admin-home',
|
||||||
|
category: 'widgets',
|
||||||
|
attributes: {
|
||||||
|
layout: { type: 'string', default: 'grid' },
|
||||||
|
columns: { type: 'number', default: 3 },
|
||||||
|
limit: { type: 'number', default: 12 },
|
||||||
|
buildingId: { type: 'number', default: 0 },
|
||||||
|
roomType: { type: 'string', default: '' },
|
||||||
|
showImage: { type: 'boolean', default: true },
|
||||||
|
showPrice: { type: 'boolean', default: true },
|
||||||
|
showCapacity: { type: 'boolean', default: true },
|
||||||
|
showAmenities: { type: 'boolean', default: true },
|
||||||
|
showBuilding: { type: 'boolean', default: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: i18n.displaySettings },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.layout,
|
||||||
|
value: attributes.layout,
|
||||||
|
options: [
|
||||||
|
{ value: 'grid', label: i18n.grid },
|
||||||
|
{ value: 'list', label: i18n.list }
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value })
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.columns,
|
||||||
|
value: attributes.columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 4
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: i18n.limit,
|
||||||
|
value: attributes.limit,
|
||||||
|
onChange: (value) => setAttributes({ limit: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 48
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showImage,
|
||||||
|
checked: attributes.showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showPrice,
|
||||||
|
checked: attributes.showPrice,
|
||||||
|
onChange: (value) => setAttributes({ showPrice: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showCapacity,
|
||||||
|
checked: attributes.showCapacity,
|
||||||
|
onChange: (value) => setAttributes({ showCapacity: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showAmenities,
|
||||||
|
checked: attributes.showAmenities,
|
||||||
|
onChange: (value) => setAttributes({ showAmenities: value })
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: i18n.showBuilding,
|
||||||
|
checked: attributes.showBuilding,
|
||||||
|
onChange: (value) => setAttributes({ showBuilding: value })
|
||||||
|
})
|
||||||
|
),
|
||||||
|
el(PanelBody, { title: i18n.filterSettings, initialOpen: false },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.buildingBlock,
|
||||||
|
value: attributes.buildingId,
|
||||||
|
options: buildingFilterOptions,
|
||||||
|
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
|
||||||
|
}),
|
||||||
|
el(SelectControl, {
|
||||||
|
label: i18n.roomType,
|
||||||
|
value: attributes.roomType,
|
||||||
|
options: roomTypeOptions,
|
||||||
|
onChange: (value) => setAttributes({ roomType: value })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', blockProps,
|
||||||
|
el(ServerSideRender, {
|
||||||
|
block: 'wp-bnb/rooms',
|
||||||
|
attributes: attributes,
|
||||||
|
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'admin-home', label: i18n.roomsBlock }, el(Spinner))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window.wp);
|
||||||
@@ -1,12 +1,825 @@
|
|||||||
/**
|
/**
|
||||||
* WP BnB Frontend JavaScript
|
* WP BnB Frontend JavaScript
|
||||||
*
|
*
|
||||||
|
* Handles search forms, calendar widgets, and interactive elements.
|
||||||
|
*
|
||||||
* @package Magdev\WpBnb
|
* @package Magdev\WpBnb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Placeholder - Frontend scripts will be added as features are implemented
|
/**
|
||||||
|
* WP BnB Frontend namespace.
|
||||||
|
*/
|
||||||
|
const WpBnb = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration from localized script.
|
||||||
|
*/
|
||||||
|
config: window.wpBnbFrontend || {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all frontend components.
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.initSearchForms();
|
||||||
|
this.initCalendarWidgets();
|
||||||
|
this.initAvailabilityForms();
|
||||||
|
this.initPriceCalculators();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize room search forms.
|
||||||
|
*/
|
||||||
|
initSearchForms: function() {
|
||||||
|
const forms = document.querySelectorAll('.wp-bnb-search-form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
new SearchForm(form);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize calendar widgets.
|
||||||
|
*/
|
||||||
|
initCalendarWidgets: function() {
|
||||||
|
const calendars = document.querySelectorAll('.wp-bnb-availability-calendar-widget');
|
||||||
|
calendars.forEach(calendar => {
|
||||||
|
new CalendarWidget(calendar);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize availability check forms on single room pages.
|
||||||
|
*/
|
||||||
|
initAvailabilityForms: function() {
|
||||||
|
const forms = document.querySelectorAll('.wp-bnb-availability-check');
|
||||||
|
forms.forEach(form => {
|
||||||
|
new AvailabilityForm(form);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize price calculator forms.
|
||||||
|
*/
|
||||||
|
initPriceCalculators: function() {
|
||||||
|
const calculators = document.querySelectorAll('.wp-bnb-price-calculator');
|
||||||
|
calculators.forEach(calculator => {
|
||||||
|
new PriceCalculator(calculator);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an AJAX request.
|
||||||
|
*
|
||||||
|
* @param {string} action The AJAX action.
|
||||||
|
* @param {Object} data The request data.
|
||||||
|
* @return {Promise} Promise resolving to response data.
|
||||||
|
*/
|
||||||
|
ajax: function(action, data = {}) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', action);
|
||||||
|
formData.append('nonce', this.config.nonce || '');
|
||||||
|
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (data[key] !== null && data[key] !== undefined) {
|
||||||
|
formData.append(key, data[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetch(this.config.ajaxUrl || '/wp-admin/admin-ajax.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'same-origin'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.data?.message || 'Request failed');
|
||||||
|
}
|
||||||
|
return data.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as YYYY-MM-DD.
|
||||||
|
*
|
||||||
|
* @param {Date} date The date object.
|
||||||
|
* @return {string} Formatted date string.
|
||||||
|
*/
|
||||||
|
formatDate: function(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date string.
|
||||||
|
*
|
||||||
|
* @param {string} dateStr Date string in YYYY-MM-DD format.
|
||||||
|
* @return {Date|null} Date object or null if invalid.
|
||||||
|
*/
|
||||||
|
parseDate: function(dateStr) {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
const parts = dateStr.split('-');
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate nights between two dates.
|
||||||
|
*
|
||||||
|
* @param {Date} checkIn Check-in date.
|
||||||
|
* @param {Date} checkOut Check-out date.
|
||||||
|
* @return {number} Number of nights.
|
||||||
|
*/
|
||||||
|
calculateNights: function(checkIn, checkOut) {
|
||||||
|
if (!checkIn || !checkOut) return 0;
|
||||||
|
const diffTime = checkOut.getTime() - checkIn.getTime();
|
||||||
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce a function.
|
||||||
|
*
|
||||||
|
* @param {Function} func The function to debounce.
|
||||||
|
* @param {number} wait Wait time in milliseconds.
|
||||||
|
* @return {Function} Debounced function.
|
||||||
|
*/
|
||||||
|
debounce: function(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Form handler class.
|
||||||
|
*/
|
||||||
|
class SearchForm {
|
||||||
|
constructor(element) {
|
||||||
|
this.form = element;
|
||||||
|
this.resultsContainer = document.querySelector(
|
||||||
|
this.form.dataset.results || '.wp-bnb-search-results'
|
||||||
|
);
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Form submission.
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.search();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Date validation.
|
||||||
|
const checkIn = this.form.querySelector('[name="check_in"]');
|
||||||
|
const checkOut = this.form.querySelector('[name="check_out"]');
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
// Set min date to today.
|
||||||
|
const today = WpBnb.formatDate(new Date());
|
||||||
|
checkIn.setAttribute('min', today);
|
||||||
|
|
||||||
|
checkIn.addEventListener('change', () => {
|
||||||
|
if (checkIn.value) {
|
||||||
|
// Set check-out min to day after check-in.
|
||||||
|
const minCheckOut = WpBnb.parseDate(checkIn.value);
|
||||||
|
if (minCheckOut) {
|
||||||
|
minCheckOut.setDate(minCheckOut.getDate() + 1);
|
||||||
|
checkOut.setAttribute('min', WpBnb.formatDate(minCheckOut));
|
||||||
|
|
||||||
|
// Clear check-out if it's before new minimum.
|
||||||
|
if (checkOut.value && checkOut.value <= checkIn.value) {
|
||||||
|
checkOut.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkOut.addEventListener('change', () => {
|
||||||
|
if (checkOut.value && checkIn.value && checkOut.value <= checkIn.value) {
|
||||||
|
alert(WpBnb.config.i18n?.invalidDateRange || 'Check-out must be after check-in');
|
||||||
|
checkOut.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button.
|
||||||
|
const resetBtn = this.form.querySelector('[type="reset"]');
|
||||||
|
if (resetBtn) {
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.clearResults();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load more button.
|
||||||
|
if (this.resultsContainer) {
|
||||||
|
this.resultsContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('wp-bnb-load-more')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormData() {
|
||||||
|
const formData = new FormData(this.form);
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (value) {
|
||||||
|
// Handle array fields (amenities[]).
|
||||||
|
if (key.endsWith('[]')) {
|
||||||
|
const cleanKey = key.slice(0, -2);
|
||||||
|
if (!data[cleanKey]) {
|
||||||
|
data[cleanKey] = [];
|
||||||
|
}
|
||||||
|
data[cleanKey].push(value);
|
||||||
|
} else {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert arrays to comma-separated strings for AJAX.
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (Array.isArray(data[key])) {
|
||||||
|
data[key] = data[key].join(',');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
search() {
|
||||||
|
if (this.isLoading) return;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.showLoading();
|
||||||
|
|
||||||
|
const data = this.getFormData();
|
||||||
|
data.page = this.currentPage;
|
||||||
|
data.per_page = this.form.dataset.perPage || 12;
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_search_rooms', data)
|
||||||
|
.then(response => {
|
||||||
|
this.renderResults(response, this.currentPage === 1);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.showError(error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.hideLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore() {
|
||||||
|
this.currentPage++;
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResults(response, replace = true) {
|
||||||
|
if (!this.resultsContainer) return;
|
||||||
|
|
||||||
|
const { rooms, total, page, total_pages } = response;
|
||||||
|
|
||||||
|
if (replace) {
|
||||||
|
this.resultsContainer.innerHTML = '';
|
||||||
|
} else {
|
||||||
|
// Remove existing load more button.
|
||||||
|
const existingLoadMore = this.resultsContainer.querySelector('.wp-bnb-load-more-wrapper');
|
||||||
|
if (existingLoadMore) {
|
||||||
|
existingLoadMore.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rooms.length === 0 && replace) {
|
||||||
|
this.resultsContainer.innerHTML = `
|
||||||
|
<div class="wp-bnb-no-results">
|
||||||
|
<p>${WpBnb.config.i18n?.noResults || 'No rooms found matching your criteria.'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create results count.
|
||||||
|
if (replace) {
|
||||||
|
const countEl = document.createElement('div');
|
||||||
|
countEl.className = 'wp-bnb-results-count';
|
||||||
|
countEl.innerHTML = `<p>${WpBnb.config.i18n?.resultsFound?.replace('%d', total) || `${total} rooms found`}</p>`;
|
||||||
|
this.resultsContainer.appendChild(countEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create grid container.
|
||||||
|
let grid = this.resultsContainer.querySelector('.wp-bnb-rooms-grid');
|
||||||
|
if (!grid) {
|
||||||
|
grid = document.createElement('div');
|
||||||
|
grid.className = 'wp-bnb-rooms-grid wp-bnb-grid wp-bnb-grid-3';
|
||||||
|
this.resultsContainer.appendChild(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render room cards.
|
||||||
|
rooms.forEach(room => {
|
||||||
|
const card = this.createRoomCard(room);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add load more button if there are more pages.
|
||||||
|
if (page < total_pages) {
|
||||||
|
const loadMoreWrapper = document.createElement('div');
|
||||||
|
loadMoreWrapper.className = 'wp-bnb-load-more-wrapper';
|
||||||
|
loadMoreWrapper.innerHTML = `
|
||||||
|
<button type="button" class="wp-bnb-load-more wp-bnb-button">
|
||||||
|
${WpBnb.config.i18n?.loadMore || 'Load More'}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
this.resultsContainer.appendChild(loadMoreWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoomCard(room) {
|
||||||
|
const card = document.createElement('article');
|
||||||
|
card.className = 'wp-bnb-room-card';
|
||||||
|
|
||||||
|
let imageHtml = '';
|
||||||
|
if (room.thumbnail) {
|
||||||
|
imageHtml = `
|
||||||
|
<div class="wp-bnb-room-card-image">
|
||||||
|
<a href="${this.escapeHtml(room.permalink)}">
|
||||||
|
<img src="${this.escapeHtml(room.thumbnail)}" alt="${this.escapeHtml(room.title)}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amenitiesHtml = '';
|
||||||
|
if (room.amenities && room.amenities.length > 0) {
|
||||||
|
const amenityItems = room.amenities.slice(0, 4).map(a =>
|
||||||
|
`<span class="wp-bnb-amenity-tag">${this.escapeHtml(a.name)}</span>`
|
||||||
|
).join('');
|
||||||
|
amenitiesHtml = `<div class="wp-bnb-room-card-amenities">${amenityItems}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let priceHtml = '';
|
||||||
|
if (room.price_display) {
|
||||||
|
priceHtml = `
|
||||||
|
<div class="wp-bnb-room-card-price">
|
||||||
|
<span class="wp-bnb-price">${this.escapeHtml(room.price_display)}</span>
|
||||||
|
<span class="wp-bnb-price-unit">/ ${WpBnb.config.i18n?.perNight || 'night'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
${imageHtml}
|
||||||
|
<div class="wp-bnb-room-card-content">
|
||||||
|
<h3 class="wp-bnb-room-card-title">
|
||||||
|
<a href="${this.escapeHtml(room.permalink)}">${this.escapeHtml(room.title)}</a>
|
||||||
|
</h3>
|
||||||
|
${room.building_name ? `<p class="wp-bnb-room-card-building">${this.escapeHtml(room.building_name)}</p>` : ''}
|
||||||
|
<div class="wp-bnb-room-card-meta">
|
||||||
|
${room.capacity ? `<span class="wp-bnb-capacity">${room.capacity} ${WpBnb.config.i18n?.guests || 'guests'}</span>` : ''}
|
||||||
|
${room.room_type ? `<span class="wp-bnb-room-type">${this.escapeHtml(room.room_type)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
${amenitiesHtml}
|
||||||
|
${priceHtml}
|
||||||
|
<a href="${this.escapeHtml(room.permalink)}" class="wp-bnb-room-card-link wp-bnb-button wp-bnb-button-small">
|
||||||
|
${WpBnb.config.i18n?.viewDetails || 'View Details'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
this.form.classList.add('wp-bnb-loading');
|
||||||
|
const submitBtn = this.form.querySelector('[type="submit"]');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.dataset.originalText = submitBtn.textContent;
|
||||||
|
submitBtn.textContent = WpBnb.config.i18n?.searching || 'Searching...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading() {
|
||||||
|
this.form.classList.remove('wp-bnb-loading');
|
||||||
|
const submitBtn = this.form.querySelector('[type="submit"]');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
if (submitBtn.dataset.originalText) {
|
||||||
|
submitBtn.textContent = submitBtn.dataset.originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
if (!this.resultsContainer) return;
|
||||||
|
this.resultsContainer.innerHTML = `
|
||||||
|
<div class="wp-bnb-error">
|
||||||
|
<p>${this.escapeHtml(message)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearResults() {
|
||||||
|
if (this.resultsContainer) {
|
||||||
|
this.resultsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar Widget handler class.
|
||||||
|
*/
|
||||||
|
class CalendarWidget {
|
||||||
|
constructor(element) {
|
||||||
|
this.container = element;
|
||||||
|
this.roomId = element.dataset.roomId;
|
||||||
|
this.currentYear = parseInt(element.querySelector('[data-year]')?.dataset.year) || new Date().getFullYear();
|
||||||
|
this.currentMonth = parseInt(element.querySelector('[data-month]')?.dataset.month) || (new Date().getMonth() + 1);
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Navigation buttons.
|
||||||
|
this.container.addEventListener('click', (e) => {
|
||||||
|
const navBtn = e.target.closest('.wp-bnb-calendar-nav');
|
||||||
|
if (navBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
const direction = navBtn.dataset.direction;
|
||||||
|
if (direction === 'prev') {
|
||||||
|
this.navigatePrev();
|
||||||
|
} else if (direction === 'next') {
|
||||||
|
this.navigateNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigatePrev() {
|
||||||
|
this.currentMonth--;
|
||||||
|
if (this.currentMonth < 1) {
|
||||||
|
this.currentMonth = 12;
|
||||||
|
this.currentYear--;
|
||||||
|
}
|
||||||
|
this.loadCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateNext() {
|
||||||
|
this.currentMonth++;
|
||||||
|
if (this.currentMonth > 12) {
|
||||||
|
this.currentMonth = 1;
|
||||||
|
this.currentYear++;
|
||||||
|
}
|
||||||
|
this.loadCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCalendar() {
|
||||||
|
this.container.classList.add('wp-bnb-loading');
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_get_calendar', {
|
||||||
|
room_id: this.roomId,
|
||||||
|
year: this.currentYear,
|
||||||
|
month: this.currentMonth
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
this.renderCalendar(response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Calendar load error:', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.container.classList.remove('wp-bnb-loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCalendar(data) {
|
||||||
|
const monthContainer = this.container.querySelector('.wp-bnb-calendar-month');
|
||||||
|
if (!monthContainer) return;
|
||||||
|
|
||||||
|
// Update month/year attributes.
|
||||||
|
monthContainer.dataset.year = this.currentYear;
|
||||||
|
monthContainer.dataset.month = this.currentMonth;
|
||||||
|
|
||||||
|
// Update month name.
|
||||||
|
const monthNameEl = monthContainer.querySelector('.wp-bnb-calendar-month-name');
|
||||||
|
if (monthNameEl) {
|
||||||
|
monthNameEl.textContent = `${data.month_name} ${this.currentYear}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild calendar grid.
|
||||||
|
const tbody = monthContainer.querySelector('.wp-bnb-calendar-grid tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
let day = 1;
|
||||||
|
const totalDays = data.days_in_month;
|
||||||
|
const firstDay = data.first_day_of_week;
|
||||||
|
const weeks = Math.ceil((firstDay + totalDays) / 7);
|
||||||
|
|
||||||
|
for (let week = 0; week < weeks; week++) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
for (let dow = 0; dow < 7; dow++) {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
const cellIndex = week * 7 + dow;
|
||||||
|
|
||||||
|
if (cellIndex < firstDay || day > totalDays) {
|
||||||
|
td.className = 'wp-bnb-calendar-empty';
|
||||||
|
} else {
|
||||||
|
const dayData = data.days[day];
|
||||||
|
const classes = ['wp-bnb-calendar-day'];
|
||||||
|
|
||||||
|
if (dayData) {
|
||||||
|
if (dayData.is_booked) {
|
||||||
|
classes.push('wp-bnb-booked');
|
||||||
|
} else {
|
||||||
|
classes.push('wp-bnb-available');
|
||||||
|
}
|
||||||
|
if (dayData.is_past) {
|
||||||
|
classes.push('wp-bnb-past');
|
||||||
|
}
|
||||||
|
if (dayData.is_today) {
|
||||||
|
classes.push('wp-bnb-today');
|
||||||
|
}
|
||||||
|
td.dataset.date = dayData.date || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
td.className = classes.join(' ');
|
||||||
|
td.textContent = day;
|
||||||
|
day++;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.appendChild(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability Form handler class.
|
||||||
|
* For checking availability on single room pages.
|
||||||
|
*/
|
||||||
|
class AvailabilityForm {
|
||||||
|
constructor(element) {
|
||||||
|
this.form = element;
|
||||||
|
this.roomId = element.dataset.roomId;
|
||||||
|
this.resultContainer = element.querySelector('.wp-bnb-availability-result');
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.checkAvailability();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Date validation.
|
||||||
|
const checkIn = this.form.querySelector('[name="check_in"]');
|
||||||
|
const checkOut = this.form.querySelector('[name="check_out"]');
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
const today = WpBnb.formatDate(new Date());
|
||||||
|
checkIn.setAttribute('min', today);
|
||||||
|
|
||||||
|
checkIn.addEventListener('change', () => {
|
||||||
|
if (checkIn.value) {
|
||||||
|
const minCheckOut = WpBnb.parseDate(checkIn.value);
|
||||||
|
if (minCheckOut) {
|
||||||
|
minCheckOut.setDate(minCheckOut.getDate() + 1);
|
||||||
|
checkOut.setAttribute('min', WpBnb.formatDate(minCheckOut));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clearResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkOut.addEventListener('change', () => {
|
||||||
|
this.clearResult();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAvailability() {
|
||||||
|
const checkIn = this.form.querySelector('[name="check_in"]')?.value;
|
||||||
|
const checkOut = this.form.querySelector('[name="check_out"]')?.value;
|
||||||
|
|
||||||
|
if (!checkIn || !checkOut) {
|
||||||
|
this.showResult('error', WpBnb.config.i18n?.selectDates || 'Please select check-in and check-out dates.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkOut <= checkIn) {
|
||||||
|
this.showResult('error', WpBnb.config.i18n?.invalidDateRange || 'Check-out must be after check-in.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.classList.add('wp-bnb-loading');
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_get_availability', {
|
||||||
|
room_id: this.roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.available) {
|
||||||
|
let message = WpBnb.config.i18n?.available || 'Room is available!';
|
||||||
|
if (response.price_display) {
|
||||||
|
message += ` ${WpBnb.config.i18n?.totalPrice || 'Total'}: ${response.price_display}`;
|
||||||
|
}
|
||||||
|
this.showResult('success', message, response);
|
||||||
|
} else {
|
||||||
|
this.showResult('error', WpBnb.config.i18n?.notAvailable || 'Sorry, the room is not available for these dates.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.showResult('error', error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.form.classList.remove('wp-bnb-loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult(type, message, data = null) {
|
||||||
|
if (!this.resultContainer) return;
|
||||||
|
|
||||||
|
let html = `<div class="wp-bnb-availability-${type}">${this.escapeHtml(message)}</div>`;
|
||||||
|
|
||||||
|
if (type === 'success' && data && data.booking_url) {
|
||||||
|
html += `
|
||||||
|
<a href="${this.escapeHtml(data.booking_url)}" class="wp-bnb-button wp-bnb-book-now">
|
||||||
|
${WpBnb.config.i18n?.bookNow || 'Book Now'}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resultContainer.innerHTML = html;
|
||||||
|
this.resultContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
clearResult() {
|
||||||
|
if (this.resultContainer) {
|
||||||
|
this.resultContainer.innerHTML = '';
|
||||||
|
this.resultContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price Calculator handler class.
|
||||||
|
*/
|
||||||
|
class PriceCalculator {
|
||||||
|
constructor(element) {
|
||||||
|
this.container = element;
|
||||||
|
this.roomId = element.dataset.roomId;
|
||||||
|
this.priceDisplay = element.querySelector('.wp-bnb-calculated-price');
|
||||||
|
this.breakdownDisplay = element.querySelector('.wp-bnb-price-breakdown');
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const checkIn = this.container.querySelector('[name="check_in"]');
|
||||||
|
const checkOut = this.container.querySelector('[name="check_out"]');
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
const debouncedCalculate = WpBnb.debounce(() => this.calculate(), 300);
|
||||||
|
|
||||||
|
checkIn.addEventListener('change', debouncedCalculate);
|
||||||
|
checkOut.addEventListener('change', debouncedCalculate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
const checkIn = this.container.querySelector('[name="check_in"]')?.value;
|
||||||
|
const checkOut = this.container.querySelector('[name="check_out"]')?.value;
|
||||||
|
|
||||||
|
if (!checkIn || !checkOut || checkOut <= checkIn) {
|
||||||
|
this.clearDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.classList.add('wp-bnb-loading');
|
||||||
|
|
||||||
|
WpBnb.ajax('wp_bnb_calculate_price', {
|
||||||
|
room_id: this.roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
this.displayPrice(response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Price calculation error:', error);
|
||||||
|
this.clearDisplay();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.container.classList.remove('wp-bnb-loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displayPrice(data) {
|
||||||
|
if (this.priceDisplay) {
|
||||||
|
this.priceDisplay.innerHTML = `
|
||||||
|
<span class="wp-bnb-price-label">${WpBnb.config.i18n?.total || 'Total'}:</span>
|
||||||
|
<span class="wp-bnb-price-amount">${this.escapeHtml(data.formatted_total)}</span>
|
||||||
|
`;
|
||||||
|
this.priceDisplay.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.breakdownDisplay && data.breakdown) {
|
||||||
|
let breakdownHtml = '<ul class="wp-bnb-breakdown-list">';
|
||||||
|
|
||||||
|
if (data.breakdown.nights) {
|
||||||
|
breakdownHtml += `<li>${data.breakdown.nights} ${WpBnb.config.i18n?.nights || 'nights'}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.tier) {
|
||||||
|
breakdownHtml += `<li>${this.escapeHtml(data.breakdown.tier)}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.base_total) {
|
||||||
|
breakdownHtml += `<li>${WpBnb.config.i18n?.basePrice || 'Base'}: ${this.escapeHtml(data.breakdown.base_total)}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.weekend_total && parseFloat(data.breakdown.weekend_total) > 0) {
|
||||||
|
breakdownHtml += `<li>${WpBnb.config.i18n?.weekendSurcharge || 'Weekend surcharge'}: ${this.escapeHtml(data.breakdown.weekend_total)}</li>`;
|
||||||
|
}
|
||||||
|
if (data.breakdown.season_name) {
|
||||||
|
breakdownHtml += `<li>${WpBnb.config.i18n?.season || 'Season'}: ${this.escapeHtml(data.breakdown.season_name)}</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakdownHtml += '</ul>';
|
||||||
|
|
||||||
|
this.breakdownDisplay.innerHTML = breakdownHtml;
|
||||||
|
this.breakdownDisplay.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDisplay() {
|
||||||
|
if (this.priceDisplay) {
|
||||||
|
this.priceDisplay.innerHTML = '';
|
||||||
|
this.priceDisplay.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (this.breakdownDisplay) {
|
||||||
|
this.breakdownDisplay.innerHTML = '';
|
||||||
|
this.breakdownDisplay.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on DOM ready.
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => WpBnb.init());
|
||||||
|
} else {
|
||||||
|
WpBnb.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose to global scope for potential external use.
|
||||||
|
window.WpBnb = WpBnb;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
465
src/Blocks/BlockRegistrar.php
Normal file
465
src/Blocks/BlockRegistrar.php
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Block registrar.
|
||||||
|
*
|
||||||
|
* Handles registration of all Gutenberg blocks.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Blocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Blocks;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block registrar class.
|
||||||
|
*/
|
||||||
|
final class BlockRegistrar {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize block registration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register_blocks' ) );
|
||||||
|
add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all blocks.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_blocks(): void {
|
||||||
|
// Building block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/building',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'buildingId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0,
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAddress' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showRooms' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showContact' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_building_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Room block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/room',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'roomId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0,
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showGallery' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showPrice' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAmenities' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAvailability' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_room_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Room Search block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/room-search',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'layout' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'grid',
|
||||||
|
),
|
||||||
|
'columns' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 3,
|
||||||
|
),
|
||||||
|
'showDates' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showGuests' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showRoomType' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAmenities' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showPriceRange' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showBuilding' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'resultsPerPage' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_room_search_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buildings List block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/buildings',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'layout' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'grid',
|
||||||
|
),
|
||||||
|
'columns' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 3,
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => -1,
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAddress' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showRoomsCount' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_buildings_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rooms List block.
|
||||||
|
register_block_type(
|
||||||
|
'wp-bnb/rooms',
|
||||||
|
array(
|
||||||
|
'attributes' => array(
|
||||||
|
'layout' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'grid',
|
||||||
|
),
|
||||||
|
'columns' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 3,
|
||||||
|
),
|
||||||
|
'limit' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 12,
|
||||||
|
),
|
||||||
|
'buildingId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0,
|
||||||
|
),
|
||||||
|
'roomType' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '',
|
||||||
|
),
|
||||||
|
'showImage' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showPrice' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showCapacity' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showAmenities' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
'showBuilding' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'render_callback' => array( self::class, 'render_rooms_block' ),
|
||||||
|
'editor_script' => 'wp-bnb-blocks-editor',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue editor assets.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function enqueue_editor_assets(): void {
|
||||||
|
// Register the editor script.
|
||||||
|
wp_register_script(
|
||||||
|
'wp-bnb-blocks-editor',
|
||||||
|
WP_BNB_URL . 'assets/js/blocks-editor.js',
|
||||||
|
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data' ),
|
||||||
|
WP_BNB_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get buildings and rooms for selectors.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$form_data = Search::get_search_form_data();
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
'wp-bnb-blocks-editor',
|
||||||
|
'wpBnbBlocks',
|
||||||
|
array(
|
||||||
|
'buildings' => array_map(
|
||||||
|
function ( $building ) {
|
||||||
|
return array(
|
||||||
|
'value' => $building->ID,
|
||||||
|
'label' => $building->post_title,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$buildings
|
||||||
|
),
|
||||||
|
'rooms' => array_map(
|
||||||
|
function ( $room ) {
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
return array(
|
||||||
|
'value' => $room->ID,
|
||||||
|
'label' => $room->post_title,
|
||||||
|
'building' => $building ? $building->post_title : '',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$rooms
|
||||||
|
),
|
||||||
|
'roomTypes' => $form_data['room_types'],
|
||||||
|
'amenities' => $form_data['amenities'],
|
||||||
|
'i18n' => array(
|
||||||
|
'selectBuilding' => __( 'Select a building', 'wp-bnb' ),
|
||||||
|
'selectRoom' => __( 'Select a room', 'wp-bnb' ),
|
||||||
|
'noBuildings' => __( 'No buildings found. Create a building first.', 'wp-bnb' ),
|
||||||
|
'noRooms' => __( 'No rooms found. Create a room first.', 'wp-bnb' ),
|
||||||
|
'buildingBlock' => __( 'Building', 'wp-bnb' ),
|
||||||
|
'roomBlock' => __( 'Room', 'wp-bnb' ),
|
||||||
|
'roomSearchBlock' => __( 'Room Search', 'wp-bnb' ),
|
||||||
|
'buildingsBlock' => __( 'Buildings List', 'wp-bnb' ),
|
||||||
|
'roomsBlock' => __( 'Rooms List', 'wp-bnb' ),
|
||||||
|
'displaySettings' => __( 'Display Settings', 'wp-bnb' ),
|
||||||
|
'filterSettings' => __( 'Filter Settings', 'wp-bnb' ),
|
||||||
|
'layout' => __( 'Layout', 'wp-bnb' ),
|
||||||
|
'grid' => __( 'Grid', 'wp-bnb' ),
|
||||||
|
'list' => __( 'List', 'wp-bnb' ),
|
||||||
|
'columns' => __( 'Columns', 'wp-bnb' ),
|
||||||
|
'limit' => __( 'Limit', 'wp-bnb' ),
|
||||||
|
'showImage' => __( 'Show image', 'wp-bnb' ),
|
||||||
|
'showAddress' => __( 'Show address', 'wp-bnb' ),
|
||||||
|
'showRooms' => __( 'Show rooms', 'wp-bnb' ),
|
||||||
|
'showRoomsCount' => __( 'Show rooms count', 'wp-bnb' ),
|
||||||
|
'showContact' => __( 'Show contact', 'wp-bnb' ),
|
||||||
|
'showGallery' => __( 'Show gallery', 'wp-bnb' ),
|
||||||
|
'showPrice' => __( 'Show price', 'wp-bnb' ),
|
||||||
|
'showAmenities' => __( 'Show amenities', 'wp-bnb' ),
|
||||||
|
'showAvailability' => __( 'Show availability', 'wp-bnb' ),
|
||||||
|
'showCapacity' => __( 'Show capacity', 'wp-bnb' ),
|
||||||
|
'showBuilding' => __( 'Show building', 'wp-bnb' ),
|
||||||
|
'showDates' => __( 'Show date filter', 'wp-bnb' ),
|
||||||
|
'showGuests' => __( 'Show guests filter', 'wp-bnb' ),
|
||||||
|
'showRoomType' => __( 'Show room type filter', 'wp-bnb' ),
|
||||||
|
'showPriceRange' => __( 'Show price range filter', 'wp-bnb' ),
|
||||||
|
'resultsPerPage' => __( 'Results per page', 'wp-bnb' ),
|
||||||
|
'roomType' => __( 'Room Type', 'wp-bnb' ),
|
||||||
|
'allTypes' => __( 'All Types', 'wp-bnb' ),
|
||||||
|
'allBuildings' => __( 'All Buildings', 'wp-bnb' ),
|
||||||
|
'previewPlaceholder' => __( 'Preview will appear here', 'wp-bnb' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Editor styles.
|
||||||
|
wp_enqueue_style(
|
||||||
|
'wp-bnb-blocks-editor',
|
||||||
|
WP_BNB_URL . 'assets/css/blocks-editor.css',
|
||||||
|
array(),
|
||||||
|
WP_BNB_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render building block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_building_block( array $attributes ): string {
|
||||||
|
$building_id = $attributes['buildingId'] ?? 0;
|
||||||
|
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return '<p class="wp-bnb-block-placeholder">' . esc_html__( 'Please select a building.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shortcodes::render_single_building(
|
||||||
|
array(
|
||||||
|
'id' => $building_id,
|
||||||
|
'show_rooms' => ( $attributes['showRooms'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_address' => ( $attributes['showAddress'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_contact' => ( $attributes['showContact'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_room_block( array $attributes ): string {
|
||||||
|
$room_id = $attributes['roomId'] ?? 0;
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return '<p class="wp-bnb-block-placeholder">' . esc_html__( 'Please select a room.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shortcodes::render_single_room(
|
||||||
|
array(
|
||||||
|
'id' => $room_id,
|
||||||
|
'show_gallery' => ( $attributes['showGallery'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_pricing' => ( $attributes['showPrice'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_availability' => ( $attributes['showAvailability'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room search block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_room_search_block( array $attributes ): string {
|
||||||
|
return Shortcodes::render_room_search(
|
||||||
|
array(
|
||||||
|
'layout' => $attributes['layout'] ?? 'grid',
|
||||||
|
'columns' => $attributes['columns'] ?? 3,
|
||||||
|
'show_dates' => ( $attributes['showDates'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_guests' => ( $attributes['showGuests'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_room_type' => ( $attributes['showRoomType'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_price_range' => ( $attributes['showPriceRange'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_building' => ( $attributes['showBuilding'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'results_per_page' => $attributes['resultsPerPage'] ?? 12,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render buildings list block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_buildings_block( array $attributes ): string {
|
||||||
|
return Shortcodes::render_buildings(
|
||||||
|
array(
|
||||||
|
'layout' => $attributes['layout'] ?? 'grid',
|
||||||
|
'columns' => $attributes['columns'] ?? 3,
|
||||||
|
'limit' => $attributes['limit'] ?? -1,
|
||||||
|
'show_image' => ( $attributes['showImage'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_address' => ( $attributes['showAddress'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_rooms_count' => ( $attributes['showRoomsCount'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render rooms list block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_rooms_block( array $attributes ): string {
|
||||||
|
return Shortcodes::render_rooms(
|
||||||
|
array(
|
||||||
|
'layout' => $attributes['layout'] ?? 'grid',
|
||||||
|
'columns' => $attributes['columns'] ?? 3,
|
||||||
|
'limit' => $attributes['limit'] ?? 12,
|
||||||
|
'building_id' => $attributes['buildingId'] ?? 0,
|
||||||
|
'room_type' => $attributes['roomType'] ?? '',
|
||||||
|
'show_image' => ( $attributes['showImage'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_price' => ( $attributes['showPrice'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_capacity' => ( $attributes['showCapacity'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||||
|
'show_building' => ( $attributes['showBuilding'] ?? true ) ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ declare( strict_types=1 );
|
|||||||
namespace Magdev\WpBnb\Booking;
|
namespace Magdev\WpBnb\Booking;
|
||||||
|
|
||||||
use Magdev\WpBnb\PostTypes\Booking;
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
use Magdev\WpBnb\PostTypes\Room;
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
use Magdev\WpBnb\Pricing\Calculator;
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
@@ -203,8 +204,8 @@ final class EmailNotifier {
|
|||||||
* @return array Booking data.
|
* @return array Booking data.
|
||||||
*/
|
*/
|
||||||
private static function get_booking_data( int $booking_id ): array {
|
private static function get_booking_data( int $booking_id ): array {
|
||||||
$booking = get_post( $booking_id );
|
$booking = get_post( $booking_id );
|
||||||
$room = Booking::get_room( $booking_id );
|
$room = Booking::get_room( $booking_id );
|
||||||
$building = Booking::get_building( $booking_id );
|
$building = Booking::get_building( $booking_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 );
|
||||||
@@ -221,31 +222,84 @@ final class EmailNotifier {
|
|||||||
|
|
||||||
$statuses = Booking::get_booking_statuses();
|
$statuses = Booking::get_booking_statuses();
|
||||||
|
|
||||||
|
// Get guest data - prefer Guest CPT if linked, fallback to booking meta.
|
||||||
|
$guest_data = self::get_guest_data( $booking_id );
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'booking_id' => $booking_id,
|
'booking_id' => $booking_id,
|
||||||
'booking_reference' => $booking ? $booking->post_title : '',
|
'booking_reference' => $booking ? $booking->post_title : '',
|
||||||
'guest_name' => get_post_meta( $booking_id, '_bnb_booking_guest_name', true ),
|
'guest_name' => $guest_data['name'],
|
||||||
'guest_email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ),
|
'guest_first_name' => $guest_data['first_name'],
|
||||||
'guest_phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ),
|
'guest_last_name' => $guest_data['last_name'],
|
||||||
'guest_notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ),
|
'guest_email' => $guest_data['email'],
|
||||||
'adults' => $adults ?: 1,
|
'guest_phone' => $guest_data['phone'],
|
||||||
'children' => $children ?: 0,
|
'guest_notes' => $guest_data['notes'],
|
||||||
'room_name' => $room ? $room->post_title : '',
|
'guest_full_address' => $guest_data['full_address'],
|
||||||
'room_id' => $room ? $room->ID : 0,
|
'adults' => $adults ?: 1,
|
||||||
'building_name' => $building ? $building->post_title : '',
|
'children' => $children ?: 0,
|
||||||
'building_id' => $building ? $building->ID : 0,
|
'room_name' => $room ? $room->post_title : '',
|
||||||
'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '',
|
'room_id' => $room ? $room->ID : 0,
|
||||||
'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '',
|
'building_name' => $building ? $building->post_title : '',
|
||||||
'check_in_raw' => $check_in,
|
'building_id' => $building ? $building->ID : 0,
|
||||||
'check_out_raw' => $check_out,
|
'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '',
|
||||||
'nights' => $nights,
|
'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '',
|
||||||
'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '',
|
'check_in_raw' => $check_in,
|
||||||
'status' => $statuses[ $status ] ?? $status,
|
'check_out_raw' => $check_out,
|
||||||
'status_raw' => $status,
|
'nights' => $nights,
|
||||||
'site_name' => get_bloginfo( 'name' ),
|
'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '',
|
||||||
'site_url' => home_url(),
|
'status' => $statuses[ $status ] ?? $status,
|
||||||
'admin_email' => get_option( 'admin_email' ),
|
'status_raw' => $status,
|
||||||
'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ),
|
'site_name' => get_bloginfo( 'name' ),
|
||||||
|
'site_url' => home_url(),
|
||||||
|
'admin_email' => get_option( 'admin_email' ),
|
||||||
|
'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest data from Guest CPT or booking meta.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return array Guest data with keys: name, first_name, last_name, email, phone, notes, full_address.
|
||||||
|
*/
|
||||||
|
private static function get_guest_data( int $booking_id ): array {
|
||||||
|
$guest_id = get_post_meta( $booking_id, '_bnb_booking_guest_id', true );
|
||||||
|
|
||||||
|
// Try to get data from Guest CPT.
|
||||||
|
if ( $guest_id ) {
|
||||||
|
$guest = get_post( $guest_id );
|
||||||
|
if ( $guest && Guest::POST_TYPE === $guest->post_type ) {
|
||||||
|
$first_name = get_post_meta( $guest_id, '_bnb_guest_first_name', true );
|
||||||
|
$last_name = get_post_meta( $guest_id, '_bnb_guest_last_name', true );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'name' => Guest::get_full_name( $guest_id ),
|
||||||
|
'first_name' => $first_name,
|
||||||
|
'last_name' => $last_name,
|
||||||
|
'email' => get_post_meta( $guest_id, '_bnb_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $guest_id, '_bnb_guest_phone', true ),
|
||||||
|
'notes' => get_post_meta( $guest_id, '_bnb_guest_notes', true ),
|
||||||
|
'full_address' => Guest::get_formatted_address( $guest_id ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to booking meta (legacy bookings).
|
||||||
|
$guest_name = get_post_meta( $booking_id, '_bnb_booking_guest_name', true );
|
||||||
|
|
||||||
|
// Try to split name into first/last for legacy data.
|
||||||
|
$name_parts = explode( ' ', $guest_name, 2 );
|
||||||
|
$first_name = $name_parts[0] ?? '';
|
||||||
|
$last_name = $name_parts[1] ?? '';
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'name' => $guest_name,
|
||||||
|
'first_name' => $first_name,
|
||||||
|
'last_name' => $last_name,
|
||||||
|
'email' => get_post_meta( $booking_id, '_bnb_booking_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $booking_id, '_bnb_booking_guest_phone', true ),
|
||||||
|
'notes' => get_post_meta( $booking_id, '_bnb_booking_guest_notes', true ),
|
||||||
|
'full_address' => '', // Legacy bookings don't have full address.
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
677
src/Frontend/Search.php
Normal file
677
src/Frontend/Search.php
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Frontend room search.
|
||||||
|
*
|
||||||
|
* Handles room search with availability checking and filtering.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search class for frontend room searches.
|
||||||
|
*/
|
||||||
|
final class Search {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the search system.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Public AJAX handlers (no login required).
|
||||||
|
add_action( 'wp_ajax_wp_bnb_search_rooms', array( self::class, 'ajax_search_rooms' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_search_rooms', array( self::class, 'ajax_search_rooms' ) );
|
||||||
|
|
||||||
|
add_action( 'wp_ajax_wp_bnb_get_availability', array( self::class, 'ajax_get_availability' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_get_availability', array( self::class, 'ajax_get_availability' ) );
|
||||||
|
|
||||||
|
add_action( 'wp_ajax_wp_bnb_get_calendar', array( self::class, 'ajax_get_calendar' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_get_calendar', array( self::class, 'ajax_get_calendar' ) );
|
||||||
|
|
||||||
|
add_action( 'wp_ajax_wp_bnb_calculate_price', array( self::class, 'ajax_calculate_price' ) );
|
||||||
|
add_action( 'wp_ajax_nopriv_wp_bnb_calculate_price', array( self::class, 'ajax_calculate_price' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for rooms with filters.
|
||||||
|
*
|
||||||
|
* @param array $args Search arguments.
|
||||||
|
* @return array Array of room data.
|
||||||
|
*/
|
||||||
|
public static function search( array $args = array() ): array {
|
||||||
|
$defaults = array(
|
||||||
|
'check_in' => '',
|
||||||
|
'check_out' => '',
|
||||||
|
'guests' => 0,
|
||||||
|
'room_type' => '',
|
||||||
|
'amenities' => array(),
|
||||||
|
'price_min' => 0,
|
||||||
|
'price_max' => 0,
|
||||||
|
'building_id' => 0,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'limit' => -1,
|
||||||
|
'offset' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
|
// Build base query.
|
||||||
|
$query_args = array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => (int) $args['limit'],
|
||||||
|
'offset' => (int) $args['offset'],
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
),
|
||||||
|
'tax_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by building.
|
||||||
|
if ( ! empty( $args['building_id'] ) ) {
|
||||||
|
$query_args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => (int) $args['building_id'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by capacity.
|
||||||
|
if ( ! empty( $args['guests'] ) && (int) $args['guests'] > 0 ) {
|
||||||
|
$query_args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_capacity',
|
||||||
|
'value' => (int) $args['guests'],
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'NUMERIC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by room status (only available rooms).
|
||||||
|
$query_args['meta_query'][] = array(
|
||||||
|
'relation' => 'OR',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'value' => 'available',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'compare' => 'NOT EXISTS',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by room type.
|
||||||
|
if ( ! empty( $args['room_type'] ) ) {
|
||||||
|
$query_args['tax_query'][] = array(
|
||||||
|
'taxonomy' => RoomType::TAXONOMY,
|
||||||
|
'field' => is_numeric( $args['room_type'] ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $args['room_type'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by amenities (all must match).
|
||||||
|
if ( ! empty( $args['amenities'] ) ) {
|
||||||
|
$amenities = is_array( $args['amenities'] ) ? $args['amenities'] : explode( ',', $args['amenities'] );
|
||||||
|
$amenities = array_map( 'trim', $amenities );
|
||||||
|
$amenities = array_filter( $amenities );
|
||||||
|
|
||||||
|
if ( ! empty( $amenities ) ) {
|
||||||
|
$query_args['tax_query'][] = array(
|
||||||
|
'taxonomy' => Amenity::TAXONOMY,
|
||||||
|
'field' => is_numeric( $amenities[0] ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $amenities,
|
||||||
|
'operator' => 'AND',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ordering.
|
||||||
|
switch ( $args['orderby'] ) {
|
||||||
|
case 'price':
|
||||||
|
$query_args['meta_key'] = '_bnb_room_price_' . PricingTier::SHORT_TERM->value;
|
||||||
|
$query_args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
case 'capacity':
|
||||||
|
$query_args['meta_key'] = '_bnb_room_capacity';
|
||||||
|
$query_args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
$query_args['orderby'] = 'date';
|
||||||
|
break;
|
||||||
|
case 'random':
|
||||||
|
$query_args['orderby'] = 'rand';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$query_args['orderby'] = 'title';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query_args['order'] = strtoupper( $args['order'] ) === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
|
// Execute query.
|
||||||
|
$rooms = get_posts( $query_args );
|
||||||
|
|
||||||
|
// Filter by availability if dates provided.
|
||||||
|
if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||||
|
$rooms = self::filter_by_availability( $rooms, $args['check_in'], $args['check_out'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by price range.
|
||||||
|
if ( ( ! empty( $args['price_min'] ) || ! empty( $args['price_max'] ) ) && ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||||
|
$rooms = self::filter_by_price_range(
|
||||||
|
$rooms,
|
||||||
|
(float) $args['price_min'],
|
||||||
|
(float) $args['price_max'],
|
||||||
|
$args['check_in'],
|
||||||
|
$args['check_out']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result array with room data.
|
||||||
|
$results = array();
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$results[] = self::get_room_data( $room, $args['check_in'], $args['check_out'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rooms by availability.
|
||||||
|
*
|
||||||
|
* @param array $rooms Array of WP_Post objects.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @return array Filtered rooms.
|
||||||
|
*/
|
||||||
|
public static function filter_by_availability( array $rooms, string $check_in, string $check_out ): array {
|
||||||
|
return array_filter(
|
||||||
|
$rooms,
|
||||||
|
function ( $room ) use ( $check_in, $check_out ) {
|
||||||
|
return Availability::is_available( $room->ID, $check_in, $check_out );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rooms by price range.
|
||||||
|
*
|
||||||
|
* @param array $rooms Array of WP_Post objects.
|
||||||
|
* @param float $min Minimum price.
|
||||||
|
* @param float $max Maximum price.
|
||||||
|
* @param string $check_in Check-in date.
|
||||||
|
* @param string $check_out Check-out date.
|
||||||
|
* @return array Filtered rooms.
|
||||||
|
*/
|
||||||
|
public static function filter_by_price_range( array $rooms, float $min, float $max, string $check_in, string $check_out ): array {
|
||||||
|
return array_filter(
|
||||||
|
$rooms,
|
||||||
|
function ( $room ) use ( $min, $max, $check_in, $check_out ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room->ID, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
|
||||||
|
if ( $min > 0 && $price < $min ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( $max > 0 && $price > $max ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete room data for display.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @param string $check_in Optional check-in date.
|
||||||
|
* @param string $check_out Optional check-out date.
|
||||||
|
* @return array Room data array.
|
||||||
|
*/
|
||||||
|
public static function get_room_data( \WP_Post $room, string $check_in = '', string $check_out = '' ): array {
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
|
||||||
|
// Get room types.
|
||||||
|
$room_types = wp_get_post_terms( $room->ID, RoomType::TAXONOMY, array( 'fields' => 'names' ) );
|
||||||
|
|
||||||
|
// Get amenities with icons.
|
||||||
|
$amenities = wp_get_post_terms( $room->ID, Amenity::TAXONOMY );
|
||||||
|
$amenity_list = array();
|
||||||
|
foreach ( $amenities as $amenity ) {
|
||||||
|
$amenity_list[] = array(
|
||||||
|
'id' => $amenity->term_id,
|
||||||
|
'name' => $amenity->name,
|
||||||
|
'slug' => $amenity->slug,
|
||||||
|
'icon' => get_term_meta( $amenity->term_id, 'amenity_icon', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gallery images.
|
||||||
|
$gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true );
|
||||||
|
$gallery = array();
|
||||||
|
if ( $gallery_ids ) {
|
||||||
|
$ids = explode( ',', $gallery_ids );
|
||||||
|
foreach ( $ids as $id ) {
|
||||||
|
$image = wp_get_attachment_image_src( (int) $id, 'large' );
|
||||||
|
if ( $image ) {
|
||||||
|
$gallery[] = array(
|
||||||
|
'id' => (int) $id,
|
||||||
|
'url' => $image[0],
|
||||||
|
'width' => $image[1],
|
||||||
|
'height' => $image[2],
|
||||||
|
'thumb' => wp_get_attachment_image_src( (int) $id, 'thumbnail' )[0] ?? $image[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get pricing.
|
||||||
|
$pricing = Calculator::getRoomPricing( $room->ID );
|
||||||
|
$nightly_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? null;
|
||||||
|
|
||||||
|
// Calculate stay price if dates provided.
|
||||||
|
$stay_price = null;
|
||||||
|
$nights = 0;
|
||||||
|
if ( ! empty( $check_in ) && ! empty( $check_out ) ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room->ID, $check_in, $check_out );
|
||||||
|
$stay_price = $calculator->calculate();
|
||||||
|
$nights = $calculator->getNights();
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$stay_price = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => $room->post_title,
|
||||||
|
'slug' => $room->post_name,
|
||||||
|
'excerpt' => get_the_excerpt( $room ),
|
||||||
|
'content' => apply_filters( 'the_content', $room->post_content ),
|
||||||
|
'permalink' => get_permalink( $room->ID ),
|
||||||
|
'featured_image' => get_the_post_thumbnail_url( $room->ID, 'large' ),
|
||||||
|
'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'medium' ),
|
||||||
|
'gallery' => $gallery,
|
||||||
|
'building' => $building ? array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => $building->post_title,
|
||||||
|
'permalink' => get_permalink( $building->ID ),
|
||||||
|
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||||
|
) : null,
|
||||||
|
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
|
||||||
|
'floor' => (int) get_post_meta( $room->ID, '_bnb_room_floor', true ),
|
||||||
|
'capacity' => (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ),
|
||||||
|
'max_adults' => (int) get_post_meta( $room->ID, '_bnb_room_max_adults', true ),
|
||||||
|
'max_children' => (int) get_post_meta( $room->ID, '_bnb_room_max_children', true ),
|
||||||
|
'size' => (float) get_post_meta( $room->ID, '_bnb_room_size', true ),
|
||||||
|
'beds' => get_post_meta( $room->ID, '_bnb_room_beds', true ),
|
||||||
|
'bathrooms' => (float) get_post_meta( $room->ID, '_bnb_room_bathrooms', true ),
|
||||||
|
'room_types' => $room_types,
|
||||||
|
'amenities' => $amenity_list,
|
||||||
|
'nightly_price' => $nightly_price,
|
||||||
|
'price_formatted' => $nightly_price ? Calculator::formatPrice( $nightly_price ) : null,
|
||||||
|
'stay_price' => $stay_price,
|
||||||
|
'stay_price_formatted' => $stay_price ? Calculator::formatPrice( $stay_price ) : null,
|
||||||
|
'nights' => $nights,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data for search form (room types, amenities, buildings).
|
||||||
|
*
|
||||||
|
* @return array Form data.
|
||||||
|
*/
|
||||||
|
public static function get_search_form_data(): array {
|
||||||
|
// Get all room types.
|
||||||
|
$room_types = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => RoomType::TAXONOMY,
|
||||||
|
'hide_empty' => true,
|
||||||
|
'orderby' => 'meta_value_num',
|
||||||
|
'meta_key' => 'room_type_sort_order',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all amenities.
|
||||||
|
$amenities = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => Amenity::TAXONOMY,
|
||||||
|
'hide_empty' => true,
|
||||||
|
'orderby' => 'name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all buildings with rooms.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter buildings to only those with rooms.
|
||||||
|
$buildings_with_rooms = array();
|
||||||
|
foreach ( $buildings as $building ) {
|
||||||
|
$rooms = Room::get_rooms_for_building( $building->ID );
|
||||||
|
if ( ! empty( $rooms ) ) {
|
||||||
|
$buildings_with_rooms[] = array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => $building->post_title,
|
||||||
|
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get price range from all rooms.
|
||||||
|
$price_range = self::get_price_range();
|
||||||
|
|
||||||
|
// Get capacity range.
|
||||||
|
$capacity_range = self::get_capacity_range();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'room_types' => array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'parent' => $term->parent,
|
||||||
|
'count' => $term->count,
|
||||||
|
'capacity' => (int) get_term_meta( $term->term_id, 'room_type_base_capacity', true ),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$room_types
|
||||||
|
),
|
||||||
|
'amenities' => array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'icon' => get_term_meta( $term->term_id, 'amenity_icon', true ),
|
||||||
|
'count' => $term->count,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$amenities
|
||||||
|
),
|
||||||
|
'buildings' => $buildings_with_rooms,
|
||||||
|
'price_range' => $price_range,
|
||||||
|
'capacity_range' => $capacity_range,
|
||||||
|
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price range from all rooms.
|
||||||
|
*
|
||||||
|
* @return array Min and max prices.
|
||||||
|
*/
|
||||||
|
public static function get_price_range(): array {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$meta_key = '_bnb_room_price_' . PricingTier::SHORT_TERM->value;
|
||||||
|
|
||||||
|
$result = $wpdb->get_row(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT MIN(CAST(meta_value AS DECIMAL(10,2))) as min_price,
|
||||||
|
MAX(CAST(meta_value AS DECIMAL(10,2))) as max_price
|
||||||
|
FROM {$wpdb->postmeta} pm
|
||||||
|
JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||||
|
WHERE pm.meta_key = %s
|
||||||
|
AND pm.meta_value != ''
|
||||||
|
AND pm.meta_value > 0
|
||||||
|
AND p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'",
|
||||||
|
$meta_key,
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'min' => $result ? (float) $result->min_price : 0,
|
||||||
|
'max' => $result ? (float) $result->max_price : 500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get capacity range from all rooms.
|
||||||
|
*
|
||||||
|
* @return array Min and max capacity.
|
||||||
|
*/
|
||||||
|
public static function get_capacity_range(): array {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_row(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT MIN(CAST(meta_value AS UNSIGNED)) as min_capacity,
|
||||||
|
MAX(CAST(meta_value AS UNSIGNED)) as max_capacity
|
||||||
|
FROM {$wpdb->postmeta} pm
|
||||||
|
JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||||
|
WHERE pm.meta_key = '_bnb_room_capacity'
|
||||||
|
AND pm.meta_value != ''
|
||||||
|
AND p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'",
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'min' => $result && $result->min_capacity ? (int) $result->min_capacity : 1,
|
||||||
|
'max' => $result && $result->max_capacity ? (int) $result->max_capacity : 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for room search.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_search_rooms(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$args = array(
|
||||||
|
'check_in' => isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '',
|
||||||
|
'check_out' => isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '',
|
||||||
|
'guests' => isset( $_POST['guests'] ) ? absint( $_POST['guests'] ) : 0,
|
||||||
|
'room_type' => isset( $_POST['room_type'] ) ? sanitize_text_field( wp_unslash( $_POST['room_type'] ) ) : '',
|
||||||
|
'amenities' => isset( $_POST['amenities'] ) ? array_map( 'sanitize_text_field', (array) $_POST['amenities'] ) : array(),
|
||||||
|
'price_min' => isset( $_POST['price_min'] ) ? (float) $_POST['price_min'] : 0,
|
||||||
|
'price_max' => isset( $_POST['price_max'] ) ? (float) $_POST['price_max'] : 0,
|
||||||
|
'building_id' => isset( $_POST['building_id'] ) ? absint( $_POST['building_id'] ) : 0,
|
||||||
|
'orderby' => isset( $_POST['orderby'] ) ? sanitize_text_field( wp_unslash( $_POST['orderby'] ) ) : 'title',
|
||||||
|
'order' => isset( $_POST['order'] ) ? sanitize_text_field( wp_unslash( $_POST['order'] ) ) : 'ASC',
|
||||||
|
'limit' => isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 12,
|
||||||
|
'offset' => isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0,
|
||||||
|
);
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
// Validate dates if provided.
|
||||||
|
if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||||
|
$check_in = strtotime( $args['check_in'] );
|
||||||
|
$check_out = strtotime( $args['check_out'] );
|
||||||
|
|
||||||
|
if ( ! $check_in || ! $check_out || $check_out <= $check_in ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Invalid date range.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $check_in < strtotime( 'today' ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Check-in date cannot be in the past.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = self::search( $args );
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'rooms' => $results,
|
||||||
|
'count' => count( $results ),
|
||||||
|
'args' => $args,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for availability check.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_get_availability(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||||
|
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||||
|
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = Availability::is_available( $room_id, $check_in, $check_out );
|
||||||
|
|
||||||
|
$result = array(
|
||||||
|
'available' => $available,
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $available ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
|
||||||
|
$result['price'] = $price;
|
||||||
|
$result['price_formatted'] = Calculator::formatPrice( $price );
|
||||||
|
$result['nights'] = $calculator->getNights();
|
||||||
|
$result['breakdown'] = $calculator->getBreakdown();
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$result['price_error'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for calendar data.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_get_calendar(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||||
|
$year = isset( $_POST['year'] ) ? absint( $_POST['year'] ) : (int) gmdate( 'Y' );
|
||||||
|
$month = isset( $_POST['month'] ) ? absint( $_POST['month'] ) : (int) gmdate( 'n' );
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Room ID is required.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate month.
|
||||||
|
$month = max( 1, min( 12, $month ) );
|
||||||
|
|
||||||
|
// Get calendar data.
|
||||||
|
$calendar = Availability::get_calendar_data( $room_id, $year, $month );
|
||||||
|
|
||||||
|
// Simplify for frontend (remove booking details, just show availability).
|
||||||
|
$days = array();
|
||||||
|
foreach ( $calendar['days'] as $day_num => $day_data ) {
|
||||||
|
$days[ $day_num ] = array(
|
||||||
|
'date' => $day_data['date'],
|
||||||
|
'day' => $day_data['day'],
|
||||||
|
'available' => ! $day_data['is_booked'],
|
||||||
|
'is_past' => $day_data['is_past'],
|
||||||
|
'is_today' => $day_data['is_today'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'month_name' => $calendar['month_name'],
|
||||||
|
'days_in_month' => $calendar['days_in_month'],
|
||||||
|
'first_day_of_week' => $calendar['first_day_of_week'],
|
||||||
|
'days' => $days,
|
||||||
|
'prev_month' => $calendar['prev_month'],
|
||||||
|
'next_month' => $calendar['next_month'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for price calculation.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_calculate_price(): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||||
|
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||||
|
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||||
|
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
|
||||||
|
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
$breakdown = $calculator->getBreakdown();
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $calculator->getNights(),
|
||||||
|
'price' => $price,
|
||||||
|
'price_formatted' => Calculator::formatPrice( $price ),
|
||||||
|
'tier' => $breakdown['tier'] ?? null,
|
||||||
|
'breakdown' => $breakdown,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => $e->getMessage() )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
867
src/Frontend/Shortcodes.php
Normal file
867
src/Frontend/Shortcodes.php
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Frontend shortcodes.
|
||||||
|
*
|
||||||
|
* Handles all shortcode registration and rendering.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcodes class.
|
||||||
|
*/
|
||||||
|
final class Shortcodes {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize shortcodes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_shortcode( 'bnb_buildings', array( self::class, 'render_buildings' ) );
|
||||||
|
add_shortcode( 'bnb_rooms', array( self::class, 'render_rooms' ) );
|
||||||
|
add_shortcode( 'bnb_room_search', array( self::class, 'render_room_search' ) );
|
||||||
|
add_shortcode( 'bnb_building', array( self::class, 'render_single_building' ) );
|
||||||
|
add_shortcode( 'bnb_room', array( self::class, 'render_single_room' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render buildings list/grid shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_buildings( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'limit' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'show_image' => 'yes',
|
||||||
|
'show_address' => 'yes',
|
||||||
|
'show_rooms_count' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_buildings'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query buildings.
|
||||||
|
$query_args = array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => (int) $atts['limit'],
|
||||||
|
'orderby' => sanitize_text_field( $atts['orderby'] ),
|
||||||
|
'order' => strtoupper( $atts['order'] ) === 'DESC' ? 'DESC' : 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$buildings = get_posts( $query_args );
|
||||||
|
|
||||||
|
if ( empty( $buildings ) ) {
|
||||||
|
return '<p class="wp-bnb-no-results">' . esc_html__( 'No buildings found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$layout = sanitize_text_field( $atts['layout'] );
|
||||||
|
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||||
|
|
||||||
|
$classes = array(
|
||||||
|
'wp-bnb-buildings',
|
||||||
|
'wp-bnb-buildings-' . $layout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( 'grid' === $layout ) {
|
||||||
|
$classes[] = 'wp-bnb-columns-' . $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<?php echo self::render_building_card( $building, $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single building card.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $building Building post.
|
||||||
|
* @param array $atts Display attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
private static function render_building_card( \WP_Post $building, array $atts ): string {
|
||||||
|
$show_image = 'yes' === $atts['show_image'];
|
||||||
|
$show_address = 'yes' === $atts['show_address'];
|
||||||
|
$show_rooms_count = 'yes' === $atts['show_rooms_count'];
|
||||||
|
|
||||||
|
// Get room count.
|
||||||
|
$rooms = Room::get_rooms_for_building( $building->ID );
|
||||||
|
$room_count = count( $rooms );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-building-card">
|
||||||
|
<?php if ( $show_image && has_post_thumbnail( $building->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-image">
|
||||||
|
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>">
|
||||||
|
<?php echo get_the_post_thumbnail( $building->ID, 'medium_large' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-building-content">
|
||||||
|
<h3 class="wp-bnb-building-title">
|
||||||
|
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>">
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<?php if ( $show_address ) : ?>
|
||||||
|
<?php
|
||||||
|
$city = get_post_meta( $building->ID, '_bnb_building_city', true );
|
||||||
|
$country = get_post_meta( $building->ID, '_bnb_building_country', true );
|
||||||
|
if ( $city || $country ) :
|
||||||
|
$countries = Building::get_countries();
|
||||||
|
$country_name = $countries[ $country ] ?? $country;
|
||||||
|
?>
|
||||||
|
<p class="wp-bnb-building-address">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<?php echo esc_html( implode( ', ', array_filter( array( $city, $country_name ) ) ) ); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_rooms_count && $room_count > 0 ) : ?>
|
||||||
|
<p class="wp-bnb-building-rooms">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of rooms */
|
||||||
|
esc_html( _n( '%d room', '%d rooms', $room_count, 'wp-bnb' ) ),
|
||||||
|
(int) $room_count
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( has_excerpt( $building->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-excerpt">
|
||||||
|
<?php echo wp_kses_post( get_the_excerpt( $building->ID ) ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>" class="wp-bnb-button">
|
||||||
|
<?php esc_html_e( 'View Details', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render rooms list/grid shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_rooms( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'limit' => 12,
|
||||||
|
'building_id' => 0,
|
||||||
|
'room_type' => '',
|
||||||
|
'amenities' => '',
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'show_image' => 'yes',
|
||||||
|
'show_price' => 'yes',
|
||||||
|
'show_capacity' => 'yes',
|
||||||
|
'show_amenities' => 'yes',
|
||||||
|
'show_building' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_rooms'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use search function for filtering.
|
||||||
|
$search_args = array(
|
||||||
|
'building_id' => (int) $atts['building_id'],
|
||||||
|
'room_type' => sanitize_text_field( $atts['room_type'] ),
|
||||||
|
'amenities' => $atts['amenities'] ? explode( ',', $atts['amenities'] ) : array(),
|
||||||
|
'orderby' => sanitize_text_field( $atts['orderby'] ),
|
||||||
|
'order' => $atts['order'],
|
||||||
|
'limit' => (int) $atts['limit'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = Search::search( $search_args );
|
||||||
|
|
||||||
|
if ( empty( $rooms ) ) {
|
||||||
|
return '<p class="wp-bnb-no-results">' . esc_html__( 'No rooms found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$layout = sanitize_text_field( $atts['layout'] );
|
||||||
|
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||||
|
|
||||||
|
$classes = array(
|
||||||
|
'wp-bnb-rooms',
|
||||||
|
'wp-bnb-rooms-' . $layout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( 'grid' === $layout ) {
|
||||||
|
$classes[] = 'wp-bnb-columns-' . $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||||
|
<?php foreach ( $rooms as $room_data ) : ?>
|
||||||
|
<?php echo self::render_room_card( $room_data, $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single room card.
|
||||||
|
*
|
||||||
|
* @param array $room Room data array.
|
||||||
|
* @param array $atts Display attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
private static function render_room_card( array $room, array $atts ): string {
|
||||||
|
$show_image = 'yes' === $atts['show_image'];
|
||||||
|
$show_price = 'yes' === $atts['show_price'];
|
||||||
|
$show_capacity = 'yes' === $atts['show_capacity'];
|
||||||
|
$show_amenities = 'yes' === $atts['show_amenities'];
|
||||||
|
$show_building = 'yes' === $atts['show_building'];
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-room-card" data-room-id="<?php echo esc_attr( $room['id'] ); ?>">
|
||||||
|
<?php if ( $show_image && ! empty( $room['featured_image'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-image">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<img src="<?php echo esc_url( $room['featured_image'] ); ?>" alt="<?php echo esc_attr( $room['title'] ); ?>">
|
||||||
|
</a>
|
||||||
|
<?php if ( ! empty( $room['room_types'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-type-badge"><?php echo esc_html( $room['room_types'][0] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-content">
|
||||||
|
<h3 class="wp-bnb-room-title">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<?php if ( $show_building && ! empty( $room['building'] ) ) : ?>
|
||||||
|
<p class="wp-bnb-room-building">
|
||||||
|
<span class="dashicons dashicons-building"></span>
|
||||||
|
<a href="<?php echo esc_url( $room['building']['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room['building']['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( ! empty( $room['building']['city'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-city">, <?php echo esc_html( $room['building']['city'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-meta">
|
||||||
|
<?php if ( $show_capacity && ! empty( $room['capacity'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-capacity">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of guests */
|
||||||
|
esc_html( _n( '%d guest', '%d guests', $room['capacity'], 'wp-bnb' ) ),
|
||||||
|
(int) $room['capacity']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['size'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-size">
|
||||||
|
<span class="dashicons dashicons-editor-expand"></span>
|
||||||
|
<?php echo esc_html( $room['size'] ); ?> m²
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['beds'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-beds">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
<?php echo esc_html( $room['beds'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( $show_amenities && ! empty( $room['amenities'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-amenities">
|
||||||
|
<?php foreach ( array_slice( $room['amenities'], 0, 4 ) as $amenity ) : ?>
|
||||||
|
<span class="wp-bnb-amenity" title="<?php echo esc_attr( $amenity['name'] ); ?>">
|
||||||
|
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="wp-bnb-amenity-name"><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if ( count( $room['amenities'] ) > 4 ) : ?>
|
||||||
|
<span class="wp-bnb-amenity-more">
|
||||||
|
+<?php echo (int) ( count( $room['amenities'] ) - 4 ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-footer">
|
||||||
|
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-price">
|
||||||
|
<span class="wp-bnb-price-amount"><?php echo esc_html( $room['price_formatted'] ); ?></span>
|
||||||
|
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>" class="wp-bnb-button">
|
||||||
|
<?php esc_html_e( 'View Details', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render room search form with results.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_room_search( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'show_dates' => 'yes',
|
||||||
|
'show_guests' => 'yes',
|
||||||
|
'show_room_type' => 'yes',
|
||||||
|
'show_amenities' => 'yes',
|
||||||
|
'show_price_range' => 'yes',
|
||||||
|
'show_building' => 'yes',
|
||||||
|
'results_per_page' => 12,
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_room_search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get search form data.
|
||||||
|
$form_data = Search::get_search_form_data();
|
||||||
|
|
||||||
|
$layout = sanitize_text_field( $atts['layout'] );
|
||||||
|
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-room-search" data-layout="<?php echo esc_attr( $layout ); ?>" data-columns="<?php echo esc_attr( $columns ); ?>" data-per-page="<?php echo esc_attr( $atts['results_per_page'] ); ?>">
|
||||||
|
<form class="wp-bnb-search-form" id="wp-bnb-search-form">
|
||||||
|
<div class="wp-bnb-search-fields">
|
||||||
|
<?php if ( 'yes' === $atts['show_dates'] ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-dates">
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-check-in"><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-check-in" name="check_in" min="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-check-out"><?php esc_html_e( 'Check-out', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-check-out" name="check_out" min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_guests'] ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-guests">
|
||||||
|
<label for="wp-bnb-guests"><?php esc_html_e( 'Guests', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-guests" name="guests">
|
||||||
|
<option value=""><?php esc_html_e( 'Any', 'wp-bnb' ); ?></option>
|
||||||
|
<?php for ( $i = 1; $i <= $form_data['capacity_range']['max']; $i++ ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $i ); ?>">
|
||||||
|
<?php echo esc_html( $i ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_room_type'] && ! empty( $form_data['room_types'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-room-type">
|
||||||
|
<label for="wp-bnb-room-type"><?php esc_html_e( 'Room Type', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-room-type" name="room_type">
|
||||||
|
<option value=""><?php esc_html_e( 'All Types', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $form_data['room_types'] as $type ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $type['slug'] ); ?>">
|
||||||
|
<?php echo esc_html( $type['name'] ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_building'] && ! empty( $form_data['buildings'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-building">
|
||||||
|
<label for="wp-bnb-building"><?php esc_html_e( 'Building', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-building" name="building_id">
|
||||||
|
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $form_data['buildings'] as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building['id'] ); ?>">
|
||||||
|
<?php echo esc_html( $building['title'] ); ?>
|
||||||
|
<?php if ( ! empty( $building['city'] ) ) : ?>
|
||||||
|
(<?php echo esc_html( $building['city'] ); ?>)
|
||||||
|
<?php endif; ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_price_range'] && $form_data['price_range']['max'] > 0 ) : ?>
|
||||||
|
<div class="wp-bnb-field wp-bnb-field-price-range">
|
||||||
|
<label><?php esc_html_e( 'Price Range', 'wp-bnb' ); ?></label>
|
||||||
|
<div class="wp-bnb-price-range-inputs">
|
||||||
|
<input type="number" id="wp-bnb-price-min" name="price_min" placeholder="<?php esc_attr_e( 'Min', 'wp-bnb' ); ?>" min="0" step="10">
|
||||||
|
<span class="wp-bnb-price-separator">-</span>
|
||||||
|
<input type="number" id="wp-bnb-price-max" name="price_max" placeholder="<?php esc_attr_e( 'Max', 'wp-bnb' ); ?>" min="0" step="10">
|
||||||
|
<span class="wp-bnb-currency"><?php echo esc_html( $form_data['currency'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( 'yes' === $atts['show_amenities'] && ! empty( $form_data['amenities'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-search-amenities">
|
||||||
|
<label><?php esc_html_e( 'Amenities', 'wp-bnb' ); ?></label>
|
||||||
|
<div class="wp-bnb-amenities-list">
|
||||||
|
<?php foreach ( $form_data['amenities'] as $amenity ) : ?>
|
||||||
|
<label class="wp-bnb-amenity-checkbox">
|
||||||
|
<input type="checkbox" name="amenities[]" value="<?php echo esc_attr( $amenity['slug'] ); ?>">
|
||||||
|
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-actions">
|
||||||
|
<button type="submit" class="wp-bnb-button wp-bnb-button-primary">
|
||||||
|
<span class="dashicons dashicons-search"></span>
|
||||||
|
<?php esc_html_e( 'Search Rooms', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
<button type="reset" class="wp-bnb-button wp-bnb-button-secondary">
|
||||||
|
<?php esc_html_e( 'Clear', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-results-container">
|
||||||
|
<div class="wp-bnb-search-status">
|
||||||
|
<span class="wp-bnb-results-count"></span>
|
||||||
|
<div class="wp-bnb-sort-options">
|
||||||
|
<label for="wp-bnb-sort"><?php esc_html_e( 'Sort by:', 'wp-bnb' ); ?></label>
|
||||||
|
<select id="wp-bnb-sort" name="orderby">
|
||||||
|
<option value="title"><?php esc_html_e( 'Name', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="price"><?php esc_html_e( 'Price', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="capacity"><?php esc_html_e( 'Capacity', 'wp-bnb' ); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-results wp-bnb-rooms wp-bnb-rooms-<?php echo esc_attr( $layout ); ?> wp-bnb-columns-<?php echo esc_attr( $columns ); ?>">
|
||||||
|
<div class="wp-bnb-loading">
|
||||||
|
<span class="wp-bnb-spinner"></span>
|
||||||
|
<span><?php esc_html_e( 'Loading rooms...', 'wp-bnb' ); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-search-pagination">
|
||||||
|
<button type="button" class="wp-bnb-button wp-bnb-load-more" style="display:none;">
|
||||||
|
<?php esc_html_e( 'Load More', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render single building shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_single_building( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'id' => 0,
|
||||||
|
'show_rooms' => 'yes',
|
||||||
|
'show_address' => 'yes',
|
||||||
|
'show_contact' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_building'
|
||||||
|
);
|
||||||
|
|
||||||
|
$building_id = (int) $atts['id'];
|
||||||
|
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Building ID is required.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
|
||||||
|
if ( ! $building || Building::POST_TYPE !== $building->post_type ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Building not found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$show_rooms = 'yes' === $atts['show_rooms'];
|
||||||
|
$show_address = 'yes' === $atts['show_address'];
|
||||||
|
$show_contact = 'yes' === $atts['show_contact'];
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-building-single">
|
||||||
|
<?php if ( has_post_thumbnail( $building->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-featured-image">
|
||||||
|
<?php echo get_the_post_thumbnail( $building->ID, 'large' ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-building-header">
|
||||||
|
<h2 class="wp-bnb-building-title"><?php echo esc_html( $building->post_title ); ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-building-details">
|
||||||
|
<?php if ( $show_address ) : ?>
|
||||||
|
<?php $address = Building::get_formatted_address( $building->ID ); ?>
|
||||||
|
<?php if ( ! empty( $address ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-address">
|
||||||
|
<h4><?php esc_html_e( 'Address', 'wp-bnb' ); ?></h4>
|
||||||
|
<address><?php echo nl2br( esc_html( $address ) ); ?></address>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_contact ) : ?>
|
||||||
|
<?php
|
||||||
|
$phone = get_post_meta( $building->ID, '_bnb_building_phone', true );
|
||||||
|
$email = get_post_meta( $building->ID, '_bnb_building_email', true );
|
||||||
|
$website = get_post_meta( $building->ID, '_bnb_building_website', true );
|
||||||
|
?>
|
||||||
|
<?php if ( $phone || $email || $website ) : ?>
|
||||||
|
<div class="wp-bnb-building-contact">
|
||||||
|
<h4><?php esc_html_e( 'Contact', 'wp-bnb' ); ?></h4>
|
||||||
|
<?php if ( $phone ) : ?>
|
||||||
|
<p class="wp-bnb-contact-phone">
|
||||||
|
<span class="dashicons dashicons-phone"></span>
|
||||||
|
<a href="tel:<?php echo esc_attr( $phone ); ?>"><?php echo esc_html( $phone ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $email ) : ?>
|
||||||
|
<p class="wp-bnb-contact-email">
|
||||||
|
<span class="dashicons dashicons-email"></span>
|
||||||
|
<a href="mailto:<?php echo esc_attr( $email ); ?>"><?php echo esc_html( $email ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $website ) : ?>
|
||||||
|
<p class="wp-bnb-contact-website">
|
||||||
|
<span class="dashicons dashicons-admin-site"></span>
|
||||||
|
<a href="<?php echo esc_url( $website ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $website ); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$check_in_time = get_post_meta( $building->ID, '_bnb_building_check_in_time', true );
|
||||||
|
$check_out_time = get_post_meta( $building->ID, '_bnb_building_check_out_time', true );
|
||||||
|
if ( $check_in_time || $check_out_time ) :
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-building-times">
|
||||||
|
<h4><?php esc_html_e( 'Check-in / Check-out', 'wp-bnb' ); ?></h4>
|
||||||
|
<?php if ( $check_in_time ) : ?>
|
||||||
|
<p><strong><?php esc_html_e( 'Check-in:', 'wp-bnb' ); ?></strong> <?php echo esc_html( $check_in_time ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $check_out_time ) : ?>
|
||||||
|
<p><strong><?php esc_html_e( 'Check-out:', 'wp-bnb' ); ?></strong> <?php echo esc_html( $check_out_time ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $building->post_content ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-description">
|
||||||
|
<?php echo wp_kses_post( apply_filters( 'the_content', $building->post_content ) ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_rooms ) : ?>
|
||||||
|
<?php $rooms = Room::get_rooms_for_building( $building->ID ); ?>
|
||||||
|
<?php if ( ! empty( $rooms ) ) : ?>
|
||||||
|
<div class="wp-bnb-building-rooms">
|
||||||
|
<h3><?php esc_html_e( 'Available Rooms', 'wp-bnb' ); ?></h3>
|
||||||
|
<?php
|
||||||
|
echo self::render_rooms(
|
||||||
|
array(
|
||||||
|
'building_id' => $building->ID,
|
||||||
|
'show_building' => 'no',
|
||||||
|
'limit' => -1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render single room shortcode.
|
||||||
|
*
|
||||||
|
* @param array $atts Shortcode attributes.
|
||||||
|
* @return string HTML output.
|
||||||
|
*/
|
||||||
|
public static function render_single_room( $atts ): string {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'id' => 0,
|
||||||
|
'show_gallery' => 'yes',
|
||||||
|
'show_pricing' => 'yes',
|
||||||
|
'show_amenities' => 'yes',
|
||||||
|
'show_availability' => 'yes',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'bnb_room'
|
||||||
|
);
|
||||||
|
|
||||||
|
$room_id = (int) $atts['id'];
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Room ID is required.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||||
|
return '<p class="wp-bnb-error">' . esc_html__( 'Room not found.', 'wp-bnb' ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$show_gallery = 'yes' === $atts['show_gallery'];
|
||||||
|
$show_pricing = 'yes' === $atts['show_pricing'];
|
||||||
|
$show_amenities = 'yes' === $atts['show_amenities'];
|
||||||
|
$show_availability = 'yes' === $atts['show_availability'];
|
||||||
|
|
||||||
|
// Get room data.
|
||||||
|
$room_data = Search::get_room_data( $room );
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-room-single" data-room-id="<?php echo esc_attr( $room->ID ); ?>">
|
||||||
|
<?php if ( $show_gallery && ( has_post_thumbnail( $room->ID ) || ! empty( $room_data['gallery'] ) ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-gallery">
|
||||||
|
<?php if ( has_post_thumbnail( $room->ID ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-featured-image">
|
||||||
|
<?php echo get_the_post_thumbnail( $room->ID, 'large' ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['gallery'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-gallery-thumbnails">
|
||||||
|
<?php foreach ( $room_data['gallery'] as $image ) : ?>
|
||||||
|
<a href="<?php echo esc_url( $image['url'] ); ?>" class="wp-bnb-gallery-thumb" data-gallery>
|
||||||
|
<img src="<?php echo esc_url( $image['thumb'] ); ?>" alt="">
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-header">
|
||||||
|
<div class="wp-bnb-room-header-content">
|
||||||
|
<h2 class="wp-bnb-room-title"><?php echo esc_html( $room->post_title ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['building'] ) ) : ?>
|
||||||
|
<p class="wp-bnb-room-building">
|
||||||
|
<span class="dashicons dashicons-building"></span>
|
||||||
|
<a href="<?php echo esc_url( $room_data['building']['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room_data['building']['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( ! empty( $room_data['building']['city'] ) ) : ?>
|
||||||
|
<span>, <?php echo esc_html( $room_data['building']['city'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['room_types'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-room-type"><?php echo esc_html( implode( ', ', $room_data['room_types'] ) ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( $show_pricing && ! empty( $room_data['price_formatted'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-header-price">
|
||||||
|
<span class="wp-bnb-price-label"><?php esc_html_e( 'From', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-price-amount"><?php echo esc_html( $room_data['price_formatted'] ); ?></span>
|
||||||
|
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-bnb-room-info">
|
||||||
|
<div class="wp-bnb-room-specs">
|
||||||
|
<?php if ( ! empty( $room_data['capacity'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Capacity', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of guests */
|
||||||
|
esc_html( _n( '%d guest', '%d guests', $room_data['capacity'], 'wp-bnb' ) ),
|
||||||
|
(int) $room_data['capacity']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['size'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-editor-expand"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Size', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['size'] ); ?> m²</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['beds'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Beds', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['beds'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['bathrooms'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-admin-page"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Bathrooms', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['bathrooms'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room_data['floor'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-spec">
|
||||||
|
<span class="dashicons dashicons-building"></span>
|
||||||
|
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Floor', 'wp-bnb' ); ?></span>
|
||||||
|
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['floor'] ); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( $show_amenities && ! empty( $room_data['amenities'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-amenities-full">
|
||||||
|
<h4><?php esc_html_e( 'Amenities', 'wp-bnb' ); ?></h4>
|
||||||
|
<ul class="wp-bnb-amenities-list">
|
||||||
|
<?php foreach ( $room_data['amenities'] as $amenity ) : ?>
|
||||||
|
<li class="wp-bnb-amenity">
|
||||||
|
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room->post_content ) ) : ?>
|
||||||
|
<div class="wp-bnb-room-description">
|
||||||
|
<?php echo wp_kses_post( apply_filters( 'the_content', $room->post_content ) ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_pricing ) : ?>
|
||||||
|
<?php $pricing = Calculator::getRoomPricing( $room->ID ); ?>
|
||||||
|
<div class="wp-bnb-room-pricing-details">
|
||||||
|
<h4><?php esc_html_e( 'Pricing', 'wp-bnb' ); ?></h4>
|
||||||
|
<table class="wp-bnb-pricing-table">
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( PricingTier::cases() as $tier ) : ?>
|
||||||
|
<?php $price = $pricing[ $tier->value ]['price'] ?? null; ?>
|
||||||
|
<?php if ( $price ) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="wp-bnb-tier-label"><?php echo esc_html( $tier->label() ); ?></td>
|
||||||
|
<td class="wp-bnb-tier-price">
|
||||||
|
<?php echo esc_html( Calculator::formatPrice( $price ) ); ?>
|
||||||
|
<span class="wp-bnb-tier-unit"><?php echo esc_html( $tier->unit() ); ?></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_availability ) : ?>
|
||||||
|
<div class="wp-bnb-room-availability">
|
||||||
|
<h4><?php esc_html_e( 'Check Availability', 'wp-bnb' ); ?></h4>
|
||||||
|
<form class="wp-bnb-availability-form" data-room-id="<?php echo esc_attr( $room->ID ); ?>">
|
||||||
|
<div class="wp-bnb-availability-fields">
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-avail-check-in"><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-avail-check-in" name="check_in" min="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-field-group">
|
||||||
|
<label for="wp-bnb-avail-check-out"><?php esc_html_e( 'Check-out', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="date" id="wp-bnb-avail-check-out" name="check_out" min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="wp-bnb-button wp-bnb-button-primary">
|
||||||
|
<?php esc_html_e( 'Check', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-availability-result" style="display:none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
298
src/Frontend/Widgets/AvailabilityCalendar.php
Normal file
298
src/Frontend/Widgets/AvailabilityCalendar.php
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Availability Calendar widget.
|
||||||
|
*
|
||||||
|
* Displays a mini calendar showing room availability.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend\Widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability Calendar widget class.
|
||||||
|
*/
|
||||||
|
class AvailabilityCalendar extends \WP_Widget {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'wp_bnb_availability_calendar',
|
||||||
|
__( 'WP BnB: Availability Calendar', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'classname' => 'wp-bnb-widget-availability-calendar',
|
||||||
|
'description' => __( 'Display a room availability calendar.', 'wp-bnb' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget content.
|
||||||
|
*
|
||||||
|
* @param array $args Widget arguments.
|
||||||
|
* @param array $instance Widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function widget( $args, $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Availability', 'wp-bnb' );
|
||||||
|
$room_id = ! empty( $instance['room_id'] ) ? (int) $instance['room_id'] : 0;
|
||||||
|
$months_to_show = ! empty( $instance['months'] ) ? (int) $instance['months'] : 1;
|
||||||
|
$show_legend = ! empty( $instance['show_legend'] );
|
||||||
|
$show_navigation = ! empty( $instance['show_navigation'] );
|
||||||
|
|
||||||
|
// Auto-detect room from single room page.
|
||||||
|
if ( ! $room_id && is_singular( Room::POST_TYPE ) ) {
|
||||||
|
$room_id = get_the_ID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit months to show.
|
||||||
|
$months_to_show = max( 1, min( 3, $months_to_show ) );
|
||||||
|
|
||||||
|
// Get current month or from request.
|
||||||
|
$year = (int) gmdate( 'Y' );
|
||||||
|
$month = (int) gmdate( 'n' );
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
if ( $title ) {
|
||||||
|
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-availability-calendar-widget" data-room-id="<?php echo esc_attr( $room_id ); ?>">
|
||||||
|
<?php for ( $i = 0; $i < $months_to_show; $i++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$display_year = $year;
|
||||||
|
$display_month = $month + $i;
|
||||||
|
|
||||||
|
if ( $display_month > 12 ) {
|
||||||
|
$display_month -= 12;
|
||||||
|
$display_year++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$calendar = Availability::get_calendar_data( $room_id, $display_year, $display_month );
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-calendar-month" data-year="<?php echo esc_attr( $display_year ); ?>" data-month="<?php echo esc_attr( $display_month ); ?>">
|
||||||
|
<div class="wp-bnb-calendar-header">
|
||||||
|
<?php if ( $show_navigation && 0 === $i ) : ?>
|
||||||
|
<button type="button" class="wp-bnb-calendar-nav wp-bnb-calendar-prev" data-direction="prev" aria-label="<?php esc_attr_e( 'Previous month', 'wp-bnb' ); ?>">
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<span class="wp-bnb-calendar-month-name">
|
||||||
|
<?php echo esc_html( $calendar['month_name'] . ' ' . $display_year ); ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<?php if ( $show_navigation && $i === $months_to_show - 1 ) : ?>
|
||||||
|
<button type="button" class="wp-bnb-calendar-nav wp-bnb-calendar-next" data-direction="next" aria-label="<?php esc_attr_e( 'Next month', 'wp-bnb' ); ?>">
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="wp-bnb-calendar-grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Su', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Mo', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Tu', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'We', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Th', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Fr', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Sa', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
$day = 1;
|
||||||
|
$total_days = $calendar['days_in_month'];
|
||||||
|
$first_day = $calendar['first_day_of_week']; // 0 = Sunday.
|
||||||
|
|
||||||
|
// Calculate weeks.
|
||||||
|
$weeks = ceil( ( $first_day + $total_days ) / 7 );
|
||||||
|
|
||||||
|
for ( $week = 0; $week < $weeks; $week++ ) :
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<?php for ( $dow = 0; $dow < 7; $dow++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$cell_index = $week * 7 + $dow;
|
||||||
|
|
||||||
|
if ( $cell_index < $first_day || $day > $total_days ) {
|
||||||
|
echo '<td class="wp-bnb-calendar-empty"></td>';
|
||||||
|
} else {
|
||||||
|
$day_data = $calendar['days'][ $day ] ?? null;
|
||||||
|
$classes = array( 'wp-bnb-calendar-day' );
|
||||||
|
|
||||||
|
if ( $day_data ) {
|
||||||
|
if ( $day_data['is_booked'] ) {
|
||||||
|
$classes[] = 'wp-bnb-booked';
|
||||||
|
} else {
|
||||||
|
$classes[] = 'wp-bnb-available';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $day_data['is_past'] ) {
|
||||||
|
$classes[] = 'wp-bnb-past';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $day_data['is_today'] ) {
|
||||||
|
$classes[] = 'wp-bnb-today';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<td class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>" data-date="<?php echo esc_attr( $day_data['date'] ?? '' ); ?>">
|
||||||
|
<?php echo esc_html( $day ); ?>
|
||||||
|
</td>
|
||||||
|
<?php
|
||||||
|
$day++;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_legend ) : ?>
|
||||||
|
<div class="wp-bnb-calendar-legend">
|
||||||
|
<span class="wp-bnb-legend-item wp-bnb-legend-available">
|
||||||
|
<span class="wp-bnb-legend-color"></span>
|
||||||
|
<?php esc_html_e( 'Available', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
<span class="wp-bnb-legend-item wp-bnb-legend-booked">
|
||||||
|
<span class="wp-bnb-legend-color"></span>
|
||||||
|
<?php esc_html_e( 'Booked', 'wp-bnb' ); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget settings form.
|
||||||
|
*
|
||||||
|
* @param array $instance Current widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function form( $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Availability', 'wp-bnb' );
|
||||||
|
$room_id = ! empty( $instance['room_id'] ) ? (int) $instance['room_id'] : 0;
|
||||||
|
$months = ! empty( $instance['months'] ) ? (int) $instance['months'] : 1;
|
||||||
|
$show_legend = ! empty( $instance['show_legend'] );
|
||||||
|
$show_navigation = ! empty( $instance['show_navigation'] );
|
||||||
|
|
||||||
|
// Get all rooms.
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
|
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'room_id' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Room:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'room_id' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'room_id' ) ); ?>">
|
||||||
|
<option value="0"><?php esc_html_e( '— Auto-detect from page —', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $rooms as $room ) : ?>
|
||||||
|
<?php
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
?>
|
||||||
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
<?php if ( $building ) : ?>
|
||||||
|
(<?php echo esc_html( $building->post_title ); ?>)
|
||||||
|
<?php endif; ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small><?php esc_html_e( 'Leave as auto-detect to show calendar of the current room page.', 'wp-bnb' ); ?></small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'months' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Months to show:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'months' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'months' ) ); ?>">
|
||||||
|
<option value="1" <?php selected( $months, 1 ); ?>>1</option>
|
||||||
|
<option value="2" <?php selected( $months, 2 ); ?>>2</option>
|
||||||
|
<option value="3" <?php selected( $months, 3 ); ?>>3</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_legend' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_legend' ) ); ?>"
|
||||||
|
<?php checked( $show_legend ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_legend' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show legend', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_navigation' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_navigation' ) ); ?>"
|
||||||
|
<?php checked( $show_navigation ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_navigation' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Allow navigation', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget settings.
|
||||||
|
*
|
||||||
|
* @param array $new_instance New settings.
|
||||||
|
* @param array $old_instance Old settings.
|
||||||
|
* @return array Updated settings.
|
||||||
|
*/
|
||||||
|
public function update( $new_instance, $old_instance ): array {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['room_id'] = ! empty( $new_instance['room_id'] ) ? absint( $new_instance['room_id'] ) : 0;
|
||||||
|
$instance['months'] = ! empty( $new_instance['months'] ) ? absint( $new_instance['months'] ) : 1;
|
||||||
|
$instance['show_legend'] = ! empty( $new_instance['show_legend'] );
|
||||||
|
$instance['show_navigation'] = ! empty( $new_instance['show_navigation'] );
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/Frontend/Widgets/BuildingRooms.php
Normal file
261
src/Frontend/Widgets/BuildingRooms.php
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Building Rooms widget.
|
||||||
|
*
|
||||||
|
* Displays all rooms in a specific building.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend\Widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building Rooms widget class.
|
||||||
|
*/
|
||||||
|
class BuildingRooms extends \WP_Widget {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'wp_bnb_building_rooms',
|
||||||
|
__( 'WP BnB: Building Rooms', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'classname' => 'wp-bnb-widget-building-rooms',
|
||||||
|
'description' => __( 'Display all rooms in a building.', 'wp-bnb' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget content.
|
||||||
|
*
|
||||||
|
* @param array $args Widget arguments.
|
||||||
|
* @param array $instance Widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function widget( $args, $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Rooms', 'wp-bnb' );
|
||||||
|
$building_id = ! empty( $instance['building_id'] ) ? (int) $instance['building_id'] : 0;
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : -1;
|
||||||
|
$show_availability = ! empty( $instance['show_availability'] );
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$layout = ! empty( $instance['layout'] ) ? $instance['layout'] : 'list';
|
||||||
|
|
||||||
|
// Auto-detect building from single building page.
|
||||||
|
if ( ! $building_id && is_singular( Building::POST_TYPE ) ) {
|
||||||
|
$building_id = get_the_ID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $building_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rooms for building.
|
||||||
|
$search_args = array(
|
||||||
|
'building_id' => $building_id,
|
||||||
|
'limit' => $count,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = Search::search( $search_args );
|
||||||
|
|
||||||
|
if ( empty( $rooms ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
if ( $title ) {
|
||||||
|
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
$list_class = 'compact' === $layout ? 'wp-bnb-building-rooms-compact' : 'wp-bnb-building-rooms-list';
|
||||||
|
echo '<ul class="' . esc_attr( $list_class ) . '">';
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$status = get_post_meta( $room['id'], '_bnb_room_status', true ) ?: 'available';
|
||||||
|
?>
|
||||||
|
<li class="wp-bnb-building-room">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>" class="wp-bnb-building-room-link">
|
||||||
|
<span class="wp-bnb-building-room-title"><?php echo esc_html( $room['title'] ); ?></span>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['room_number'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-building-room-number">#<?php echo esc_html( $room['room_number'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_availability ) : ?>
|
||||||
|
<span class="wp-bnb-building-room-status wp-bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||||
|
<?php
|
||||||
|
$statuses = Room::get_room_statuses();
|
||||||
|
echo esc_html( $statuses[ $status ] ?? $status );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-building-room-price">
|
||||||
|
<?php echo esc_html( $room['price_formatted'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if ( 'list' === $layout ) : ?>
|
||||||
|
<div class="wp-bnb-building-room-meta">
|
||||||
|
<?php if ( ! empty( $room['capacity'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-meta-item">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
<?php echo esc_html( $room['capacity'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $room['room_types'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-meta-item">
|
||||||
|
<?php echo esc_html( $room['room_types'][0] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</ul>';
|
||||||
|
|
||||||
|
// Show view all link if there are more rooms.
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
if ( $building && $count > 0 && count( $rooms ) >= $count ) {
|
||||||
|
$all_rooms = Room::get_rooms_for_building( $building_id );
|
||||||
|
if ( count( $all_rooms ) > $count ) {
|
||||||
|
printf(
|
||||||
|
'<a href="%s" class="wp-bnb-view-all-rooms">%s</a>',
|
||||||
|
esc_url( get_permalink( $building_id ) ),
|
||||||
|
esc_html__( 'View all rooms', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget settings form.
|
||||||
|
*
|
||||||
|
* @param array $instance Current widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function form( $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Rooms', 'wp-bnb' );
|
||||||
|
$building_id = ! empty( $instance['building_id'] ) ? (int) $instance['building_id'] : 0;
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : -1;
|
||||||
|
$show_availability = ! empty( $instance['show_availability'] );
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$layout = ! empty( $instance['layout'] ) ? $instance['layout'] : 'list';
|
||||||
|
|
||||||
|
// Get all buildings.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
|
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'building_id' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Building:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'building_id' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'building_id' ) ); ?>">
|
||||||
|
<option value="0"><?php esc_html_e( '— Auto-detect from page —', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $building_id, $building->ID ); ?>>
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small><?php esc_html_e( 'Leave as auto-detect to show rooms of the current building page.', 'wp-bnb' ); ?></small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Number of rooms:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||||
|
type="number" min="-1" max="50" value="<?php echo esc_attr( $count ); ?>">
|
||||||
|
<small><?php esc_html_e( '-1 for all rooms', 'wp-bnb' ); ?></small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'layout' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Layout:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'layout' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'layout' ) ); ?>">
|
||||||
|
<option value="list" <?php selected( $layout, 'list' ); ?>>
|
||||||
|
<?php esc_html_e( 'List (with details)', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="compact" <?php selected( $layout, 'compact' ); ?>>
|
||||||
|
<?php esc_html_e( 'Compact', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_availability' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_availability' ) ); ?>"
|
||||||
|
<?php checked( $show_availability ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_availability' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show availability status', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_price' ) ); ?>"
|
||||||
|
<?php checked( $show_price ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show price', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget settings.
|
||||||
|
*
|
||||||
|
* @param array $new_instance New settings.
|
||||||
|
* @param array $old_instance Old settings.
|
||||||
|
* @return array Updated settings.
|
||||||
|
*/
|
||||||
|
public function update( $new_instance, $old_instance ): array {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['building_id'] = ! empty( $new_instance['building_id'] ) ? absint( $new_instance['building_id'] ) : 0;
|
||||||
|
$instance['count'] = isset( $new_instance['count'] ) ? (int) $new_instance['count'] : -1;
|
||||||
|
$instance['show_availability'] = ! empty( $new_instance['show_availability'] );
|
||||||
|
$instance['show_price'] = ! empty( $new_instance['show_price'] );
|
||||||
|
$instance['layout'] = ! empty( $new_instance['layout'] ) ? sanitize_text_field( $new_instance['layout'] ) : 'list';
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
233
src/Frontend/Widgets/SimilarRooms.php
Normal file
233
src/Frontend/Widgets/SimilarRooms.php
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Similar Rooms widget.
|
||||||
|
*
|
||||||
|
* Displays rooms similar to the current room based on building or room type.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Frontend\Widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar Rooms widget class.
|
||||||
|
*/
|
||||||
|
class SimilarRooms extends \WP_Widget {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'wp_bnb_similar_rooms',
|
||||||
|
__( 'WP BnB: Similar Rooms', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'classname' => 'wp-bnb-widget-similar-rooms',
|
||||||
|
'description' => __( 'Display rooms similar to the current room.', 'wp-bnb' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget content.
|
||||||
|
*
|
||||||
|
* @param array $args Widget arguments.
|
||||||
|
* @param array $instance Widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function widget( $args, $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Similar Rooms', 'wp-bnb' );
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : 3;
|
||||||
|
$match_by = ! empty( $instance['match_by'] ) ? $instance['match_by'] : 'building';
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$show_image = ! empty( $instance['show_image'] );
|
||||||
|
|
||||||
|
// Get current room.
|
||||||
|
$current_room_id = 0;
|
||||||
|
if ( is_singular( Room::POST_TYPE ) ) {
|
||||||
|
$current_room_id = get_the_ID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $current_room_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query based on match type.
|
||||||
|
$search_args = array(
|
||||||
|
'limit' => $count + 1, // Get extra in case current room is included.
|
||||||
|
);
|
||||||
|
|
||||||
|
switch ( $match_by ) {
|
||||||
|
case 'building':
|
||||||
|
$building_id = get_post_meta( $current_room_id, '_bnb_room_building_id', true );
|
||||||
|
if ( $building_id ) {
|
||||||
|
$search_args['building_id'] = (int) $building_id;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'room_type':
|
||||||
|
$terms = wp_get_post_terms( $current_room_id, RoomType::TAXONOMY, array( 'fields' => 'slugs' ) );
|
||||||
|
if ( ! empty( $terms ) ) {
|
||||||
|
$search_args['room_type'] = $terms[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'amenities':
|
||||||
|
$amenities = wp_get_post_terms( $current_room_id, 'bnb_amenity', array( 'fields' => 'slugs' ) );
|
||||||
|
if ( ! empty( $amenities ) ) {
|
||||||
|
$search_args['amenities'] = array_slice( $amenities, 0, 3 );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = Search::search( $search_args );
|
||||||
|
|
||||||
|
// Remove current room from results.
|
||||||
|
$rooms = array_filter(
|
||||||
|
$rooms,
|
||||||
|
function ( $room ) use ( $current_room_id ) {
|
||||||
|
return $room['id'] !== $current_room_id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limit to requested count.
|
||||||
|
$rooms = array_slice( $rooms, 0, $count );
|
||||||
|
|
||||||
|
if ( empty( $rooms ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
if ( $title ) {
|
||||||
|
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<ul class="wp-bnb-similar-rooms-list">';
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
?>
|
||||||
|
<li class="wp-bnb-similar-room">
|
||||||
|
<?php if ( $show_image && ! empty( $room['thumbnail'] ) ) : ?>
|
||||||
|
<div class="wp-bnb-similar-room-image">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<img src="<?php echo esc_url( $room['thumbnail'] ); ?>" alt="<?php echo esc_attr( $room['title'] ); ?>">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-similar-room-content">
|
||||||
|
<h4 class="wp-bnb-similar-room-title">
|
||||||
|
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||||
|
<?php echo esc_html( $room['title'] ); ?>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||||
|
<span class="wp-bnb-similar-room-price">
|
||||||
|
<?php echo esc_html( $room['price_formatted'] ); ?>
|
||||||
|
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</ul>';
|
||||||
|
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the widget settings form.
|
||||||
|
*
|
||||||
|
* @param array $instance Current widget instance settings.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function form( $instance ): void {
|
||||||
|
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Similar Rooms', 'wp-bnb' );
|
||||||
|
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : 3;
|
||||||
|
$match_by = ! empty( $instance['match_by'] ) ? $instance['match_by'] : 'building';
|
||||||
|
$show_price = ! empty( $instance['show_price'] );
|
||||||
|
$show_image = ! empty( $instance['show_image'] );
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
|
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Number of rooms:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||||
|
type="number" min="1" max="10" value="<?php echo esc_attr( $count ); ?>">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'match_by' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Match by:', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'match_by' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'match_by' ) ); ?>">
|
||||||
|
<option value="building" <?php selected( $match_by, 'building' ); ?>>
|
||||||
|
<?php esc_html_e( 'Same Building', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="room_type" <?php selected( $match_by, 'room_type' ); ?>>
|
||||||
|
<?php esc_html_e( 'Same Room Type', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="amenities" <?php selected( $match_by, 'amenities' ); ?>>
|
||||||
|
<?php esc_html_e( 'Similar Amenities', 'wp-bnb' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_image' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_image' ) ); ?>"
|
||||||
|
<?php checked( $show_image ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_image' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show image', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>"
|
||||||
|
name="<?php echo esc_attr( $this->get_field_name( 'show_price' ) ); ?>"
|
||||||
|
<?php checked( $show_price ); ?>>
|
||||||
|
<label for="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Show price', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget settings.
|
||||||
|
*
|
||||||
|
* @param array $new_instance New settings.
|
||||||
|
* @param array $old_instance Old settings.
|
||||||
|
* @return array Updated settings.
|
||||||
|
*/
|
||||||
|
public function update( $new_instance, $old_instance ): array {
|
||||||
|
$instance = array();
|
||||||
|
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||||
|
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 3;
|
||||||
|
$instance['match_by'] = ! empty( $new_instance['match_by'] ) ? sanitize_text_field( $new_instance['match_by'] ) : 'building';
|
||||||
|
$instance['show_price'] = ! empty( $new_instance['show_price'] );
|
||||||
|
$instance['show_image'] = ! empty( $new_instance['show_image'] );
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/Plugin.php
172
src/Plugin.php
@@ -11,15 +11,26 @@ namespace Magdev\WpBnb;
|
|||||||
|
|
||||||
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
|
use Magdev\WpBnb\Blocks\BlockRegistrar;
|
||||||
use Magdev\WpBnb\Booking\Availability;
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||||
|
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||||
|
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||||
|
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
use Magdev\WpBnb\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\Room;
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
||||||
use Magdev\WpBnb\Pricing\Season;
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
use Magdev\WpBnb\Taxonomies\ServiceCategory;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Loader\FilesystemLoader;
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
@@ -92,6 +103,8 @@ final class Plugin {
|
|||||||
Building::init();
|
Building::init();
|
||||||
Room::init();
|
Room::init();
|
||||||
Booking::init();
|
Booking::init();
|
||||||
|
Guest::init();
|
||||||
|
Service::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,6 +116,7 @@ final class Plugin {
|
|||||||
// Taxonomies must be registered before post types that use them.
|
// Taxonomies must be registered before post types that use them.
|
||||||
Amenity::init();
|
Amenity::init();
|
||||||
RoomType::init();
|
RoomType::init();
|
||||||
|
ServiceCategory::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,8 +158,12 @@ final class Plugin {
|
|||||||
// Initialize email notifier.
|
// Initialize email notifier.
|
||||||
EmailNotifier::init();
|
EmailNotifier::init();
|
||||||
|
|
||||||
|
// Initialize privacy manager for GDPR compliance.
|
||||||
|
PrivacyManager::init();
|
||||||
|
|
||||||
// Register AJAX handlers.
|
// Register AJAX handlers.
|
||||||
add_action( 'wp_ajax_wp_bnb_check_availability', array( $this, 'ajax_check_availability' ) );
|
add_action( 'wp_ajax_wp_bnb_check_availability', array( $this, 'ajax_check_availability' ) );
|
||||||
|
add_action( 'wp_ajax_wp_bnb_search_guest', array( $this, 'ajax_search_guest' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,7 +172,28 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function init_frontend(): void {
|
private function init_frontend(): void {
|
||||||
// Frontend shortcodes, blocks, and widgets will be added here.
|
// Initialize search (registers AJAX handlers).
|
||||||
|
Search::init();
|
||||||
|
|
||||||
|
// Initialize shortcodes.
|
||||||
|
Shortcodes::init();
|
||||||
|
|
||||||
|
// Initialize Gutenberg blocks.
|
||||||
|
BlockRegistrar::init();
|
||||||
|
|
||||||
|
// Register widgets.
|
||||||
|
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register frontend widgets.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_widgets(): void {
|
||||||
|
register_widget( SimilarRooms::class );
|
||||||
|
register_widget( BuildingRooms::class );
|
||||||
|
register_widget( AvailabilityCalendar::class );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +220,7 @@ final class Plugin {
|
|||||||
|
|
||||||
// Check if we're on plugin pages or editing our custom post types.
|
// Check if we're on plugin pages or editing our custom post types.
|
||||||
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
||||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE ), true );
|
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
||||||
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
||||||
|
|
||||||
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
||||||
@@ -219,22 +258,28 @@ final class Plugin {
|
|||||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||||
'postType' => $post_type,
|
'postType' => $post_type,
|
||||||
'i18n' => array(
|
'i18n' => array(
|
||||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||||
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
'error' => __( 'An error occurred. Please try again.', 'wp-bnb' ),
|
||||||
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
'selectImages' => __( 'Select Images', 'wp-bnb' ),
|
||||||
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
'addToGallery' => __( 'Add to Gallery', 'wp-bnb' ),
|
||||||
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
'confirmRemove' => __( 'Are you sure you want to remove this image?', 'wp-bnb' ),
|
||||||
'increase' => __( 'increase', 'wp-bnb' ),
|
'increase' => __( 'increase', 'wp-bnb' ),
|
||||||
'discount' => __( 'discount', 'wp-bnb' ),
|
'discount' => __( 'discount', 'wp-bnb' ),
|
||||||
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
'normalPrice' => __( 'Normal price', 'wp-bnb' ),
|
||||||
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
'checking' => __( 'Checking availability...', 'wp-bnb' ),
|
||||||
'available' => __( 'Available', 'wp-bnb' ),
|
'available' => __( 'Available', 'wp-bnb' ),
|
||||||
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||||
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||||
'nights' => __( 'nights', 'wp-bnb' ),
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
'night' => __( 'night', 'wp-bnb' ),
|
'night' => __( 'night', 'wp-bnb' ),
|
||||||
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||||
|
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
|
||||||
|
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
||||||
|
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
||||||
|
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
|
'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ),
|
||||||
|
'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -265,6 +310,35 @@ final class Plugin {
|
|||||||
WP_BNB_VERSION,
|
WP_BNB_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
'wp-bnb-frontend',
|
||||||
|
'wpBnbFrontend',
|
||||||
|
array(
|
||||||
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'wp_bnb_frontend_nonce' ),
|
||||||
|
'i18n' => array(
|
||||||
|
'searching' => __( 'Searching...', 'wp-bnb' ),
|
||||||
|
'noResults' => __( 'No rooms found matching your criteria.', 'wp-bnb' ),
|
||||||
|
'resultsFound' => __( '%d rooms found', 'wp-bnb' ),
|
||||||
|
'loadMore' => __( 'Load More', 'wp-bnb' ),
|
||||||
|
'viewDetails' => __( 'View Details', 'wp-bnb' ),
|
||||||
|
'perNight' => __( 'night', 'wp-bnb' ),
|
||||||
|
'guests' => __( 'guests', 'wp-bnb' ),
|
||||||
|
'invalidDateRange' => __( 'Check-out must be after check-in', 'wp-bnb' ),
|
||||||
|
'selectDates' => __( 'Please select check-in and check-out dates.', 'wp-bnb' ),
|
||||||
|
'available' => __( 'Room is available!', 'wp-bnb' ),
|
||||||
|
'notAvailable' => __( 'Sorry, the room is not available for these dates.', 'wp-bnb' ),
|
||||||
|
'totalPrice' => __( 'Total', 'wp-bnb' ),
|
||||||
|
'bookNow' => __( 'Book Now', 'wp-bnb' ),
|
||||||
|
'total' => __( 'Total', 'wp-bnb' ),
|
||||||
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
|
'basePrice' => __( 'Base', 'wp-bnb' ),
|
||||||
|
'weekendSurcharge' => __( 'Weekend surcharge', 'wp-bnb' ),
|
||||||
|
'season' => __( 'Season', 'wp-bnb' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -875,6 +949,68 @@ final class Plugin {
|
|||||||
wp_send_json_success( $result );
|
wp_send_json_success( $result );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for searching guests by email.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_search_guest(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
|
||||||
|
|
||||||
|
if ( strlen( $search ) < 2 ) {
|
||||||
|
wp_send_json_success( array( 'guests' => array() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by email or name.
|
||||||
|
$guests = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'OR',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_email',
|
||||||
|
'value' => $search,
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_first_name',
|
||||||
|
'value' => $search,
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_last_name',
|
||||||
|
'value' => $search,
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
foreach ( $guests as $guest ) {
|
||||||
|
$status = get_post_meta( $guest->ID, '_bnb_guest_status', true ) ?: 'active';
|
||||||
|
$results[] = array(
|
||||||
|
'id' => $guest->ID,
|
||||||
|
'name' => Guest::get_full_name( $guest->ID ),
|
||||||
|
'email' => get_post_meta( $guest->ID, '_bnb_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $guest->ID, '_bnb_guest_phone', true ),
|
||||||
|
'status' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( array( 'guests' => $results ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Twig environment.
|
* Get Twig environment.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ use Magdev\WpBnb\Pricing\Calculator;
|
|||||||
*/
|
*/
|
||||||
final class Booking {
|
final class Booking {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services meta key.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const SERVICES_META_KEY = '_bnb_booking_services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post type slug.
|
* Post type slug.
|
||||||
*
|
*
|
||||||
@@ -125,6 +132,15 @@ final class Booking {
|
|||||||
'high'
|
'high'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_booking_services',
|
||||||
|
__( 'Additional Services', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_services_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
|
||||||
add_meta_box(
|
add_meta_box(
|
||||||
'bnb_booking_pricing',
|
'bnb_booking_pricing',
|
||||||
__( 'Pricing', 'wp-bnb' ),
|
__( 'Pricing', 'wp-bnb' ),
|
||||||
@@ -280,41 +296,97 @@ 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_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 );
|
||||||
$adults = get_post_meta( $post->ID, self::META_PREFIX . 'adults', true );
|
$adults = get_post_meta( $post->ID, self::META_PREFIX . 'adults', true );
|
||||||
$children = get_post_meta( $post->ID, self::META_PREFIX . 'children', true );
|
$children = get_post_meta( $post->ID, self::META_PREFIX . 'children', true );
|
||||||
$guest_notes = get_post_meta( $post->ID, self::META_PREFIX . 'guest_notes', true );
|
$guest_notes = get_post_meta( $post->ID, self::META_PREFIX . 'guest_notes', true );
|
||||||
|
|
||||||
|
// If guest_id exists, get guest data from Guest CPT.
|
||||||
|
$linked_guest = null;
|
||||||
|
if ( $guest_id ) {
|
||||||
|
$linked_guest = get_post( $guest_id );
|
||||||
|
if ( $linked_guest && Guest::POST_TYPE === $linked_guest->post_type ) {
|
||||||
|
$guest_name = Guest::get_full_name( $guest_id );
|
||||||
|
$guest_email = get_post_meta( $guest_id, '_bnb_guest_email', true );
|
||||||
|
$guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true );
|
||||||
|
} else {
|
||||||
|
$linked_guest = null;
|
||||||
|
$guest_id = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
<input type="hidden" id="bnb_booking_guest_id" name="bnb_booking_guest_id" value="<?php echo esc_attr( $guest_id ); ?>">
|
||||||
|
|
||||||
|
<?php if ( $linked_guest ) : ?>
|
||||||
|
<div id="bnb-linked-guest-info" class="bnb-linked-guest">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-admin-users"></span>
|
||||||
|
<strong><?php echo esc_html( $linked_guest->post_title ); ?></strong>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $guest_id ) ); ?>" target="_blank" class="button button-small">
|
||||||
|
<?php esc_html_e( 'View Guest Profile', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<button type="button" id="bnb-unlink-guest" class="button button-small button-link-delete">
|
||||||
|
<?php esc_html_e( 'Unlink', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<?php if ( $guest_email ) : ?>
|
||||||
|
<p><small><?php echo esc_html( $guest_email ); ?></small></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="bnb-guest-search-container" class="bnb-guest-search" <?php echo $linked_guest ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_booking_guest_search"><?php esc_html_e( 'Search Guest', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_booking_guest_search" class="regular-text"
|
||||||
|
placeholder="<?php esc_attr_e( 'Search by email...', 'wp-bnb' ); ?>">
|
||||||
|
<p class="description"><?php esc_html_e( 'Search for existing guest or enter details below.', 'wp-bnb' ); ?></p>
|
||||||
|
<div id="bnb-guest-search-results" class="bnb-guest-search-results" style="display:none;"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bnb-guest-fields-container" <?php echo $linked_guest ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
|
||||||
|
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" <?php echo $linked_guest ? '' : 'required'; ?>>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
|
||||||
|
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
|
||||||
|
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<label for="bnb_booking_guest_name"><?php esc_html_e( 'Guest Name', 'wp-bnb' ); ?> <span class="required">*</span></label>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="bnb_booking_guest_name" name="bnb_booking_guest_name"
|
|
||||||
value="<?php echo esc_attr( $guest_name ); ?>" class="regular-text" required>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<label for="bnb_booking_guest_email"><?php esc_html_e( 'Email', 'wp-bnb' ); ?></label>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<input type="email" id="bnb_booking_guest_email" name="bnb_booking_guest_email"
|
|
||||||
value="<?php echo esc_attr( $guest_email ); ?>" class="regular-text">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<label for="bnb_booking_guest_phone"><?php esc_html_e( 'Phone', 'wp-bnb' ); ?></label>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<input type="tel" id="bnb_booking_guest_phone" name="bnb_booking_guest_phone"
|
|
||||||
value="<?php echo esc_attr( $guest_phone ); ?>" class="regular-text">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<?php esc_html_e( 'Guests', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'Guests', 'wp-bnb' ); ?>
|
||||||
@@ -343,6 +415,111 @@ final class Booking {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render services meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_services_meta_box( \WP_Post $post ): void {
|
||||||
|
$selected_services = get_post_meta( $post->ID, self::SERVICES_META_KEY, true ) ?: array();
|
||||||
|
$check_in = get_post_meta( $post->ID, self::META_PREFIX . 'check_in', true );
|
||||||
|
$check_out = get_post_meta( $post->ID, self::META_PREFIX . 'check_out', true );
|
||||||
|
$nights = ( $check_in && $check_out ) ? self::calculate_nights( $check_in, $check_out ) : 1;
|
||||||
|
|
||||||
|
// Get all active services.
|
||||||
|
$available_services = Service::get_services_for_booking();
|
||||||
|
|
||||||
|
if ( empty( $available_services ) ) {
|
||||||
|
?>
|
||||||
|
<p class="bnb-no-services-message">
|
||||||
|
<?php esc_html_e( 'No services available.', 'wp-bnb' ); ?>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Service::POST_TYPE ) ); ?>">
|
||||||
|
<?php esc_html_e( 'Add a service', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a lookup map for selected services.
|
||||||
|
$selected_map = array();
|
||||||
|
if ( is_array( $selected_services ) ) {
|
||||||
|
foreach ( $selected_services as $service ) {
|
||||||
|
if ( isset( $service['service_id'] ) ) {
|
||||||
|
$selected_map[ $service['service_id'] ] = $service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="bnb-services-selector" data-nights="<?php echo esc_attr( $nights ); ?>">
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Select additional services for this booking.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bnb-services-list">
|
||||||
|
<?php foreach ( $available_services as $service ) : ?>
|
||||||
|
<?php
|
||||||
|
$is_selected = isset( $selected_map[ $service['id'] ] );
|
||||||
|
$quantity = $is_selected ? ( $selected_map[ $service['id'] ]['quantity'] ?? 1 ) : 1;
|
||||||
|
$service_total = $is_selected
|
||||||
|
? Service::calculate_service_price( $service['id'], $quantity, $nights )
|
||||||
|
: 0;
|
||||||
|
?>
|
||||||
|
<div class="bnb-service-item <?php echo $is_selected ? 'selected' : ''; ?>"
|
||||||
|
data-service-id="<?php echo esc_attr( $service['id'] ); ?>"
|
||||||
|
data-price="<?php echo esc_attr( $service['price'] ); ?>"
|
||||||
|
data-pricing-type="<?php echo esc_attr( $service['pricing_type'] ); ?>"
|
||||||
|
data-max-quantity="<?php echo esc_attr( $service['max_quantity'] ); ?>">
|
||||||
|
<label class="bnb-service-checkbox">
|
||||||
|
<input type="checkbox" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][selected]"
|
||||||
|
value="1" <?php checked( $is_selected ); ?>>
|
||||||
|
<span class="bnb-service-name"><?php echo esc_html( $service['name'] ); ?></span>
|
||||||
|
</label>
|
||||||
|
<div class="bnb-service-details">
|
||||||
|
<span class="bnb-service-price-label">
|
||||||
|
<?php
|
||||||
|
if ( 'included' === $service['pricing_type'] ) {
|
||||||
|
echo '<span class="bnb-service-included-badge">' . esc_html__( 'Included', 'wp-bnb' ) . '</span>';
|
||||||
|
} else {
|
||||||
|
echo esc_html( $service['formatted_price'] );
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php if ( $service['max_quantity'] > 1 && 'included' !== $service['pricing_type'] ) : ?>
|
||||||
|
<span class="bnb-service-quantity" <?php echo ! $is_selected ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<label>
|
||||||
|
<?php esc_html_e( 'Qty:', 'wp-bnb' ); ?>
|
||||||
|
<input type="number" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][quantity]"
|
||||||
|
value="<?php echo esc_attr( $quantity ); ?>"
|
||||||
|
min="1" max="<?php echo esc_attr( $service['max_quantity'] ); ?>"
|
||||||
|
class="small-text bnb-service-qty-input">
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<?php else : ?>
|
||||||
|
<input type="hidden" name="bnb_booking_services[<?php echo esc_attr( $service['id'] ); ?>][quantity]" value="1">
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="bnb-service-line-total" <?php echo ( ! $is_selected || $service_total <= 0 ) ? 'style="display:none;"' : ''; ?>>
|
||||||
|
= <strong class="bnb-service-total-value"><?php echo esc_html( Calculator::formatPrice( $service_total ) ); ?></strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bnb-services-total">
|
||||||
|
<strong><?php esc_html_e( 'Services Total:', 'wp-bnb' ); ?></strong>
|
||||||
|
<span id="bnb-services-total-amount">
|
||||||
|
<?php
|
||||||
|
$services_total = self::calculate_booking_services_total( $post->ID );
|
||||||
|
echo esc_html( Calculator::formatPrice( $services_total ) );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render pricing meta box.
|
* Render pricing meta box.
|
||||||
*
|
*
|
||||||
@@ -391,6 +568,36 @@ final class Booking {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$services_total = self::calculate_booking_services_total( $post->ID );
|
||||||
|
if ( $services_total > 0 ) :
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<?php esc_html_e( 'Services', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-booking-services-summary">
|
||||||
|
<?php echo esc_html( Calculator::formatPrice( $services_total ) ); ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<?php esc_html_e( 'Grand Total', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-booking-grand-total">
|
||||||
|
<strong>
|
||||||
|
<?php
|
||||||
|
$room_price = (float) $calculated_price;
|
||||||
|
echo esc_html( Calculator::formatPrice( $room_price + $services_total ) );
|
||||||
|
?>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="bnb_booking_override_price"><?php esc_html_e( 'Override Price', 'wp-bnb' ); ?></label>
|
<label for="bnb_booking_override_price"><?php esc_html_e( 'Override Price', 'wp-bnb' ); ?></label>
|
||||||
@@ -535,21 +742,48 @@ final class Booking {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest text fields.
|
// Guest ID (linked guest record).
|
||||||
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
|
$guest_id = isset( $_POST['bnb_booking_guest_id'] ) ? absint( $_POST['bnb_booking_guest_id'] ) : 0;
|
||||||
foreach ( $guest_fields as $field ) {
|
if ( $guest_id ) {
|
||||||
$key = 'bnb_booking_' . $field;
|
// Verify guest exists.
|
||||||
if ( isset( $_POST[ $key ] ) ) {
|
$guest = get_post( $guest_id );
|
||||||
$value = wp_unslash( $_POST[ $key ] );
|
if ( $guest && Guest::POST_TYPE === $guest->post_type ) {
|
||||||
if ( 'guest_email' === $field ) {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $guest_id );
|
||||||
$value = sanitize_email( $value );
|
// Sync guest data from Guest CPT for searching/display purposes.
|
||||||
} elseif ( 'guest_notes' === $field ) {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $guest_id ) );
|
||||||
$value = sanitize_textarea_field( $value );
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) );
|
||||||
} else {
|
update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) );
|
||||||
$value = sanitize_text_field( $value );
|
} else {
|
||||||
}
|
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
||||||
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' );
|
||||||
|
|
||||||
|
// Guest text fields (only save if no guest_id).
|
||||||
|
$guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' );
|
||||||
|
foreach ( $guest_fields as $field ) {
|
||||||
|
$key = 'bnb_booking_' . $field;
|
||||||
|
if ( isset( $_POST[ $key ] ) ) {
|
||||||
|
$value = wp_unslash( $_POST[ $key ] );
|
||||||
|
if ( 'guest_email' === $field ) {
|
||||||
|
$value = sanitize_email( $value );
|
||||||
|
} elseif ( 'guest_notes' === $field ) {
|
||||||
|
$value = sanitize_textarea_field( $value );
|
||||||
|
} else {
|
||||||
|
$value = sanitize_text_field( $value );
|
||||||
|
}
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . $field, $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guest notes are always saved (per-booking notes).
|
||||||
|
if ( isset( $_POST['bnb_booking_guest_notes'] ) ) {
|
||||||
|
update_post_meta(
|
||||||
|
$post_id,
|
||||||
|
self::META_PREFIX . 'guest_notes',
|
||||||
|
sanitize_textarea_field( wp_unslash( $_POST['bnb_booking_guest_notes'] ) )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest counts.
|
// Guest counts.
|
||||||
@@ -595,6 +829,43 @@ final class Booking {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services.
|
||||||
|
$services_data = array();
|
||||||
|
if ( isset( $_POST['bnb_booking_services'] ) && is_array( $_POST['bnb_booking_services'] ) ) {
|
||||||
|
$nights = ( $check_in && $check_out ) ? self::calculate_nights( $check_in, $check_out ) : 1;
|
||||||
|
|
||||||
|
foreach ( $_POST['bnb_booking_services'] as $service_id => $service_input ) {
|
||||||
|
$service_id = absint( $service_id );
|
||||||
|
if ( ! $service_id ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include if selected checkbox is checked.
|
||||||
|
if ( empty( $service_input['selected'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify service exists and is active.
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
if ( ! $service_data || 'active' !== $service_data['status'] ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = isset( $service_input['quantity'] ) ? absint( $service_input['quantity'] ) : 1;
|
||||||
|
$quantity = max( 1, min( $quantity, $service_data['max_quantity'] ) );
|
||||||
|
|
||||||
|
$price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
|
||||||
|
$services_data[] = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $price,
|
||||||
|
'pricing_type' => $service_data['pricing_type'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_post_meta( $post_id, self::SERVICES_META_KEY, $services_data );
|
||||||
|
|
||||||
// Trigger status change action.
|
// Trigger status change action.
|
||||||
if ( $old_status && $status !== $old_status ) {
|
if ( $old_status && $status !== $old_status ) {
|
||||||
/**
|
/**
|
||||||
@@ -664,10 +935,21 @@ final class Booking {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'guest':
|
case 'guest':
|
||||||
|
$guest_id = 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 ) {
|
||||||
echo esc_html( $guest_name );
|
if ( $guest_id ) {
|
||||||
|
// Linked guest - show link to guest profile.
|
||||||
|
printf(
|
||||||
|
'<a href="%s">%s</a>',
|
||||||
|
esc_url( get_edit_post_link( $guest_id ) ),
|
||||||
|
esc_html( $guest_name )
|
||||||
|
);
|
||||||
|
echo ' <span class="dashicons dashicons-admin-users" style="font-size: 14px; vertical-align: middle;" title="' . esc_attr__( 'Linked Guest', 'wp-bnb' ) . '"></span>';
|
||||||
|
} else {
|
||||||
|
echo esc_html( $guest_name );
|
||||||
|
}
|
||||||
if ( $guest_email ) {
|
if ( $guest_email ) {
|
||||||
echo '<br><small><a href="mailto:' . esc_attr( $guest_email ) . '">' . esc_html( $guest_email ) . '</a></small>';
|
echo '<br><small><a href="mailto:' . esc_attr( $guest_email ) . '">' . esc_html( $guest_email ) . '</a></small>';
|
||||||
}
|
}
|
||||||
@@ -700,13 +982,21 @@ final class Booking {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'price':
|
case 'price':
|
||||||
$price = get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
|
$room_price = (float) get_post_meta( $post_id, self::META_PREFIX . 'calculated_price', true );
|
||||||
if ( $price ) {
|
$services_total = self::calculate_booking_services_total( $post_id );
|
||||||
echo esc_html( Calculator::formatPrice( (float) $price ) );
|
$total_price = $room_price + $services_total;
|
||||||
|
|
||||||
|
if ( $total_price > 0 ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( $total_price ) );
|
||||||
$override = get_post_meta( $post_id, self::META_PREFIX . 'override_price', true );
|
$override = get_post_meta( $post_id, self::META_PREFIX . 'override_price', true );
|
||||||
if ( $override ) {
|
if ( $override ) {
|
||||||
echo ' <span class="bnb-price-override" title="' . esc_attr__( 'Price manually overridden', 'wp-bnb' ) . '">*</span>';
|
echo ' <span class="bnb-price-override" title="' . esc_attr__( 'Price manually overridden', 'wp-bnb' ) . '">*</span>';
|
||||||
}
|
}
|
||||||
|
if ( $services_total > 0 ) {
|
||||||
|
echo '<br><small style="color: #646970;">' . esc_html__( 'incl. services', 'wp-bnb' ) . '</small>';
|
||||||
|
}
|
||||||
|
} elseif ( $room_price > 0 ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( $room_price ) );
|
||||||
} else {
|
} else {
|
||||||
echo '—';
|
echo '—';
|
||||||
}
|
}
|
||||||
@@ -1071,6 +1361,47 @@ final class Booking {
|
|||||||
return Room::get_building( $room->ID );
|
return Room::get_building( $room->ID );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest for a booking.
|
||||||
|
*
|
||||||
|
* Returns the linked Guest post if guest_id exists, or a stdClass object
|
||||||
|
* with guest data from booking meta for backward compatibility.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return \WP_Post|\stdClass|null Guest post, virtual guest object, or null.
|
||||||
|
*/
|
||||||
|
public static function get_guest( int $booking_id ) {
|
||||||
|
$guest_id = get_post_meta( $booking_id, self::META_PREFIX . 'guest_id', true );
|
||||||
|
|
||||||
|
// If linked to Guest CPT, return the guest post.
|
||||||
|
if ( $guest_id ) {
|
||||||
|
$guest = get_post( $guest_id );
|
||||||
|
if ( $guest && Guest::POST_TYPE === $guest->post_type ) {
|
||||||
|
return $guest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a virtual guest object from booking meta.
|
||||||
|
$guest_name = get_post_meta( $booking_id, self::META_PREFIX . 'guest_name', true );
|
||||||
|
$guest_email = get_post_meta( $booking_id, self::META_PREFIX . 'guest_email', true );
|
||||||
|
$guest_phone = get_post_meta( $booking_id, self::META_PREFIX . 'guest_phone', true );
|
||||||
|
|
||||||
|
if ( ! $guest_name && ! $guest_email ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return virtual guest object for backward compatibility.
|
||||||
|
$virtual_guest = new \stdClass();
|
||||||
|
$virtual_guest->ID = 0;
|
||||||
|
$virtual_guest->post_type = 'virtual_guest';
|
||||||
|
$virtual_guest->post_title = $guest_name ?: '';
|
||||||
|
$virtual_guest->name = $guest_name ?: '';
|
||||||
|
$virtual_guest->email = $guest_email ?: '';
|
||||||
|
$virtual_guest->phone = $guest_phone ?: '';
|
||||||
|
|
||||||
|
return $virtual_guest;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all bookings for a room.
|
* Get all bookings for a room.
|
||||||
*
|
*
|
||||||
@@ -1097,6 +1428,56 @@ final class Booking {
|
|||||||
return get_posts( array_merge( $defaults, $args ) );
|
return get_posts( array_merge( $defaults, $args ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total services cost for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return float Total services cost.
|
||||||
|
*/
|
||||||
|
public static function calculate_booking_services_total( int $booking_id ): float {
|
||||||
|
$services = get_post_meta( $booking_id, self::SERVICES_META_KEY, true );
|
||||||
|
|
||||||
|
if ( ! is_array( $services ) || empty( $services ) ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0.0;
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
if ( isset( $service['price'] ) ) {
|
||||||
|
$total += (float) $service['price'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selected services for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return array Array of service data with names.
|
||||||
|
*/
|
||||||
|
public static function get_booking_services( int $booking_id ): array {
|
||||||
|
$services = get_post_meta( $booking_id, self::SERVICES_META_KEY, true );
|
||||||
|
|
||||||
|
if ( ! is_array( $services ) || empty( $services ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$service_post = get_post( $service['service_id'] ?? 0 );
|
||||||
|
if ( $service_post ) {
|
||||||
|
$result[] = array_merge(
|
||||||
|
$service,
|
||||||
|
array( 'name' => $service_post->post_title )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format price breakdown for display.
|
* Format price breakdown for display.
|
||||||
*
|
*
|
||||||
|
|||||||
1086
src/PostTypes/Guest.php
Normal file
1086
src/PostTypes/Guest.php
Normal file
File diff suppressed because it is too large
Load Diff
623
src/PostTypes/Service.php
Normal file
623
src/PostTypes/Service.php
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Service post type.
|
||||||
|
*
|
||||||
|
* Custom post type for BnB additional services.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\PostTypes
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\PostTypes;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service post type class.
|
||||||
|
*/
|
||||||
|
final class Service {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post type slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const POST_TYPE = 'bnb_service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta key prefix.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const META_PREFIX = '_bnb_service_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the post type.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register' ) );
|
||||||
|
add_action( 'add_meta_boxes', array( self::class, 'add_meta_boxes' ) );
|
||||||
|
add_action( 'save_post_' . self::POST_TYPE, array( self::class, 'save_meta' ), 10, 2 );
|
||||||
|
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( self::class, 'add_columns' ) );
|
||||||
|
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( self::class, 'render_column' ), 10, 2 );
|
||||||
|
add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( self::class, 'sortable_columns' ) );
|
||||||
|
add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) );
|
||||||
|
add_action( 'pre_get_posts', array( self::class, 'filter_query' ) );
|
||||||
|
add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the post type.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _x( 'Services', 'post type general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Service', 'post type singular name', 'wp-bnb' ),
|
||||||
|
'menu_name' => _x( 'Services', 'admin menu', 'wp-bnb' ),
|
||||||
|
'name_admin_bar' => _x( 'Service', 'add new on admin bar', 'wp-bnb' ),
|
||||||
|
'add_new' => _x( 'Add New', 'service', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Service', 'wp-bnb' ),
|
||||||
|
'new_item' => __( 'New Service', 'wp-bnb' ),
|
||||||
|
'edit_item' => __( 'Edit Service', 'wp-bnb' ),
|
||||||
|
'view_item' => __( 'View Service', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'Services', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Services', 'wp-bnb' ),
|
||||||
|
'parent_item_colon' => __( 'Parent Services:', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No services found.', 'wp-bnb' ),
|
||||||
|
'not_found_in_trash' => __( 'No services found in Trash.', 'wp-bnb' ),
|
||||||
|
'archives' => __( 'Service archives', 'wp-bnb' ),
|
||||||
|
'insert_into_item' => __( 'Insert into service', 'wp-bnb' ),
|
||||||
|
'uploaded_to_this_item' => __( 'Uploaded to this service', 'wp-bnb' ),
|
||||||
|
'filter_items_list' => __( 'Filter services list', 'wp-bnb' ),
|
||||||
|
'items_list_navigation' => __( 'Services list navigation', 'wp-bnb' ),
|
||||||
|
'items_list' => __( 'Services list', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'public' => false,
|
||||||
|
'publicly_queryable' => false,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => 'wp-bnb',
|
||||||
|
'query_var' => false,
|
||||||
|
'capability_type' => 'post',
|
||||||
|
'has_archive' => false,
|
||||||
|
'hierarchical' => false,
|
||||||
|
'menu_position' => null,
|
||||||
|
'menu_icon' => 'dashicons-plus-alt',
|
||||||
|
'supports' => array( 'title', 'editor', 'thumbnail' ),
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'rest_base' => 'services',
|
||||||
|
'rest_controller_class' => 'WP_REST_Posts_Controller',
|
||||||
|
);
|
||||||
|
|
||||||
|
register_post_type( self::POST_TYPE, $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add meta boxes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_meta_boxes(): void {
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_service_pricing',
|
||||||
|
__( 'Pricing', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_pricing_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
'bnb_service_settings',
|
||||||
|
__( 'Service Settings', 'wp-bnb' ),
|
||||||
|
array( self::class, 'render_settings_meta_box' ),
|
||||||
|
self::POST_TYPE,
|
||||||
|
'side',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render pricing meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_pricing_meta_box( \WP_Post $post ): void {
|
||||||
|
wp_nonce_field( 'bnb_service_meta', 'bnb_service_meta_nonce' );
|
||||||
|
|
||||||
|
$pricing_type = get_post_meta( $post->ID, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||||
|
$price = get_post_meta( $post->ID, self::META_PREFIX . 'price', true );
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
?>
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_service_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="bnb_service_pricing_type" value="included"
|
||||||
|
<?php checked( $pricing_type, 'included' ); ?>>
|
||||||
|
<?php esc_html_e( 'Included (Free)', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Service is included at no extra cost.', 'wp-bnb' ); ?></p>
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="bnb_service_pricing_type" value="per_booking"
|
||||||
|
<?php checked( $pricing_type, 'per_booking' ); ?>>
|
||||||
|
<?php esc_html_e( 'Per Booking (One-time)', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Fixed price charged once per booking.', 'wp-bnb' ); ?></p>
|
||||||
|
</label>
|
||||||
|
<br><br>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="bnb_service_pricing_type" value="per_night"
|
||||||
|
<?php checked( $pricing_type, 'per_night' ); ?>>
|
||||||
|
<?php esc_html_e( 'Per Night', 'wp-bnb' ); ?>
|
||||||
|
<p class="description"><?php esc_html_e( 'Price multiplied by the number of nights.', 'wp-bnb' ); ?></p>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="bnb-service-price-row" <?php echo 'included' === $pricing_type ? 'style="display:none;"' : ''; ?>>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bnb_service_price"><?php esc_html_e( 'Price', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="bnb-price-input-wrapper">
|
||||||
|
<input type="number" id="bnb_service_price" name="bnb_service_price"
|
||||||
|
value="<?php echo esc_attr( $price ); ?>" class="small-text"
|
||||||
|
min="0" step="0.01">
|
||||||
|
<span class="bnb-price-unit"><?php echo esc_html( $currency ); ?></span>
|
||||||
|
<span id="bnb-service-price-suffix"></span>
|
||||||
|
</div>
|
||||||
|
<p class="description" id="bnb-service-price-description">
|
||||||
|
<?php
|
||||||
|
if ( 'per_night' === $pricing_type ) {
|
||||||
|
esc_html_e( 'This price will be charged per night of the stay.', 'wp-bnb' );
|
||||||
|
} else {
|
||||||
|
esc_html_e( 'This price will be charged once for the booking.', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render settings meta box.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Current post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_settings_meta_box( \WP_Post $post ): void {
|
||||||
|
$status = get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'active';
|
||||||
|
$sort_order = get_post_meta( $post->ID, self::META_PREFIX . 'sort_order', true ) ?: 0;
|
||||||
|
$max_qty = get_post_meta( $post->ID, self::META_PREFIX . 'max_quantity', true ) ?: 1;
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label for="bnb_service_status"><strong><?php esc_html_e( 'Status', 'wp-bnb' ); ?></strong></label>
|
||||||
|
</p>
|
||||||
|
<select id="bnb_service_status" name="bnb_service_status" class="widefat">
|
||||||
|
<option value="active" <?php selected( $status, 'active' ); ?>><?php esc_html_e( 'Active', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="inactive" <?php selected( $status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php esc_html_e( 'Inactive services cannot be added to bookings.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="bnb_service_sort_order"><strong><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></strong></label>
|
||||||
|
</p>
|
||||||
|
<input type="number" id="bnb_service_sort_order" name="bnb_service_sort_order"
|
||||||
|
value="<?php echo esc_attr( $sort_order ); ?>" class="small-text" min="0">
|
||||||
|
<p class="description"><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="bnb_service_max_quantity"><strong><?php esc_html_e( 'Maximum Quantity', 'wp-bnb' ); ?></strong></label>
|
||||||
|
</p>
|
||||||
|
<input type="number" id="bnb_service_max_quantity" name="bnb_service_max_quantity"
|
||||||
|
value="<?php echo esc_attr( $max_qty ); ?>" class="small-text" min="1" max="99">
|
||||||
|
<p class="description"><?php esc_html_e( 'Maximum times this service can be added to a booking.', 'wp-bnb' ); ?></p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save post meta.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function save_meta( int $post_id, \WP_Post $post ): void {
|
||||||
|
// Verify nonce.
|
||||||
|
if ( ! isset( $_POST['bnb_service_meta_nonce'] ) ||
|
||||||
|
! wp_verify_nonce( sanitize_key( $_POST['bnb_service_meta_nonce'] ), 'bnb_service_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check autosave.
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions.
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pricing type.
|
||||||
|
$valid_pricing_types = array( 'included', 'per_booking', 'per_night' );
|
||||||
|
$pricing_type = isset( $_POST['bnb_service_pricing_type'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['bnb_service_pricing_type'] ) )
|
||||||
|
: 'per_booking';
|
||||||
|
if ( in_array( $pricing_type, $valid_pricing_types, true ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'pricing_type', $pricing_type );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price (not required for 'included').
|
||||||
|
if ( 'included' === $pricing_type ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'price', 0 );
|
||||||
|
} elseif ( isset( $_POST['bnb_service_price'] ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'price', floatval( $_POST['bnb_service_price'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status.
|
||||||
|
$valid_statuses = array( 'active', 'inactive' );
|
||||||
|
$status = isset( $_POST['bnb_service_status'] )
|
||||||
|
? sanitize_text_field( wp_unslash( $_POST['bnb_service_status'] ) )
|
||||||
|
: 'active';
|
||||||
|
if ( in_array( $status, $valid_statuses, true ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'status', $status );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort order.
|
||||||
|
if ( isset( $_POST['bnb_service_sort_order'] ) ) {
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'sort_order', absint( $_POST['bnb_service_sort_order'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max quantity.
|
||||||
|
if ( isset( $_POST['bnb_service_max_quantity'] ) ) {
|
||||||
|
$max_qty = absint( $_POST['bnb_service_max_quantity'] );
|
||||||
|
$max_qty = max( 1, min( 99, $max_qty ) );
|
||||||
|
update_post_meta( $post_id, self::META_PREFIX . 'max_quantity', $max_qty );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom columns to the post list.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function add_columns( array $columns ): array {
|
||||||
|
$new_columns = array();
|
||||||
|
foreach ( $columns as $key => $value ) {
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
if ( 'title' === $key ) {
|
||||||
|
$new_columns['pricing_type'] = __( 'Pricing Type', 'wp-bnb' );
|
||||||
|
$new_columns['price'] = __( 'Price', 'wp-bnb' );
|
||||||
|
$new_columns['service_status'] = __( 'Status', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove date column.
|
||||||
|
unset( $new_columns['date'] );
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render custom column content.
|
||||||
|
*
|
||||||
|
* @param string $column Column name.
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_column( string $column, int $post_id ): void {
|
||||||
|
switch ( $column ) {
|
||||||
|
case 'pricing_type':
|
||||||
|
$pricing_type = get_post_meta( $post_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||||
|
$labels = self::get_pricing_type_labels();
|
||||||
|
$icons = array(
|
||||||
|
'included' => 'yes-alt',
|
||||||
|
'per_booking' => 'tag',
|
||||||
|
'per_night' => 'calendar-alt',
|
||||||
|
);
|
||||||
|
$colors = array(
|
||||||
|
'included' => '#00a32a',
|
||||||
|
'per_booking' => '#135e96',
|
||||||
|
'per_night' => '#dba617',
|
||||||
|
);
|
||||||
|
echo '<span class="dashicons dashicons-' . esc_attr( $icons[ $pricing_type ] ?? 'admin-generic' ) . '" style="color: ' . esc_attr( $colors[ $pricing_type ] ?? '#646970' ) . '; vertical-align: middle; margin-right: 3px;"></span>';
|
||||||
|
echo esc_html( $labels[ $pricing_type ] ?? $pricing_type );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'price':
|
||||||
|
$pricing_type = get_post_meta( $post_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking';
|
||||||
|
if ( 'included' === $pricing_type ) {
|
||||||
|
echo '<span class="bnb-service-included">' . esc_html__( 'Included', 'wp-bnb' ) . '</span>';
|
||||||
|
} else {
|
||||||
|
$price = get_post_meta( $post_id, self::META_PREFIX . 'price', true );
|
||||||
|
if ( $price ) {
|
||||||
|
echo esc_html( Calculator::formatPrice( (float) $price ) );
|
||||||
|
if ( 'per_night' === $pricing_type ) {
|
||||||
|
echo ' <small style="color: #646970;">' . esc_html__( '/ night', 'wp-bnb' ) . '</small>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<span class="bnb-no-price">' . esc_html__( 'Not set', 'wp-bnb' ) . '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'service_status':
|
||||||
|
$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: 'active';
|
||||||
|
$classes = array(
|
||||||
|
'active' => 'bnb-service-status-active',
|
||||||
|
'inactive' => 'bnb-service-status-inactive',
|
||||||
|
);
|
||||||
|
$labels = array(
|
||||||
|
'active' => __( 'Active', 'wp-bnb' ),
|
||||||
|
'inactive' => __( 'Inactive', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
echo '<span class="bnb-service-status ' . esc_attr( $classes[ $status ] ?? '' ) . '">';
|
||||||
|
echo esc_html( $labels[ $status ] ?? $status );
|
||||||
|
echo '</span>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add sortable columns.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing sortable columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function sortable_columns( array $columns ): array {
|
||||||
|
$columns['price'] = 'price';
|
||||||
|
$columns['service_status'] = 'status';
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add filter dropdowns to admin list.
|
||||||
|
*
|
||||||
|
* @param string $post_type Current post type.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_filters( string $post_type ): void {
|
||||||
|
if ( self::POST_TYPE !== $post_type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||||||
|
$selected_status = isset( $_GET['service_status'] ) ? sanitize_text_field( wp_unslash( $_GET['service_status'] ) ) : '';
|
||||||
|
?>
|
||||||
|
<select name="service_status">
|
||||||
|
<option value=""><?php esc_html_e( 'All Statuses', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="active" <?php selected( $selected_status, 'active' ); ?>><?php esc_html_e( 'Active', 'wp-bnb' ); ?></option>
|
||||||
|
<option value="inactive" <?php selected( $selected_status, 'inactive' ); ?>><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></option>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Pricing type filter.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter display only.
|
||||||
|
$selected_pricing = isset( $_GET['pricing_type'] ) ? sanitize_text_field( wp_unslash( $_GET['pricing_type'] ) ) : '';
|
||||||
|
$labels = self::get_pricing_type_labels();
|
||||||
|
?>
|
||||||
|
<select name="pricing_type">
|
||||||
|
<option value=""><?php esc_html_e( 'All Pricing Types', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $labels as $value => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected_pricing, $value ); ?>>
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter services by status and pricing type in admin list.
|
||||||
|
*
|
||||||
|
* @param \WP_Query $query Current query.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function filter_query( \WP_Query $query ): void {
|
||||||
|
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta_query = array();
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
if ( ! empty( $_GET['service_status'] ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => self::META_PREFIX . 'status',
|
||||||
|
'value' => sanitize_text_field( wp_unslash( $_GET['service_status'] ) ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only.
|
||||||
|
if ( ! empty( $_GET['pricing_type'] ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => self::META_PREFIX . 'pricing_type',
|
||||||
|
'value' => sanitize_text_field( wp_unslash( $_GET['pricing_type'] ) ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta_query ) ) {
|
||||||
|
$meta_query['relation'] = 'AND';
|
||||||
|
$query->set( 'meta_query', $meta_query );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sorting.
|
||||||
|
$orderby = $query->get( 'orderby' );
|
||||||
|
if ( 'price' === $orderby ) {
|
||||||
|
$query->set( 'meta_key', self::META_PREFIX . 'price' );
|
||||||
|
$query->set( 'orderby', 'meta_value_num' );
|
||||||
|
} elseif ( 'status' === $orderby ) {
|
||||||
|
$query->set( 'meta_key', self::META_PREFIX . 'status' );
|
||||||
|
$query->set( 'orderby', 'meta_value' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change title placeholder.
|
||||||
|
*
|
||||||
|
* @param string $placeholder Default placeholder.
|
||||||
|
* @param \WP_Post $post Current post.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string {
|
||||||
|
if ( self::POST_TYPE === $post->post_type ) {
|
||||||
|
return __( 'Service name', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
return $placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pricing type labels.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_pricing_type_labels(): array {
|
||||||
|
return array(
|
||||||
|
'included' => __( 'Included (Free)', 'wp-bnb' ),
|
||||||
|
'per_booking' => __( 'Per Booking', 'wp-bnb' ),
|
||||||
|
'per_night' => __( 'Per Night', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active services.
|
||||||
|
*
|
||||||
|
* @param array $args Additional query args.
|
||||||
|
* @return array<\WP_Post>
|
||||||
|
*/
|
||||||
|
public static function get_active_services( array $args = array() ): array {
|
||||||
|
$defaults = array(
|
||||||
|
'post_type' => self::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => self::META_PREFIX . 'status',
|
||||||
|
'value' => 'active',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'meta_key' => self::META_PREFIX . 'sort_order',
|
||||||
|
'orderby' => 'meta_value_num',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
return get_posts( array_merge( $defaults, $args ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get service data for a service.
|
||||||
|
*
|
||||||
|
* @param int $service_id Service post ID.
|
||||||
|
* @return array|null Service data or null if not found.
|
||||||
|
*/
|
||||||
|
public static function get_service_data( int $service_id ): ?array {
|
||||||
|
$service = get_post( $service_id );
|
||||||
|
if ( ! $service || self::POST_TYPE !== $service->post_type ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $service_id,
|
||||||
|
'name' => $service->post_title,
|
||||||
|
'description' => $service->post_content,
|
||||||
|
'pricing_type' => get_post_meta( $service_id, self::META_PREFIX . 'pricing_type', true ) ?: 'per_booking',
|
||||||
|
'price' => (float) get_post_meta( $service_id, self::META_PREFIX . 'price', true ),
|
||||||
|
'status' => get_post_meta( $service_id, self::META_PREFIX . 'status', true ) ?: 'active',
|
||||||
|
'sort_order' => (int) get_post_meta( $service_id, self::META_PREFIX . 'sort_order', true ),
|
||||||
|
'max_quantity' => (int) get_post_meta( $service_id, self::META_PREFIX . 'max_quantity', true ) ?: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate service price for a booking.
|
||||||
|
*
|
||||||
|
* @param int $service_id Service post ID.
|
||||||
|
* @param int $quantity Quantity of the service.
|
||||||
|
* @param int $nights Number of nights (for per-night pricing).
|
||||||
|
* @return float Calculated price.
|
||||||
|
*/
|
||||||
|
public static function calculate_service_price( int $service_id, int $quantity = 1, int $nights = 1 ): float {
|
||||||
|
$data = self::get_service_data( $service_id );
|
||||||
|
if ( ! $data ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'included' === $data['pricing_type'] ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$base_price = $data['price'];
|
||||||
|
|
||||||
|
if ( 'per_night' === $data['pricing_type'] ) {
|
||||||
|
return $base_price * $quantity * max( 1, $nights );
|
||||||
|
}
|
||||||
|
|
||||||
|
// per_booking.
|
||||||
|
return $base_price * $quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get services for booking display/selection.
|
||||||
|
*
|
||||||
|
* @return array Array of services with their data.
|
||||||
|
*/
|
||||||
|
public static function get_services_for_booking(): array {
|
||||||
|
$services = self::get_active_services();
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$data = self::get_service_data( $service->ID );
|
||||||
|
if ( $data ) {
|
||||||
|
$data['formatted_price'] = self::format_service_price( $data );
|
||||||
|
$result[] = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format service price for display.
|
||||||
|
*
|
||||||
|
* @param array $service_data Service data array.
|
||||||
|
* @return string Formatted price string.
|
||||||
|
*/
|
||||||
|
public static function format_service_price( array $service_data ): string {
|
||||||
|
if ( 'included' === $service_data['pricing_type'] ) {
|
||||||
|
return __( 'Included', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatted = Calculator::formatPrice( $service_data['price'] );
|
||||||
|
|
||||||
|
if ( 'per_night' === $service_data['pricing_type'] ) {
|
||||||
|
/* translators: %s: Formatted price */
|
||||||
|
return sprintf( __( '%s / night', 'wp-bnb' ), $formatted );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
800
src/Privacy/Manager.php
Normal file
800
src/Privacy/Manager.php
Normal file
@@ -0,0 +1,800 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Privacy Manager for GDPR compliance.
|
||||||
|
*
|
||||||
|
* Handles personal data export and erasure for WordPress privacy tools.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Privacy
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Privacy;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Privacy Manager class for GDPR compliance.
|
||||||
|
*/
|
||||||
|
final class Manager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager instance.
|
||||||
|
*
|
||||||
|
* @var Manager|null
|
||||||
|
*/
|
||||||
|
private static ?Manager $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get manager instance.
|
||||||
|
*
|
||||||
|
* @return Manager
|
||||||
|
*/
|
||||||
|
public static function get_instance(): Manager {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to enforce singleton.
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hooks.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
self::get_instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize WordPress hooks.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// Register personal data exporters.
|
||||||
|
add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporters' ) );
|
||||||
|
|
||||||
|
// Register personal data erasers.
|
||||||
|
add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_erasers' ) );
|
||||||
|
|
||||||
|
// Add privacy policy content suggestion.
|
||||||
|
add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register personal data exporters.
|
||||||
|
*
|
||||||
|
* @param array $exporters Existing exporters.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function register_exporters( array $exporters ): array {
|
||||||
|
$exporters['wp-bnb-guest'] = array(
|
||||||
|
'exporter_friendly_name' => __( 'WP BnB Guest Profile', 'wp-bnb' ),
|
||||||
|
'callback' => array( $this, 'export_guest_data' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$exporters['wp-bnb-bookings'] = array(
|
||||||
|
'exporter_friendly_name' => __( 'WP BnB Booking History', 'wp-bnb' ),
|
||||||
|
'callback' => array( $this, 'export_booking_data' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $exporters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register personal data erasers.
|
||||||
|
*
|
||||||
|
* @param array $erasers Existing erasers.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function register_erasers( array $erasers ): array {
|
||||||
|
$erasers['wp-bnb-guest'] = array(
|
||||||
|
'eraser_friendly_name' => __( 'WP BnB Guest Profile', 'wp-bnb' ),
|
||||||
|
'callback' => array( $this, 'erase_guest_data' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$erasers['wp-bnb-bookings'] = array(
|
||||||
|
'eraser_friendly_name' => __( 'WP BnB Booking History', 'wp-bnb' ),
|
||||||
|
'callback' => array( $this, 'erase_booking_data' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $erasers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export guest profile data.
|
||||||
|
*
|
||||||
|
* @param string $email Email address to export data for.
|
||||||
|
* @param int $page Page number for pagination.
|
||||||
|
* @return array Export data array.
|
||||||
|
*/
|
||||||
|
public function export_guest_data( string $email, int $page = 1 ): array {
|
||||||
|
$export_items = array();
|
||||||
|
|
||||||
|
// Find guest by email.
|
||||||
|
$guest = Guest::get_by_email( $email );
|
||||||
|
|
||||||
|
if ( $guest ) {
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
// Basic information.
|
||||||
|
$first_name = get_post_meta( $guest->ID, '_bnb_guest_first_name', true );
|
||||||
|
$last_name = get_post_meta( $guest->ID, '_bnb_guest_last_name', true );
|
||||||
|
|
||||||
|
if ( $first_name ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'First Name', 'wp-bnb' ),
|
||||||
|
'value' => $first_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $last_name ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Last Name', 'wp-bnb' ),
|
||||||
|
'value' => $last_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Email', 'wp-bnb' ),
|
||||||
|
'value' => get_post_meta( $guest->ID, '_bnb_guest_email', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$phone = get_post_meta( $guest->ID, '_bnb_guest_phone', true );
|
||||||
|
if ( $phone ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Phone', 'wp-bnb' ),
|
||||||
|
'value' => $phone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address.
|
||||||
|
$street = get_post_meta( $guest->ID, '_bnb_guest_street', true );
|
||||||
|
if ( $street ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Street Address', 'wp-bnb' ),
|
||||||
|
'value' => $street,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$city = get_post_meta( $guest->ID, '_bnb_guest_city', true );
|
||||||
|
if ( $city ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'City', 'wp-bnb' ),
|
||||||
|
'value' => $city,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$postal_code = get_post_meta( $guest->ID, '_bnb_guest_postal_code', true );
|
||||||
|
if ( $postal_code ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Postal Code', 'wp-bnb' ),
|
||||||
|
'value' => $postal_code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$country = get_post_meta( $guest->ID, '_bnb_guest_country', true );
|
||||||
|
if ( $country ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Country', 'wp-bnb' ),
|
||||||
|
'value' => $country,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Personal details.
|
||||||
|
$nationality = get_post_meta( $guest->ID, '_bnb_guest_nationality', true );
|
||||||
|
if ( $nationality ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Nationality', 'wp-bnb' ),
|
||||||
|
'value' => $nationality,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$date_of_birth = get_post_meta( $guest->ID, '_bnb_guest_date_of_birth', true );
|
||||||
|
if ( $date_of_birth ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Date of Birth', 'wp-bnb' ),
|
||||||
|
'value' => $date_of_birth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID information (sensitive).
|
||||||
|
$id_type = get_post_meta( $guest->ID, '_bnb_guest_id_type', true );
|
||||||
|
if ( $id_type ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'ID Type', 'wp-bnb' ),
|
||||||
|
'value' => $id_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id_number = get_post_meta( $guest->ID, '_bnb_guest_id_number', true );
|
||||||
|
if ( $id_number ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'ID Number', 'wp-bnb' ),
|
||||||
|
'value' => $id_number,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id_expiry = get_post_meta( $guest->ID, '_bnb_guest_id_expiry', true );
|
||||||
|
if ( $id_expiry ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'ID Expiry Date', 'wp-bnb' ),
|
||||||
|
'value' => $id_expiry,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consent information.
|
||||||
|
$consent_data = get_post_meta( $guest->ID, '_bnb_guest_consent_data', true );
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Data Processing Consent', 'wp-bnb' ),
|
||||||
|
'value' => $consent_data ? __( 'Yes', 'wp-bnb' ) : __( 'No', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$consent_marketing = get_post_meta( $guest->ID, '_bnb_guest_consent_marketing', true );
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Marketing Consent', 'wp-bnb' ),
|
||||||
|
'value' => $consent_marketing ? __( 'Yes', 'wp-bnb' ) : __( 'No', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$consent_date = get_post_meta( $guest->ID, '_bnb_guest_consent_date', true );
|
||||||
|
if ( $consent_date ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Consent Date', 'wp-bnb' ),
|
||||||
|
'value' => $consent_date,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notes and preferences.
|
||||||
|
$preferences = get_post_meta( $guest->ID, '_bnb_guest_preferences', true );
|
||||||
|
if ( $preferences ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Guest Preferences', 'wp-bnb' ),
|
||||||
|
'value' => $preferences,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $data ) ) {
|
||||||
|
$export_items[] = array(
|
||||||
|
'group_id' => 'wp-bnb-guest',
|
||||||
|
'group_label' => __( 'Guest Profile', 'wp-bnb' ),
|
||||||
|
'group_description' => __( 'Your guest profile information stored by WP BnB.', 'wp-bnb' ),
|
||||||
|
'item_id' => 'guest-' . $guest->ID,
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'data' => $export_items,
|
||||||
|
'done' => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export booking history data.
|
||||||
|
*
|
||||||
|
* @param string $email Email address to export data for.
|
||||||
|
* @param int $page Page number for pagination.
|
||||||
|
* @return array Export data array.
|
||||||
|
*/
|
||||||
|
public function export_booking_data( string $email, int $page = 1 ): array {
|
||||||
|
$export_items = array();
|
||||||
|
$per_page = 20;
|
||||||
|
$offset = ( $page - 1 ) * $per_page;
|
||||||
|
|
||||||
|
// Find bookings by email (both direct and through guest_id).
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => $per_page,
|
||||||
|
'offset' => $offset,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'OR',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also check via guest_id.
|
||||||
|
$guest = Guest::get_by_email( $email );
|
||||||
|
if ( $guest ) {
|
||||||
|
$bookings_by_id = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => $per_page,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_id',
|
||||||
|
'value' => $guest->ID,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge and dedupe.
|
||||||
|
$existing_ids = wp_list_pluck( $bookings, 'ID' );
|
||||||
|
foreach ( $bookings_by_id as $booking ) {
|
||||||
|
if ( ! in_array( $booking->ID, $existing_ids, true ) ) {
|
||||||
|
$bookings[] = $booking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
$reference = get_post_meta( $booking->ID, '_bnb_booking_reference', true );
|
||||||
|
if ( ! $reference ) {
|
||||||
|
$reference = 'BNB-' . $booking->ID;
|
||||||
|
}
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Booking Reference', 'wp-bnb' ),
|
||||||
|
'value' => $reference,
|
||||||
|
);
|
||||||
|
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
if ( $room_id ) {
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( $room ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Room', 'wp-bnb' ),
|
||||||
|
'value' => $room->post_title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
if ( $check_in ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Check-in Date', 'wp-bnb' ),
|
||||||
|
'value' => $check_in,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
if ( $check_out ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Check-out Date', 'wp-bnb' ),
|
||||||
|
'value' => $check_out,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
|
if ( $status ) {
|
||||||
|
$statuses = Booking::get_booking_statuses();
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Status', 'wp-bnb' ),
|
||||||
|
'value' => $statuses[ $status ] ?? $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$adults = get_post_meta( $booking->ID, '_bnb_booking_adults', true );
|
||||||
|
if ( $adults ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Adults', 'wp-bnb' ),
|
||||||
|
'value' => $adults,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$children = get_post_meta( $booking->ID, '_bnb_booking_children', true );
|
||||||
|
if ( $children ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Children', 'wp-bnb' ),
|
||||||
|
'value' => $children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
|
if ( $price ) {
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Total Price', 'wp-bnb' ),
|
||||||
|
'value' => number_format( (float) $price, 2 ) . ' ' . $currency,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$guest_notes = get_post_meta( $booking->ID, '_bnb_booking_guest_notes', true );
|
||||||
|
if ( $guest_notes ) {
|
||||||
|
$data[] = array(
|
||||||
|
'name' => __( 'Guest Notes', 'wp-bnb' ),
|
||||||
|
'value' => $guest_notes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $data ) ) {
|
||||||
|
$export_items[] = array(
|
||||||
|
'group_id' => 'wp-bnb-bookings',
|
||||||
|
'group_label' => __( 'Booking History', 'wp-bnb' ),
|
||||||
|
'group_description' => __( 'Your booking history with WP BnB.', 'wp-bnb' ),
|
||||||
|
'item_id' => 'booking-' . $booking->ID,
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are more bookings.
|
||||||
|
$total_bookings = $this->count_bookings_by_email( $email );
|
||||||
|
$done = ( $offset + $per_page ) >= $total_bookings;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'data' => $export_items,
|
||||||
|
'done' => $done,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase guest profile data.
|
||||||
|
*
|
||||||
|
* @param string $email Email address to erase data for.
|
||||||
|
* @param int $page Page number for pagination.
|
||||||
|
* @return array Erasure result array.
|
||||||
|
*/
|
||||||
|
public function erase_guest_data( string $email, int $page = 1 ): array {
|
||||||
|
$items_removed = 0;
|
||||||
|
$items_retained = 0;
|
||||||
|
$messages = array();
|
||||||
|
|
||||||
|
// Find guest by email.
|
||||||
|
$guest = Guest::get_by_email( $email );
|
||||||
|
|
||||||
|
if ( $guest ) {
|
||||||
|
// Check if guest has active bookings.
|
||||||
|
$active_bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => 1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'relation' => 'OR',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_id',
|
||||||
|
'value' => $guest->ID,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'pending', 'confirmed', 'checked_in' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $active_bookings ) ) {
|
||||||
|
// Cannot delete - has active bookings.
|
||||||
|
$messages[] = __( 'Guest profile retained due to active bookings.', 'wp-bnb' );
|
||||||
|
$items_retained = 1;
|
||||||
|
} else {
|
||||||
|
// Anonymize the guest profile instead of deleting.
|
||||||
|
$this->anonymize_guest( $guest->ID );
|
||||||
|
$items_removed = 1;
|
||||||
|
$messages[] = __( 'Guest profile anonymized.', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'items_removed' => $items_removed,
|
||||||
|
'items_retained' => $items_retained,
|
||||||
|
'messages' => $messages,
|
||||||
|
'done' => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase booking data.
|
||||||
|
*
|
||||||
|
* @param string $email Email address to erase data for.
|
||||||
|
* @param int $page Page number for pagination.
|
||||||
|
* @return array Erasure result array.
|
||||||
|
*/
|
||||||
|
public function erase_booking_data( string $email, int $page = 1 ): array {
|
||||||
|
$items_removed = 0;
|
||||||
|
$items_retained = 0;
|
||||||
|
$messages = array();
|
||||||
|
$per_page = 20;
|
||||||
|
|
||||||
|
// Find completed bookings (can be anonymized).
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => $per_page,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'checked_out', 'cancelled' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also find by guest_id.
|
||||||
|
$guest = Guest::get_by_email( $email );
|
||||||
|
if ( $guest ) {
|
||||||
|
$more_bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => $per_page,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_id',
|
||||||
|
'value' => $guest->ID,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'checked_out', 'cancelled' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$existing_ids = wp_list_pluck( $bookings, 'ID' );
|
||||||
|
foreach ( $more_bookings as $booking ) {
|
||||||
|
if ( ! in_array( $booking->ID, $existing_ids, true ) ) {
|
||||||
|
$bookings[] = $booking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$this->anonymize_booking( $booking->ID );
|
||||||
|
++$items_removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for active bookings that can't be erased.
|
||||||
|
$active_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_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'pending', 'confirmed', 'checked_in' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$items_retained = count( $active_bookings );
|
||||||
|
|
||||||
|
if ( $items_retained > 0 ) {
|
||||||
|
$messages[] = sprintf(
|
||||||
|
/* translators: %d: Number of bookings */
|
||||||
|
_n(
|
||||||
|
'%d booking retained due to active status.',
|
||||||
|
'%d bookings retained due to active status.',
|
||||||
|
$items_retained,
|
||||||
|
'wp-bnb'
|
||||||
|
),
|
||||||
|
$items_retained
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $items_removed > 0 ) {
|
||||||
|
$messages[] = sprintf(
|
||||||
|
/* translators: %d: Number of bookings */
|
||||||
|
_n(
|
||||||
|
'%d booking anonymized.',
|
||||||
|
'%d bookings anonymized.',
|
||||||
|
$items_removed,
|
||||||
|
'wp-bnb'
|
||||||
|
),
|
||||||
|
$items_removed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'items_removed' => $items_removed,
|
||||||
|
'items_retained' => $items_retained,
|
||||||
|
'messages' => $messages,
|
||||||
|
'done' => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anonymize a guest record.
|
||||||
|
*
|
||||||
|
* @param int $guest_id Guest post ID.
|
||||||
|
* @return bool True on success.
|
||||||
|
*/
|
||||||
|
public function anonymize_guest( int $guest_id ): bool {
|
||||||
|
$anonymized = __( '[Deleted]', 'wp-bnb' );
|
||||||
|
|
||||||
|
// Update post title.
|
||||||
|
wp_update_post(
|
||||||
|
array(
|
||||||
|
'ID' => $guest_id,
|
||||||
|
'post_title' => $anonymized,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Anonymize personal data.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_first_name', $anonymized );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_last_name', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_email', 'deleted-' . $guest_id . '@anonymized.local' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_phone', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_street', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_city', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_postal_code', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_country', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_nationality', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_date_of_birth', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_id_type', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_id_number', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_id_expiry', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_preferences', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_notes', '' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_status', 'inactive' );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anonymize a booking record.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool True on success.
|
||||||
|
*/
|
||||||
|
public function anonymize_booking( int $booking_id ): bool {
|
||||||
|
$anonymized = __( '[Deleted]', 'wp-bnb' );
|
||||||
|
|
||||||
|
// Remove guest reference.
|
||||||
|
delete_post_meta( $booking_id, '_bnb_booking_guest_id' );
|
||||||
|
|
||||||
|
// Anonymize guest data stored in booking.
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_name', $anonymized );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_email', '' );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_phone', '' );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_notes', '' );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count bookings by email.
|
||||||
|
*
|
||||||
|
* @param string $email Email address.
|
||||||
|
* @return int Count of bookings.
|
||||||
|
*/
|
||||||
|
private function count_bookings_by_email( string $email ): int {
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
// Direct email match.
|
||||||
|
$direct = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$count += count( $direct );
|
||||||
|
|
||||||
|
// Guest ID match.
|
||||||
|
$guest = Guest::get_by_email( $email );
|
||||||
|
if ( $guest ) {
|
||||||
|
$by_guest_id = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'post__not_in' => $direct,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_guest_id',
|
||||||
|
'value' => $guest->ID,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$count += count( $by_guest_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add privacy policy content suggestion.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add_privacy_policy_content(): void {
|
||||||
|
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = sprintf(
|
||||||
|
'<h2>%s</h2>
|
||||||
|
<p>%s</p>
|
||||||
|
|
||||||
|
<h3>%s</h3>
|
||||||
|
<p>%s</p>
|
||||||
|
<ul>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>%s</h3>
|
||||||
|
<p>%s</p>
|
||||||
|
<ul>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>%s</h3>
|
||||||
|
<p>%s</p>
|
||||||
|
|
||||||
|
<h3>%s</h3>
|
||||||
|
<p>%s</p>',
|
||||||
|
__( 'Accommodation Booking', 'wp-bnb' ),
|
||||||
|
__( 'When you make a booking with us, we collect and process the following personal data to fulfill your reservation and comply with legal requirements.', 'wp-bnb' ),
|
||||||
|
__( 'What personal data we collect', 'wp-bnb' ),
|
||||||
|
__( 'We collect the following information when you make a booking:', 'wp-bnb' ),
|
||||||
|
__( 'Name and contact information (email, phone)', 'wp-bnb' ),
|
||||||
|
__( 'Address for billing and guest registration', 'wp-bnb' ),
|
||||||
|
__( 'Identity document information (as required by local regulations)', 'wp-bnb' ),
|
||||||
|
__( 'Booking details (dates, room preferences, special requests)', 'wp-bnb' ),
|
||||||
|
__( 'Payment information (processed securely by payment providers)', 'wp-bnb' ),
|
||||||
|
__( 'Why we collect this data', 'wp-bnb' ),
|
||||||
|
__( 'We use your personal data for the following purposes:', 'wp-bnb' ),
|
||||||
|
__( 'Processing and managing your booking', 'wp-bnb' ),
|
||||||
|
__( 'Communicating with you about your reservation', 'wp-bnb' ),
|
||||||
|
__( 'Complying with legal guest registration requirements', 'wp-bnb' ),
|
||||||
|
__( 'How long we retain your data', 'wp-bnb' ),
|
||||||
|
__( 'We retain your booking data for the period required by law for guest registration and accounting purposes, typically 10 years. After this period, your data will be anonymized or deleted.', 'wp-bnb' ),
|
||||||
|
__( 'Your rights', 'wp-bnb' ),
|
||||||
|
__( 'You have the right to access, correct, or request deletion of your personal data. To exercise these rights, please contact us using the information provided on this website. Note that some data may need to be retained for legal compliance purposes.', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_add_privacy_policy_content( 'WP BnB', wp_kses_post( $content ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
276
src/Taxonomies/ServiceCategory.php
Normal file
276
src/Taxonomies/ServiceCategory.php
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Service Category taxonomy.
|
||||||
|
*
|
||||||
|
* Non-hierarchical taxonomy for categorizing additional services.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Taxonomies
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Taxonomies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Category taxonomy class.
|
||||||
|
*/
|
||||||
|
final class ServiceCategory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taxonomy slug.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const TAXONOMY = 'bnb_service_category';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'init', array( self::class, 'register' ) );
|
||||||
|
add_action( 'bnb_service_category_add_form_fields', array( self::class, 'add_form_fields' ) );
|
||||||
|
add_action( 'bnb_service_category_edit_form_fields', array( self::class, 'edit_form_fields' ), 10, 2 );
|
||||||
|
add_action( 'created_bnb_service_category', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_action( 'edited_bnb_service_category', array( self::class, 'save_term_meta' ), 10, 2 );
|
||||||
|
add_filter( 'manage_edit-bnb_service_category_columns', array( self::class, 'add_columns' ) );
|
||||||
|
add_filter( 'manage_bnb_service_category_custom_column', array( self::class, 'render_column' ), 10, 3 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the taxonomy.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register(): void {
|
||||||
|
$labels = array(
|
||||||
|
'name' => _x( 'Service Categories', 'taxonomy general name', 'wp-bnb' ),
|
||||||
|
'singular_name' => _x( 'Service Category', 'taxonomy singular name', 'wp-bnb' ),
|
||||||
|
'search_items' => __( 'Search Service Categories', 'wp-bnb' ),
|
||||||
|
'popular_items' => __( 'Popular Service Categories', 'wp-bnb' ),
|
||||||
|
'all_items' => __( 'All Service Categories', 'wp-bnb' ),
|
||||||
|
'parent_item' => null,
|
||||||
|
'parent_item_colon' => null,
|
||||||
|
'edit_item' => __( 'Edit Service Category', 'wp-bnb' ),
|
||||||
|
'update_item' => __( 'Update Service Category', 'wp-bnb' ),
|
||||||
|
'add_new_item' => __( 'Add New Service Category', 'wp-bnb' ),
|
||||||
|
'new_item_name' => __( 'New Service Category Name', 'wp-bnb' ),
|
||||||
|
'separate_items_with_commas' => __( 'Separate categories with commas', 'wp-bnb' ),
|
||||||
|
'add_or_remove_items' => __( 'Add or remove categories', 'wp-bnb' ),
|
||||||
|
'choose_from_most_used' => __( 'Choose from the most used categories', 'wp-bnb' ),
|
||||||
|
'not_found' => __( 'No service categories found.', 'wp-bnb' ),
|
||||||
|
'menu_name' => __( 'Categories', 'wp-bnb' ),
|
||||||
|
'back_to_items' => __( '← Back to Categories', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'hierarchical' => false, // Non-hierarchical (like tags).
|
||||||
|
'public' => true,
|
||||||
|
'publicly_queryable' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_menu' => true,
|
||||||
|
'show_in_nav_menus' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'show_tagcloud' => false,
|
||||||
|
'show_in_quick_edit' => true,
|
||||||
|
'show_admin_column' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'service-category',
|
||||||
|
'with_front' => false,
|
||||||
|
),
|
||||||
|
'query_var' => true,
|
||||||
|
'capabilities' => array(
|
||||||
|
'manage_terms' => 'manage_options',
|
||||||
|
'edit_terms' => 'manage_options',
|
||||||
|
'delete_terms' => 'manage_options',
|
||||||
|
'assign_terms' => 'edit_posts',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
register_taxonomy( self::TAXONOMY, array( 'bnb_service' ), $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom fields to the add term form.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_form_fields(): void {
|
||||||
|
?>
|
||||||
|
<div class="form-field term-icon-wrap">
|
||||||
|
<label for="service-category-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||||
|
<select name="service_category_icon" id="service-category-icon">
|
||||||
|
<?php foreach ( self::get_icon_options() as $value => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<p><?php esc_html_e( 'Select an icon to represent this category.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-field term-sort-order-wrap">
|
||||||
|
<label for="service-category-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||||
|
<input type="number" name="service_category_sort_order" id="service-category-sort-order" value="0" min="0">
|
||||||
|
<p><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom fields to the edit term form.
|
||||||
|
*
|
||||||
|
* @param \WP_Term $term Current term object.
|
||||||
|
* @param string $taxonomy Current taxonomy slug.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function edit_form_fields( \WP_Term $term, string $taxonomy ): void {
|
||||||
|
$icon = get_term_meta( $term->term_id, 'service_category_icon', true );
|
||||||
|
$sort_order = get_term_meta( $term->term_id, 'service_category_sort_order', true );
|
||||||
|
?>
|
||||||
|
<tr class="form-field term-icon-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="service-category-icon"><?php esc_html_e( 'Icon', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="service_category_icon" id="service-category-icon">
|
||||||
|
<?php foreach ( self::get_icon_options() as $value => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $icon, $value ); ?>>
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php esc_html_e( 'Select an icon to represent this category.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="form-field term-sort-order-wrap">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="service-category-sort-order"><?php esc_html_e( 'Sort Order', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="service_category_sort_order" id="service-category-sort-order"
|
||||||
|
value="<?php echo esc_attr( $sort_order ?: '0' ); ?>" min="0">
|
||||||
|
<p class="description"><?php esc_html_e( 'Lower numbers appear first.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save term meta data.
|
||||||
|
*
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @param int $tt_id Term taxonomy ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function save_term_meta( int $term_id, int $tt_id ): void {
|
||||||
|
if ( isset( $_POST['service_category_icon'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'service_category_icon',
|
||||||
|
sanitize_text_field( wp_unslash( $_POST['service_category_icon'] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ( isset( $_POST['service_category_sort_order'] ) ) {
|
||||||
|
update_term_meta(
|
||||||
|
$term_id,
|
||||||
|
'service_category_sort_order',
|
||||||
|
absint( $_POST['service_category_sort_order'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom columns to the taxonomy list.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing columns.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function add_columns( array $columns ): array {
|
||||||
|
$new_columns = array();
|
||||||
|
foreach ( $columns as $key => $value ) {
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
if ( 'name' === $key ) {
|
||||||
|
$new_columns['icon'] = __( 'Icon', 'wp-bnb' );
|
||||||
|
$new_columns['sort_order'] = __( 'Sort Order', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render custom column content.
|
||||||
|
*
|
||||||
|
* @param string $content Column content.
|
||||||
|
* @param string $column_name Column name.
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function render_column( string $content, string $column_name, int $term_id ): string {
|
||||||
|
if ( 'icon' === $column_name ) {
|
||||||
|
$icon = get_term_meta( $term_id, 'service_category_icon', true );
|
||||||
|
if ( $icon ) {
|
||||||
|
return '<span class="dashicons dashicons-' . esc_attr( $icon ) . '"></span>';
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
if ( 'sort_order' === $column_name ) {
|
||||||
|
$sort_order = get_term_meta( $term_id, 'service_category_sort_order', true );
|
||||||
|
return esc_html( $sort_order ?: '0' );
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available icon options.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function get_icon_options(): array {
|
||||||
|
return array(
|
||||||
|
'' => __( '— Select Icon —', 'wp-bnb' ),
|
||||||
|
'food' => __( 'Food & Dining', 'wp-bnb' ),
|
||||||
|
'car' => __( 'Transportation', 'wp-bnb' ),
|
||||||
|
'heart' => __( 'Wellness & Spa', 'wp-bnb' ),
|
||||||
|
'tickets-alt' => __( 'Activities', 'wp-bnb' ),
|
||||||
|
'admin-home' => __( 'Housekeeping', 'wp-bnb' ),
|
||||||
|
'admin-appearance' => __( 'Room Service', 'wp-bnb' ),
|
||||||
|
'store' => __( 'Shopping', 'wp-bnb' ),
|
||||||
|
'groups' => __( 'Childcare', 'wp-bnb' ),
|
||||||
|
'pets' => __( 'Pet Services', 'wp-bnb' ),
|
||||||
|
'businessman' => __( 'Business', 'wp-bnb' ),
|
||||||
|
'calendar' => __( 'Events', 'wp-bnb' ),
|
||||||
|
'camera' => __( 'Photography', 'wp-bnb' ),
|
||||||
|
'admin-generic' => __( 'Other', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default service categories to seed on activation.
|
||||||
|
*
|
||||||
|
* @return array<string, array{icon: string, sort_order: int}>
|
||||||
|
*/
|
||||||
|
public static function get_default_terms(): array {
|
||||||
|
return array(
|
||||||
|
__( 'Food & Dining', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'food',
|
||||||
|
'sort_order' => 10,
|
||||||
|
),
|
||||||
|
__( 'Transportation', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'car',
|
||||||
|
'sort_order' => 20,
|
||||||
|
),
|
||||||
|
__( 'Wellness & Spa', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'heart',
|
||||||
|
'sort_order' => 30,
|
||||||
|
),
|
||||||
|
__( 'Activities', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'tickets-alt',
|
||||||
|
'sort_order' => 40,
|
||||||
|
),
|
||||||
|
__( 'Housekeeping', 'wp-bnb' ) => array(
|
||||||
|
'icon' => 'admin-home',
|
||||||
|
'sort_order' => 50,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP BnB Management
|
* Plugin Name: WP BnB Management
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||||
* Version: 0.3.0
|
* Version: 0.6.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.3.0' );
|
define( 'WP_BNB_VERSION', '0.6.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
@@ -165,6 +165,8 @@ function wp_bnb_activate(): void {
|
|||||||
\Magdev\WpBnb\Taxonomies\RoomType::register();
|
\Magdev\WpBnb\Taxonomies\RoomType::register();
|
||||||
\Magdev\WpBnb\PostTypes\Building::register();
|
\Magdev\WpBnb\PostTypes\Building::register();
|
||||||
\Magdev\WpBnb\PostTypes\Room::register();
|
\Magdev\WpBnb\PostTypes\Room::register();
|
||||||
|
\Magdev\WpBnb\PostTypes\Booking::register();
|
||||||
|
\Magdev\WpBnb\PostTypes\Guest::register();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default options.
|
// Set default options.
|
||||||
|
|||||||
Reference in New Issue
Block a user