Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aab3a4d1aa | |||
| c66af8e299 | |||
| 0c601df568 |
126
CHANGELOG.md
126
CHANGELOG.md
@@ -5,6 +5,130 @@ 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.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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Booking System with full management features:
|
||||||
|
- Custom Post Type: Bookings (`bnb_booking`)
|
||||||
|
- Room and guest relationship tracking
|
||||||
|
- Check-in/check-out date management
|
||||||
|
- Booking status workflow (pending, confirmed, checked_in, checked_out, cancelled)
|
||||||
|
- Status transitions with validation
|
||||||
|
- Automatic price calculation using existing Calculator
|
||||||
|
- Price override option for manual adjustments
|
||||||
|
- Guest information storage (name, email, phone, notes)
|
||||||
|
- Guest count tracking (adults, children)
|
||||||
|
- Internal notes field for staff
|
||||||
|
- Auto-generated booking references (BNB-YYYY-NNNNN)
|
||||||
|
- Availability System (`src/Booking/Availability.php`)
|
||||||
|
- Real-time availability checking
|
||||||
|
- Conflict detection for overlapping bookings
|
||||||
|
- AJAX endpoint for instant availability validation
|
||||||
|
- Calendar data generation for rooms and buildings
|
||||||
|
- Support for excluding booking from conflict check (for editing)
|
||||||
|
- Utility methods: get upcoming bookings, current bookings, today's check-ins/outs
|
||||||
|
- Calendar Admin Page (WP BnB > Calendar)
|
||||||
|
- Monthly calendar view with availability visualization
|
||||||
|
- Room and building filter dropdowns
|
||||||
|
- Color-coded booking status display
|
||||||
|
- Month navigation (previous/next/today)
|
||||||
|
- Click-to-edit booking functionality
|
||||||
|
- Hover tooltips with booking details
|
||||||
|
- Legend for status colors
|
||||||
|
- Single room and multi-room views
|
||||||
|
- Email Notifications (`src/Booking/EmailNotifier.php`)
|
||||||
|
- Admin notification for new bookings
|
||||||
|
- Guest confirmation email on booking confirmation
|
||||||
|
- Admin notification on booking confirmation
|
||||||
|
- Cancellation emails to guest and admin
|
||||||
|
- HTML email templates with styling
|
||||||
|
- Placeholder-based template system
|
||||||
|
- Filter hooks for customizing recipients, subject, and content
|
||||||
|
- Booking Admin List Enhancements
|
||||||
|
- Custom columns: room, guest, dates, nights, price, status
|
||||||
|
- Status badges with color coding
|
||||||
|
- Filter by room and status
|
||||||
|
- Sortable columns for dates, guest, status
|
||||||
|
- Price override indicator
|
||||||
|
- Booking Meta Boxes
|
||||||
|
- Room & Dates: room selection, date pickers, nights display, availability check
|
||||||
|
- Guest Information: contact details, guest count, notes
|
||||||
|
- Pricing: calculated price, breakdown display, recalculate button, override
|
||||||
|
- Status & Notes: status dropdown with preview, internal notes
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php enhanced with AJAX handlers and component initialization
|
||||||
|
- Admin JavaScript updated with booking form functionality
|
||||||
|
- Admin CSS updated with booking and calendar styles
|
||||||
|
- Asset enqueuing now includes Booking post type screens
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Conflict detection prevents double-booking
|
||||||
|
- Date validation ensures check-out is after check-in
|
||||||
|
- Status transition validation prevents invalid state changes
|
||||||
|
- Nonce verification on availability AJAX requests
|
||||||
|
- Capability checks on all booking operations
|
||||||
|
|
||||||
## [0.2.0] - 2026-01-31
|
## [0.2.0] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -124,6 +248,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Input sanitization and output escaping
|
- Input sanitization and output escaping
|
||||||
- Server secret masking in license settings
|
- Server secret masking in license settings
|
||||||
|
|
||||||
|
[0.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.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
|
||||||
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.0.1
|
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.0.1
|
||||||
|
|||||||
105
CLAUDE.md
105
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
|
||||||
@@ -207,10 +232,15 @@ wp-bnb/
|
|||||||
├── src/ # PHP source code (PSR-4: Magdev\WpBnb)
|
├── src/ # PHP source code (PSR-4: Magdev\WpBnb)
|
||||||
│ ├── Plugin.php # Main plugin singleton
|
│ ├── Plugin.php # Main plugin singleton
|
||||||
│ ├── Admin/ # Admin pages
|
│ ├── Admin/ # Admin pages
|
||||||
|
│ │ ├── Calendar.php # Availability calendar page
|
||||||
│ │ └── Seasons.php # Seasons management page
|
│ │ └── Seasons.php # Seasons management page
|
||||||
|
│ ├── Booking/ # Booking system
|
||||||
|
│ │ ├── Availability.php # Availability checking
|
||||||
|
│ │ └── EmailNotifier.php # Email notifications
|
||||||
│ ├── License/
|
│ ├── License/
|
||||||
│ │ └── Manager.php # License management
|
│ │ └── Manager.php # License management
|
||||||
│ ├── PostTypes/ # Custom post types
|
│ ├── PostTypes/ # Custom post types
|
||||||
|
│ │ ├── Booking.php # Booking post type
|
||||||
│ │ ├── Building.php # Building post type
|
│ │ ├── Building.php # Building post type
|
||||||
│ │ └── Room.php # Room post type
|
│ │ └── Room.php # Room post type
|
||||||
│ ├── Pricing/ # Pricing system
|
│ ├── Pricing/ # Pricing system
|
||||||
@@ -397,3 +427,78 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Price modifiers as multipliers are more flexible than percentages
|
- Price modifiers as multipliers are more flexible than percentages
|
||||||
- Calculator class separates concerns from post type class
|
- Calculator class separates concerns from post type class
|
||||||
- Weekend days stored as comma-separated string in options
|
- Weekend days stored as comma-separated string in options
|
||||||
|
|
||||||
|
### 2026-01-31 - Version 0.3.0 (Booking System)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/PostTypes/Booking.php` custom post type
|
||||||
|
- Room and guest relationship tracking
|
||||||
|
- Check-in/check-out date management with validation
|
||||||
|
- Status workflow (pending, confirmed, checked_in, checked_out, cancelled)
|
||||||
|
- Auto-generated booking references (BNB-YYYY-NNNNN)
|
||||||
|
- Four meta boxes: Room & Dates, Guest Info, Pricing, Status & Notes
|
||||||
|
- Conflict detection prevents double-booking
|
||||||
|
- Price calculation using existing Calculator class
|
||||||
|
- Admin columns with room, guest, dates, nights, price, status
|
||||||
|
- Filters by room and status
|
||||||
|
- Status badges with color coding
|
||||||
|
- Created `src/Booking/Availability.php` class
|
||||||
|
- Real-time availability checking via AJAX
|
||||||
|
- Conflict detection algorithm
|
||||||
|
- Calendar data generation for rooms and buildings
|
||||||
|
- Utility methods for upcoming bookings, today's check-ins/outs
|
||||||
|
- Created `src/Admin/Calendar.php` admin page
|
||||||
|
- Monthly calendar view with room/building filters
|
||||||
|
- Color-coded booking status display
|
||||||
|
- Month navigation (previous/next/today)
|
||||||
|
- Click-to-edit booking functionality
|
||||||
|
- Hover tooltips with booking details
|
||||||
|
- Legend for status colors
|
||||||
|
- Created `src/Booking/EmailNotifier.php` class
|
||||||
|
- Admin notification for new bookings
|
||||||
|
- Guest confirmation email on booking confirmation
|
||||||
|
- Cancellation emails to guest and admin
|
||||||
|
- HTML email templates with inline styles
|
||||||
|
- Placeholder-based template system
|
||||||
|
- Filter hooks for customizing emails
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Registered Booking post type
|
||||||
|
- Initialized Calendar admin page
|
||||||
|
- Initialized EmailNotifier
|
||||||
|
- Added AJAX handler for availability checking
|
||||||
|
- Updated asset enqueuing for Booking screens
|
||||||
|
- Updated `assets/js/admin.js`
|
||||||
|
- Booking form with AJAX availability checking
|
||||||
|
- Real-time nights display
|
||||||
|
- Price calculation and display
|
||||||
|
- Status preview update
|
||||||
|
- Date validation (check-out after check-in)
|
||||||
|
- Calendar page interactivity
|
||||||
|
- Updated `assets/css/admin.css`
|
||||||
|
- Booking info display styles
|
||||||
|
- Availability status indicators
|
||||||
|
- Price breakdown styles
|
||||||
|
- Calendar grid and cell styles
|
||||||
|
- Legend and filter styles
|
||||||
|
- Responsive design for calendar
|
||||||
|
- Updated version to 0.3.0
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Booking conflicts use overlap detection: `A.check_in < B.check_out AND A.check_out > B.check_in`
|
||||||
|
- Excluding cancelled bookings from conflict checks allows rebooking same dates
|
||||||
|
- Guest info stored in booking meta (Phase 4 will add separate Guest CPT)
|
||||||
|
- AJAX availability check returns price calculation for immediate feedback
|
||||||
|
- Calendar displays bookings color-coded by status for quick visual overview
|
||||||
|
- HTML email templates with inline CSS for better email client compatibility
|
||||||
|
- Status transitions can trigger different email notifications via hooks
|
||||||
|
- **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
|
||||||
|
|||||||
32
PLAN.md
32
PLAN.md
@@ -59,30 +59,30 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Price breakdown display
|
- [x] Price breakdown display
|
||||||
- [x] Discount handling (via seasonal modifiers)
|
- [x] Discount handling (via seasonal modifiers)
|
||||||
|
|
||||||
## Phase 3: Booking System (v0.3.0)
|
## Phase 3: Booking System (v0.3.0) - Complete
|
||||||
|
|
||||||
### Custom Post Type: Bookings
|
### Custom Post Type: Bookings
|
||||||
|
|
||||||
- [ ] Guest reference
|
- [x] Guest reference
|
||||||
- [ ] Room reference
|
- [x] Room reference
|
||||||
- [ ] Check-in/check-out dates
|
- [x] Check-in/check-out dates
|
||||||
- [ ] Status (pending, confirmed, checked-in, checked-out, cancelled)
|
- [x] Status (pending, confirmed, checked-in, checked-out, cancelled)
|
||||||
- [ ] Price calculation and storage
|
- [x] Price calculation and storage
|
||||||
- [ ] Notes field
|
- [x] Notes field
|
||||||
|
|
||||||
### Calendar Integration
|
### Calendar Integration
|
||||||
|
|
||||||
- [ ] Availability calendar per room
|
- [x] Availability calendar per room
|
||||||
- [ ] Availability calendar per building
|
- [x] Availability calendar per building
|
||||||
- [ ] Date range picker for bookings
|
- [x] Date range picker for bookings
|
||||||
- [ ] Conflict detection
|
- [x] Conflict detection
|
||||||
|
|
||||||
### Booking Workflow
|
### Booking Workflow
|
||||||
|
|
||||||
- [ ] Booking creation (admin)
|
- [x] Booking creation (admin)
|
||||||
- [ ] Status transitions
|
- [x] Status transitions
|
||||||
- [ ] Email notifications
|
- [x] Email notifications
|
||||||
- [ ] Booking confirmation
|
- [x] Booking confirmation
|
||||||
|
|
||||||
## Phase 4: Guest Management (v0.4.0)
|
## Phase 4: Guest Management (v0.4.0)
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| 0.0.1 | Initial setup | Complete |
|
| 0.0.1 | Initial setup | Complete |
|
||||||
| 0.1.0 | Data structures | Complete |
|
| 0.1.0 | Data structures | Complete |
|
||||||
| 0.2.0 | Pricing | Complete |
|
| 0.2.0 | Pricing | Complete |
|
||||||
| 0.3.0 | Bookings | TBD |
|
| 0.3.0 | Bookings | Complete |
|
||||||
| 0.4.0 | Guests | TBD |
|
| 0.4.0 | Guests | TBD |
|
||||||
| 0.5.0 | Services | TBD |
|
| 0.5.0 | Services | TBD |
|
||||||
| 0.6.0 | Frontend | TBD |
|
| 0.6.0 | Frontend | TBD |
|
||||||
|
|||||||
@@ -323,3 +323,768 @@
|
|||||||
.bnb-season-form input[type="text"].small-text {
|
.bnb-season-form input[type="text"].small-text {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Booking System Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Booking Info Display */
|
||||||
|
.bnb-booking-info {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info.bnb-checking {
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info.bnb-available {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info.bnb-not-available {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-info .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Price Display */
|
||||||
|
.bnb-booking-price {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f0f6fc;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-price strong {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Breakdown */
|
||||||
|
.bnb-booking-breakdown,
|
||||||
|
.bnb-breakdown-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-breakdown-list li {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px dotted #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-breakdown-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Override Indicator */
|
||||||
|
.bnb-price-override {
|
||||||
|
color: #dba617;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Preview */
|
||||||
|
.bnb-status-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-timestamp {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Required Field Indicator */
|
||||||
|
.required {
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Admin Columns */
|
||||||
|
.column-room,
|
||||||
|
.column-guest,
|
||||||
|
.column-dates,
|
||||||
|
.column-nights,
|
||||||
|
.column-price {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-room small,
|
||||||
|
.column-guest small,
|
||||||
|
.column-dates small {
|
||||||
|
display: block;
|
||||||
|
color: #646970;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Calendar Page Styles
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Calendar Container */
|
||||||
|
.bnb-calendar-container {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Header */
|
||||||
|
.bnb-calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Filters */
|
||||||
|
.bnb-calendar-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-filters label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-filters select {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Grid */
|
||||||
|
.bnb-calendar-grid {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table th,
|
||||||
|
.bnb-calendar-table td {
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table th {
|
||||||
|
background: #f6f7f7;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-table th.room-header {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Day Cell */
|
||||||
|
.bnb-calendar-day {
|
||||||
|
height: 35px;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.past {
|
||||||
|
background: #f0f0f1;
|
||||||
|
color: #a7aaad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.today {
|
||||||
|
background: #f0f6fc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.today::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: #2271b1;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.available {
|
||||||
|
background: #d4edda;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked {
|
||||||
|
background: #d63638;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked.booking-hover {
|
||||||
|
background: #a02424;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked-start {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.booked-end {
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Status Colors in Calendar */
|
||||||
|
.bnb-calendar-day.status-pending {
|
||||||
|
background: #dba617;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.status-confirmed {
|
||||||
|
background: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-day.status-checked_in {
|
||||||
|
background: #72aee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Room Row in Multi-Room Calendar */
|
||||||
|
.bnb-calendar-room {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-room small {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Legend */
|
||||||
|
.bnb-calendar-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid #c3c4c7;
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.available {
|
||||||
|
background: #d4edda;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.booked {
|
||||||
|
background: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.pending {
|
||||||
|
background: #dba617;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.confirmed {
|
||||||
|
background: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-legend-color.checked-in {
|
||||||
|
background: #72aee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip for booking details */
|
||||||
|
.bnb-calendar-day[title] {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Rooms Message */
|
||||||
|
.bnb-no-rooms {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-no-rooms .dashicons {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Single Room View */
|
||||||
|
.bnb-calendar-single-room .bnb-calendar-day {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-single-room .bnb-calendar-day.booked .guest-name {
|
||||||
|
font-size: 10px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.bnb-calendar-filters {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-filters select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-calendar-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -277,6 +277,484 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize booking form functionality.
|
||||||
|
*/
|
||||||
|
function initBookingForm() {
|
||||||
|
var $roomSelect = $('#bnb_booking_room_id');
|
||||||
|
var $checkInInput = $('#bnb_booking_check_in');
|
||||||
|
var $checkOutInput = $('#bnb_booking_check_out');
|
||||||
|
var $nightsDisplay = $('#bnb-booking-nights-display');
|
||||||
|
var $availabilityDisplay = $('#bnb-booking-availability-display');
|
||||||
|
var $priceDisplay = $('#bnb-booking-price-display');
|
||||||
|
var $calculatedPriceInput = $('#bnb_booking_calculated_price');
|
||||||
|
var $priceBreakdownInput = $('#bnb_booking_price_breakdown');
|
||||||
|
var $breakdownDisplay = $('#bnb-booking-breakdown-display');
|
||||||
|
var $recalculateBtn = $('#bnb-recalculate-price');
|
||||||
|
var $statusSelect = $('#bnb_booking_status');
|
||||||
|
var $statusPreview = $('#bnb-status-preview .bnb-status-badge');
|
||||||
|
|
||||||
|
// Check if we're on a booking edit page.
|
||||||
|
if (!$roomSelect.length || !$checkInInput.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current booking ID if editing.
|
||||||
|
var bookingId = null;
|
||||||
|
var $postId = $('input[name="post_ID"]');
|
||||||
|
if ($postId.length) {
|
||||||
|
bookingId = parseInt($postId.val(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce timer for availability check.
|
||||||
|
var availabilityTimer = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update nights display based on selected dates.
|
||||||
|
*/
|
||||||
|
function updateNightsDisplay() {
|
||||||
|
var checkIn = $checkInInput.val();
|
||||||
|
var checkOut = $checkOutInput.val();
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
var startDate = new Date(checkIn);
|
||||||
|
var endDate = new Date(checkOut);
|
||||||
|
var nights = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (nights > 0) {
|
||||||
|
var nightText = nights === 1 ? wpBnbAdmin.i18n.night : wpBnbAdmin.i18n.nights;
|
||||||
|
$nightsDisplay.text(nights + ' ' + nightText);
|
||||||
|
} else {
|
||||||
|
$nightsDisplay.text(wpBnbAdmin.i18n.error || 'Invalid date range');
|
||||||
|
$nightsDisplay.css('color', '#d63638');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$nightsDisplay.text(wpBnbAdmin.i18n.selectRoomAndDates);
|
||||||
|
$nightsDisplay.css('color', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check availability via AJAX.
|
||||||
|
*/
|
||||||
|
function checkAvailability() {
|
||||||
|
var roomId = $roomSelect.val();
|
||||||
|
var checkIn = $checkInInput.val();
|
||||||
|
var checkOut = $checkOutInput.val();
|
||||||
|
|
||||||
|
if (!roomId || !checkIn || !checkOut) {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.selectRoomAndDates)
|
||||||
|
.removeClass('bnb-available bnb-not-available')
|
||||||
|
.addClass('bnb-checking');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
var startDate = new Date(checkIn);
|
||||||
|
var endDate = new Date(checkOut);
|
||||||
|
if (endDate <= startDate) {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.error || 'Check-out must be after check-in')
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show checking status.
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.checking)
|
||||||
|
.removeClass('bnb-available bnb-not-available')
|
||||||
|
.addClass('bnb-checking');
|
||||||
|
|
||||||
|
// Make AJAX request.
|
||||||
|
$.ajax({
|
||||||
|
url: wpBnbAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_check_availability',
|
||||||
|
nonce: wpBnbAdmin.nonce,
|
||||||
|
room_id: roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut,
|
||||||
|
exclude_booking: bookingId
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
|
||||||
|
if (data.available) {
|
||||||
|
$availabilityDisplay
|
||||||
|
.html('<span class="dashicons dashicons-yes-alt"></span> ' + wpBnbAdmin.i18n.available)
|
||||||
|
.removeClass('bnb-not-available bnb-checking')
|
||||||
|
.addClass('bnb-available');
|
||||||
|
|
||||||
|
// Update price display.
|
||||||
|
if (data.price_formatted) {
|
||||||
|
$priceDisplay.html('<strong>' + data.price_formatted + '</strong>');
|
||||||
|
$calculatedPriceInput.val(data.price);
|
||||||
|
|
||||||
|
if (data.breakdown) {
|
||||||
|
$priceBreakdownInput.val(JSON.stringify(data.breakdown));
|
||||||
|
updateBreakdownDisplay(data.breakdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var conflictText = wpBnbAdmin.i18n.notAvailable;
|
||||||
|
if (data.conflicts && data.conflicts.length > 0) {
|
||||||
|
conflictText += ' (' + data.conflicts[0].reference + ')';
|
||||||
|
}
|
||||||
|
$availabilityDisplay
|
||||||
|
.html('<span class="dashicons dashicons-dismiss"></span> ' + conflictText)
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update nights display with response data.
|
||||||
|
if (data.nights) {
|
||||||
|
var nightText = data.nights === 1 ? wpBnbAdmin.i18n.night : wpBnbAdmin.i18n.nights;
|
||||||
|
$nightsDisplay.text(data.nights + ' ' + nightText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(response.data.message || wpBnbAdmin.i18n.error)
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$availabilityDisplay
|
||||||
|
.text(wpBnbAdmin.i18n.error)
|
||||||
|
.removeClass('bnb-available bnb-checking')
|
||||||
|
.addClass('bnb-not-available');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update breakdown display with formatted data.
|
||||||
|
*
|
||||||
|
* @param {Object} breakdown Price breakdown data.
|
||||||
|
*/
|
||||||
|
function updateBreakdownDisplay(breakdown) {
|
||||||
|
if (!$breakdownDisplay.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<ul class="bnb-breakdown-list">';
|
||||||
|
|
||||||
|
if (breakdown.tier) {
|
||||||
|
html += '<li><strong>Pricing Tier:</strong> ' + breakdown.tier.replace('_', ' ') + '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakdown.nights && Array.isArray(breakdown.nights)) {
|
||||||
|
html += '<li><strong>Nights:</strong> ' + breakdown.nights.length + '</li>';
|
||||||
|
if (breakdown.nightly_rate) {
|
||||||
|
html += '<li><strong>Nightly Rate:</strong> ' + formatPrice(breakdown.nightly_rate) + '</li>';
|
||||||
|
}
|
||||||
|
} else if (breakdown.weeks) {
|
||||||
|
html += '<li><strong>Weeks:</strong> ' + breakdown.weeks + '</li>';
|
||||||
|
if (breakdown.weekly_rate) {
|
||||||
|
html += '<li><strong>Weekly Rate:</strong> ' + formatPrice(breakdown.weekly_rate) + '</li>';
|
||||||
|
}
|
||||||
|
} else if (breakdown.months) {
|
||||||
|
html += '<li><strong>Months:</strong> ' + breakdown.months + '</li>';
|
||||||
|
if (breakdown.monthly_rate) {
|
||||||
|
html += '<li><strong>Monthly Rate:</strong> ' + formatPrice(breakdown.monthly_rate) + '</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakdown.total) {
|
||||||
|
html += '<li><strong>Total:</strong> ' + formatPrice(breakdown.total) + '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</ul>';
|
||||||
|
|
||||||
|
$breakdownDisplay.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price for display.
|
||||||
|
*
|
||||||
|
* @param {number} price Price value.
|
||||||
|
* @return {string} Formatted price.
|
||||||
|
*/
|
||||||
|
function formatPrice(price) {
|
||||||
|
// Simple formatting - server-side Calculator::formatPrice is more complete.
|
||||||
|
return parseFloat(price).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced availability check.
|
||||||
|
*/
|
||||||
|
function debouncedAvailabilityCheck() {
|
||||||
|
updateNightsDisplay();
|
||||||
|
|
||||||
|
// Clear existing timer.
|
||||||
|
if (availabilityTimer) {
|
||||||
|
clearTimeout(availabilityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timer to check availability after 500ms.
|
||||||
|
availabilityTimer = setTimeout(checkAvailability, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind change events to trigger availability check.
|
||||||
|
$roomSelect.on('change', debouncedAvailabilityCheck);
|
||||||
|
$checkInInput.on('change', debouncedAvailabilityCheck);
|
||||||
|
$checkOutInput.on('change', debouncedAvailabilityCheck);
|
||||||
|
|
||||||
|
// Recalculate price button.
|
||||||
|
if ($recalculateBtn.length) {
|
||||||
|
$recalculateBtn.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
checkAvailability();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status preview update.
|
||||||
|
if ($statusSelect.length && $statusPreview.length) {
|
||||||
|
$statusSelect.on('change', function() {
|
||||||
|
var $selected = $(this).find('option:selected');
|
||||||
|
var color = $selected.data('color') || '#ccc';
|
||||||
|
var text = $selected.text();
|
||||||
|
|
||||||
|
$statusPreview
|
||||||
|
.css('background-color', color)
|
||||||
|
.text(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set min date for check-in to today.
|
||||||
|
var today = new Date().toISOString().split('T')[0];
|
||||||
|
$checkInInput.attr('min', today);
|
||||||
|
|
||||||
|
// Update check-out min date when check-in changes.
|
||||||
|
$checkInInput.on('change', function() {
|
||||||
|
var checkIn = $(this).val();
|
||||||
|
if (checkIn) {
|
||||||
|
var nextDay = new Date(checkIn);
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
|
var minCheckOut = nextDay.toISOString().split('T')[0];
|
||||||
|
$checkOutInput.attr('min', minCheckOut);
|
||||||
|
|
||||||
|
// If check-out is before new min, update it.
|
||||||
|
if ($checkOutInput.val() && $checkOutInput.val() <= checkIn) {
|
||||||
|
$checkOutInput.val(minCheckOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize calendar page functionality.
|
||||||
|
*/
|
||||||
|
function initCalendarPage() {
|
||||||
|
var $calendar = $('.bnb-calendar-grid');
|
||||||
|
|
||||||
|
if (!$calendar.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hover effect for booking cells.
|
||||||
|
$calendar.on('mouseenter', '.bnb-calendar-day.booked', function() {
|
||||||
|
var bookingId = $(this).data('booking-id');
|
||||||
|
if (bookingId) {
|
||||||
|
$calendar.find('.bnb-calendar-day[data-booking-id="' + bookingId + '"]')
|
||||||
|
.addClass('booking-hover');
|
||||||
|
}
|
||||||
|
}).on('mouseleave', '.bnb-calendar-day.booked', function() {
|
||||||
|
$calendar.find('.bnb-calendar-day.booking-hover')
|
||||||
|
.removeClass('booking-hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click to edit booking.
|
||||||
|
$calendar.on('click', '.bnb-calendar-day.booked', function() {
|
||||||
|
var bookingId = $(this).data('booking-id');
|
||||||
|
if (bookingId) {
|
||||||
|
window.location.href = wpBnbAdmin.ajaxUrl.replace('admin-ajax.php', 'post.php?post=' + bookingId + '&action=edit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize 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 on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
@@ -284,6 +762,9 @@
|
|||||||
initPricingSettings();
|
initPricingSettings();
|
||||||
initSeasonForm();
|
initSeasonForm();
|
||||||
initPricingMetaBox();
|
initPricingMetaBox();
|
||||||
|
initBookingForm();
|
||||||
|
initCalendarPage();
|
||||||
|
initGuestSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
359
src/Admin/Calendar.php
Normal file
359
src/Admin/Calendar.php
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Calendar admin page.
|
||||||
|
*
|
||||||
|
* Displays availability calendar for rooms and buildings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Admin;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar admin page class.
|
||||||
|
*/
|
||||||
|
final class Calendar {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the calendar page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'admin_menu', array( self::class, 'register_menu' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the admin menu item.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_menu(): void {
|
||||||
|
add_submenu_page(
|
||||||
|
'wp-bnb',
|
||||||
|
__( 'Calendar', 'wp-bnb' ),
|
||||||
|
__( 'Calendar', 'wp-bnb' ),
|
||||||
|
'edit_posts',
|
||||||
|
'wp-bnb-calendar',
|
||||||
|
array( self::class, 'render_page' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the calendar page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_page(): void {
|
||||||
|
// Get filter parameters.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$building_id = isset( $_GET['building_id'] ) ? absint( $_GET['building_id'] ) : 0;
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$room_id = isset( $_GET['room_id'] ) ? absint( $_GET['room_id'] ) : 0;
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$year = isset( $_GET['year'] ) ? absint( $_GET['year'] ) : (int) gmdate( 'Y' );
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page.
|
||||||
|
$month = isset( $_GET['month'] ) ? absint( $_GET['month'] ) : (int) gmdate( 'n' );
|
||||||
|
|
||||||
|
// Validate month.
|
||||||
|
if ( $month < 1 || $month > 12 ) {
|
||||||
|
$month = (int) gmdate( 'n' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get buildings and rooms for filters.
|
||||||
|
$buildings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Building::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If room is selected, get its building.
|
||||||
|
if ( $room_id && ! $building_id ) {
|
||||||
|
$building = Room::get_building( $room_id );
|
||||||
|
if ( $building ) {
|
||||||
|
$building_id = $building->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rooms to display.
|
||||||
|
$display_rooms = array();
|
||||||
|
if ( $room_id ) {
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( $room ) {
|
||||||
|
$display_rooms[] = $room;
|
||||||
|
}
|
||||||
|
} elseif ( $building_id ) {
|
||||||
|
$display_rooms = Room::get_rooms_for_building( $building_id );
|
||||||
|
} else {
|
||||||
|
$display_rooms = $rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate navigation dates.
|
||||||
|
$prev_month = $month === 1 ? 12 : $month - 1;
|
||||||
|
$prev_year = $month === 1 ? $year - 1 : $year;
|
||||||
|
$next_month = $month === 12 ? 1 : $month + 1;
|
||||||
|
$next_year = $month === 12 ? $year + 1 : $year;
|
||||||
|
|
||||||
|
$month_name = gmdate( 'F', mktime( 0, 0, 0, $month, 1, $year ) );
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'Availability Calendar', 'wp-bnb' ); ?></h1>
|
||||||
|
|
||||||
|
<div class="bnb-calendar-container">
|
||||||
|
<!-- Calendar Header -->
|
||||||
|
<div class="bnb-calendar-header">
|
||||||
|
<div class="bnb-calendar-nav">
|
||||||
|
<a href="<?php echo esc_url( self::get_calendar_url( $prev_year, $prev_month, $building_id, $room_id ) ); ?>"
|
||||||
|
class="button">
|
||||||
|
« <?php esc_html_e( 'Previous', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( self::get_calendar_url( (int) gmdate( 'Y' ), (int) gmdate( 'n' ), $building_id, $room_id ) ); ?>"
|
||||||
|
class="button">
|
||||||
|
<?php esc_html_e( 'Today', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( self::get_calendar_url( $next_year, $next_month, $building_id, $room_id ) ); ?>"
|
||||||
|
class="button">
|
||||||
|
<?php esc_html_e( 'Next', 'wp-bnb' ); ?> »
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h2><?php echo esc_html( $month_name . ' ' . $year ); ?></h2>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="bnb-calendar-filters">
|
||||||
|
<form method="get" action="">
|
||||||
|
<input type="hidden" name="page" value="wp-bnb-calendar">
|
||||||
|
<input type="hidden" name="year" value="<?php echo esc_attr( $year ); ?>">
|
||||||
|
<input type="hidden" name="month" value="<?php echo esc_attr( $month ); ?>">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php esc_html_e( 'Building:', 'wp-bnb' ); ?>
|
||||||
|
<select name="building_id" onchange="this.form.submit()">
|
||||||
|
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $buildings as $building ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $building_id, $building->ID ); ?>>
|
||||||
|
<?php echo esc_html( $building->post_title ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<?php esc_html_e( 'Room:', 'wp-bnb' ); ?>
|
||||||
|
<select name="room_id" onchange="this.form.submit()">
|
||||||
|
<option value=""><?php esc_html_e( 'All Rooms', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $rooms as $room ) : ?>
|
||||||
|
<?php
|
||||||
|
$room_building = Room::get_building( $room->ID );
|
||||||
|
$room_label = $room->post_title;
|
||||||
|
if ( $room_building ) {
|
||||||
|
$room_label .= ' (' . $room_building->post_title . ')';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
||||||
|
<?php echo esc_html( $room_label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar Grid -->
|
||||||
|
<div class="bnb-calendar-grid">
|
||||||
|
<?php if ( empty( $display_rooms ) ) : ?>
|
||||||
|
<div class="bnb-no-rooms">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<p><?php esc_html_e( 'No rooms found. Please add rooms first.', 'wp-bnb' ); ?></p>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Room::POST_TYPE ) ); ?>" class="button button-primary">
|
||||||
|
<?php esc_html_e( 'Add Room', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php self::render_calendar_table( $display_rooms, $year, $month, (bool) $room_id ); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="bnb-calendar-legend">
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color available"></span>
|
||||||
|
<?php esc_html_e( 'Available', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color pending"></span>
|
||||||
|
<?php esc_html_e( 'Pending', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color confirmed"></span>
|
||||||
|
<?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="bnb-calendar-legend-item">
|
||||||
|
<span class="bnb-calendar-legend-color checked-in"></span>
|
||||||
|
<?php esc_html_e( 'Checked In', 'wp-bnb' ); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the calendar table.
|
||||||
|
*
|
||||||
|
* @param array $rooms Rooms to display.
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month.
|
||||||
|
* @param bool $single_room Whether showing single room (more detail).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_calendar_table( array $rooms, int $year, int $month, bool $single_room = false ): void {
|
||||||
|
$days_in_month = (int) gmdate( 't', mktime( 0, 0, 0, $month, 1, $year ) );
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
$class = $single_room ? 'bnb-calendar-table bnb-calendar-single-room' : 'bnb-calendar-table';
|
||||||
|
?>
|
||||||
|
<table class="<?php echo esc_attr( $class ); ?>">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="room-header"><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
|
||||||
|
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||||
|
$day_of_week = gmdate( 'D', strtotime( $date_str ) );
|
||||||
|
$is_weekend = in_array( gmdate( 'N', strtotime( $date_str ) ), array( 6, 7 ), true );
|
||||||
|
?>
|
||||||
|
<th class="<?php echo $is_weekend ? 'weekend' : ''; ?>">
|
||||||
|
<?php echo esc_html( $day ); ?><br>
|
||||||
|
<small><?php echo esc_html( $day_of_week ); ?></small>
|
||||||
|
</th>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $rooms as $room ) : ?>
|
||||||
|
<?php
|
||||||
|
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||||
|
$booked_dates = Availability::get_booked_dates( $room->ID, $year, $month );
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="bnb-calendar-room">
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $room->ID ) ); ?>">
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( $room_number ) : ?>
|
||||||
|
<br><small>#<?php echo esc_html( $room_number ); ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
||||||
|
<?php
|
||||||
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||||
|
$is_past = $date_str < $today;
|
||||||
|
$is_today = $date_str === $today;
|
||||||
|
$is_booked = isset( $booked_dates[ $date_str ] );
|
||||||
|
|
||||||
|
$classes = array( 'bnb-calendar-day' );
|
||||||
|
$title = '';
|
||||||
|
$booking_id = 0;
|
||||||
|
|
||||||
|
if ( $is_past ) {
|
||||||
|
$classes[] = 'past';
|
||||||
|
}
|
||||||
|
if ( $is_today ) {
|
||||||
|
$classes[] = 'today';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $is_booked ) {
|
||||||
|
$booking_data = $booked_dates[ $date_str ];
|
||||||
|
$booking_id = $booking_data['booking_id'];
|
||||||
|
$classes[] = 'booked';
|
||||||
|
$classes[] = 'status-' . $booking_data['status'];
|
||||||
|
|
||||||
|
if ( $booking_data['is_start'] ) {
|
||||||
|
$classes[] = 'booked-start';
|
||||||
|
}
|
||||||
|
if ( $booking_data['is_end'] ) {
|
||||||
|
$classes[] = 'booked-end';
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = sprintf(
|
||||||
|
/* translators: 1: Booking reference, 2: Guest name, 3: Check-in date, 4: Check-out date */
|
||||||
|
__( '%1$s - %2$s (%3$s to %4$s)', 'wp-bnb' ),
|
||||||
|
$booking_data['reference'],
|
||||||
|
$booking_data['guest'],
|
||||||
|
wp_date( get_option( 'date_format' ), strtotime( $booking_data['check_in'] ) ),
|
||||||
|
wp_date( get_option( 'date_format' ), strtotime( $booking_data['check_out'] ) )
|
||||||
|
);
|
||||||
|
} elseif ( ! $is_past ) {
|
||||||
|
$classes[] = 'available';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<td class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
|
||||||
|
<?php if ( $booking_id ) : ?>
|
||||||
|
data-booking-id="<?php echo esc_attr( $booking_id ); ?>"
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $title ) : ?>
|
||||||
|
title="<?php echo esc_attr( $title ); ?>"
|
||||||
|
<?php endif; ?>>
|
||||||
|
<?php if ( $single_room && $is_booked && $booking_data['is_start'] ) : ?>
|
||||||
|
<span class="guest-name"><?php echo esc_html( $booking_data['guest'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar URL with parameters.
|
||||||
|
*
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month.
|
||||||
|
* @param int $building_id Building ID (optional).
|
||||||
|
* @param int $room_id Room ID (optional).
|
||||||
|
* @return string URL.
|
||||||
|
*/
|
||||||
|
private static function get_calendar_url( int $year, int $month, int $building_id = 0, int $room_id = 0 ): string {
|
||||||
|
$args = array(
|
||||||
|
'page' => 'wp-bnb-calendar',
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $building_id ) {
|
||||||
|
$args['building_id'] = $building_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $room_id ) {
|
||||||
|
$args['room_id'] = $room_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return add_query_arg( $args, admin_url( 'admin.php' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
444
src/Booking/Availability.php
Normal file
444
src/Booking/Availability.php
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Availability checker.
|
||||||
|
*
|
||||||
|
* Handles availability checks and calendar data for rooms and bookings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Booking
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Booking;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability class.
|
||||||
|
*/
|
||||||
|
final class Availability {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a room is available for a date range.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int|null $exclude_booking Booking ID to exclude (for editing).
|
||||||
|
* @return bool True if available, false if conflicts exist.
|
||||||
|
*/
|
||||||
|
public static function is_available( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): bool {
|
||||||
|
return ! Booking::has_conflict( $room_id, $check_in, $check_out, $exclude_booking );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all booked dates for a room in a specific month.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $year Year (e.g., 2024).
|
||||||
|
* @param int $month Month (1-12).
|
||||||
|
* @return array<string, array> Array of dates (Y-m-d) with booking info.
|
||||||
|
*/
|
||||||
|
public static function get_booked_dates( int $room_id, int $year, int $month ): array {
|
||||||
|
$month_start = sprintf( '%04d-%02d-01', $year, $month );
|
||||||
|
$month_end = gmdate( 'Y-m-t', strtotime( $month_start ) );
|
||||||
|
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'cancelled',
|
||||||
|
'compare' => '!=',
|
||||||
|
),
|
||||||
|
// Booking overlaps with month.
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $month_end,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $month_start,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$booked_dates = array();
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
|
$guest = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
|
||||||
|
// Iterate through each night of the booking.
|
||||||
|
$current = new \DateTimeImmutable( $check_in );
|
||||||
|
$end = new \DateTimeImmutable( $check_out );
|
||||||
|
|
||||||
|
while ( $current < $end ) {
|
||||||
|
$date_str = $current->format( 'Y-m-d' );
|
||||||
|
|
||||||
|
// Only include dates within the requested month.
|
||||||
|
if ( $current->format( 'Y-m' ) === sprintf( '%04d-%02d', $year, $month ) ) {
|
||||||
|
$booked_dates[ $date_str ] = array(
|
||||||
|
'booking_id' => $booking->ID,
|
||||||
|
'reference' => $booking->post_title,
|
||||||
|
'guest' => $guest,
|
||||||
|
'status' => $status,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'is_start' => $date_str === $check_in,
|
||||||
|
'is_end' => $current->modify( '+1 day' )->format( 'Y-m-d' ) === $check_out,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = $current->modify( '+1 day' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $booked_dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar data for a room for a specific month.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month (1-12).
|
||||||
|
* @return array Calendar data including days and bookings.
|
||||||
|
*/
|
||||||
|
public static function get_calendar_data( int $room_id, int $year, int $month ): array {
|
||||||
|
$month_start = new \DateTimeImmutable( sprintf( '%04d-%02d-01', $year, $month ) );
|
||||||
|
$days_in_month = (int) $month_start->format( 't' );
|
||||||
|
$first_day_of_week = (int) $month_start->format( 'w' ); // 0 = Sunday.
|
||||||
|
|
||||||
|
$booked_dates = self::get_booked_dates( $room_id, $year, $month );
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
$days = array();
|
||||||
|
for ( $day = 1; $day <= $days_in_month; $day++ ) {
|
||||||
|
$date_str = sprintf( '%04d-%02d-%02d', $year, $month, $day );
|
||||||
|
$is_booked = isset( $booked_dates[ $date_str ] );
|
||||||
|
|
||||||
|
$days[ $day ] = array(
|
||||||
|
'date' => $date_str,
|
||||||
|
'day' => $day,
|
||||||
|
'is_booked' => $is_booked,
|
||||||
|
'is_past' => $date_str < $today,
|
||||||
|
'is_today' => $date_str === $today,
|
||||||
|
'booking' => $is_booked ? $booked_dates[ $date_str ] : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'month_name' => $month_start->format( 'F' ),
|
||||||
|
'days_in_month' => $days_in_month,
|
||||||
|
'first_day_of_week' => $first_day_of_week,
|
||||||
|
'days' => $days,
|
||||||
|
'prev_month' => array(
|
||||||
|
'year' => $month === 1 ? $year - 1 : $year,
|
||||||
|
'month' => $month === 1 ? 12 : $month - 1,
|
||||||
|
),
|
||||||
|
'next_month' => array(
|
||||||
|
'year' => $month === 12 ? $year + 1 : $year,
|
||||||
|
'month' => $month === 12 ? 1 : $month + 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get availability summary for a building (all rooms).
|
||||||
|
*
|
||||||
|
* @param int $building_id Building post ID.
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month (1-12).
|
||||||
|
* @return array Availability data for all rooms in the building.
|
||||||
|
*/
|
||||||
|
public static function get_building_availability( int $building_id, int $year, int $month ): array {
|
||||||
|
$rooms = Room::get_rooms_for_building( $building_id );
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||||
|
$data[ $room->ID ] = array(
|
||||||
|
'room_id' => $room->ID,
|
||||||
|
'room_name' => $room->post_title,
|
||||||
|
'room_number' => $room_number,
|
||||||
|
'calendar' => self::get_calendar_data( $room->ID, $year, $month ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conflicts for a proposed booking.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int|null $exclude_booking Booking ID to exclude.
|
||||||
|
* @return array<\WP_Post> Array of conflicting booking posts.
|
||||||
|
*/
|
||||||
|
public static function get_conflicts( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): array {
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'cancelled',
|
||||||
|
'compare' => '!=',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $check_out,
|
||||||
|
'compare' => '<',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $check_in,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $exclude_booking ) {
|
||||||
|
$args['post__not_in'] = array( $exclude_booking );
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_posts( $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get availability check result with pricing.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int|null $exclude_booking Booking ID to exclude.
|
||||||
|
* @return array Result with availability, pricing, and conflicts.
|
||||||
|
*/
|
||||||
|
public static function check_availability_with_price( int $room_id, string $check_in, string $check_out, ?int $exclude_booking = null ): array {
|
||||||
|
$conflicts = self::get_conflicts( $room_id, $check_in, $check_out, $exclude_booking );
|
||||||
|
$available = empty( $conflicts );
|
||||||
|
|
||||||
|
$result = array(
|
||||||
|
'available' => $available,
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => Booking::calculate_nights( $check_in, $check_out ),
|
||||||
|
'conflicts' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $available ) {
|
||||||
|
foreach ( $conflicts as $conflict ) {
|
||||||
|
$result['conflicts'][] = array(
|
||||||
|
'booking_id' => $conflict->ID,
|
||||||
|
'reference' => $conflict->post_title,
|
||||||
|
'check_in' => get_post_meta( $conflict->ID, '_bnb_booking_check_in', true ),
|
||||||
|
'check_out' => get_post_meta( $conflict->ID, '_bnb_booking_check_out', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate price if available.
|
||||||
|
if ( $available ) {
|
||||||
|
try {
|
||||||
|
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||||
|
$price = $calculator->calculate();
|
||||||
|
$breakdown = $calculator->getBreakdown();
|
||||||
|
|
||||||
|
$result['price'] = $price;
|
||||||
|
$result['price_formatted'] = Calculator::formatPrice( $price );
|
||||||
|
$result['breakdown'] = $breakdown;
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$result['price'] = null;
|
||||||
|
$result['price_formatted'] = null;
|
||||||
|
$result['price_error'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upcoming bookings for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $limit Maximum number of bookings to return.
|
||||||
|
* @return array<\WP_Post> Array of upcoming bookings.
|
||||||
|
*/
|
||||||
|
public static function get_upcoming_bookings( int $room_id, int $limit = 5 ): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $limit,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'pending', 'confirmed' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_check_in',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current bookings (guests currently checked in).
|
||||||
|
*
|
||||||
|
* @param int|null $room_id Optional room ID to filter by.
|
||||||
|
* @return array<\WP_Post> Array of current bookings.
|
||||||
|
*/
|
||||||
|
public static function get_current_bookings( ?int $room_id = null ): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
$meta_query = array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'checked_in',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $room_id ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => $meta_query,
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_check_out',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's check-ins.
|
||||||
|
*
|
||||||
|
* @return array<\WP_Post> Array of bookings with check-in today.
|
||||||
|
*/
|
||||||
|
public static function get_todays_checkins(): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'confirmed',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_guest_name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's check-outs.
|
||||||
|
*
|
||||||
|
* @return array<\WP_Post> Array of bookings with check-out today.
|
||||||
|
*/
|
||||||
|
public static function get_todays_checkouts(): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'checked_in',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $today,
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_guest_name',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
641
src/Booking/EmailNotifier.php
Normal file
641
src/Booking/EmailNotifier.php
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Email notifier for bookings.
|
||||||
|
*
|
||||||
|
* Handles sending email notifications for booking events.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Booking
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Booking;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailNotifier class.
|
||||||
|
*/
|
||||||
|
final class EmailNotifier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the email notifier.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
add_action( 'wp_bnb_booking_status_changed', array( self::class, 'on_status_change' ), 10, 3 );
|
||||||
|
add_action( 'save_post_' . Booking::POST_TYPE, array( self::class, 'on_booking_created' ), 20, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle status change event.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @param string $old_status Previous status.
|
||||||
|
* @param string $new_status New status.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_status_change( int $booking_id, string $old_status, string $new_status ): void {
|
||||||
|
switch ( $new_status ) {
|
||||||
|
case 'confirmed':
|
||||||
|
self::send_guest_confirmation( $booking_id );
|
||||||
|
self::send_admin_confirmation( $booking_id );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancelled':
|
||||||
|
self::send_cancellation( $booking_id );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle booking created event.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_booking_created( int $post_id, \WP_Post $post ): void {
|
||||||
|
// Skip if autosave or revision.
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( wp_is_post_revision( $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a new booking (created in the last 30 seconds).
|
||||||
|
$created = get_post_time( 'U', true, $post_id );
|
||||||
|
if ( time() - $created > 30 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've already sent this notification.
|
||||||
|
$sent = get_post_meta( $post_id, '_bnb_booking_new_email_sent', true );
|
||||||
|
if ( $sent ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as sent before sending to prevent duplicates.
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_new_email_sent', '1' );
|
||||||
|
|
||||||
|
// Send admin notification for new booking.
|
||||||
|
self::send_admin_new_booking( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send new booking notification to admin.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
public static function send_admin_new_booking( int $booking_id ): bool {
|
||||||
|
$booking = get_post( $booking_id );
|
||||||
|
if ( ! $booking ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
$to = get_option( 'admin_email' );
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] New Booking: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = self::get_email_template( 'admin-new-booking', $data );
|
||||||
|
|
||||||
|
return self::send_email( $to, $subject, $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send confirmation email to guest.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
public static function send_guest_confirmation( int $booking_id ): bool {
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
|
||||||
|
if ( empty( $data['guest_email'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = self::get_email_template( 'booking-confirmed', $data );
|
||||||
|
|
||||||
|
return self::send_email( $data['guest_email'], $subject, $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send confirmation notification to admin.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
public static function send_admin_confirmation( int $booking_id ): bool {
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
$to = get_option( 'admin_email' );
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Confirmed: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = self::get_email_template( 'admin-booking-confirmed', $data );
|
||||||
|
|
||||||
|
return self::send_email( $to, $subject, $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send cancellation email.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return bool Whether emails were sent.
|
||||||
|
*/
|
||||||
|
public static function send_cancellation( int $booking_id ): bool {
|
||||||
|
$data = self::get_booking_data( $booking_id );
|
||||||
|
$result = true;
|
||||||
|
|
||||||
|
// Send to admin.
|
||||||
|
$admin_subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$admin_message = self::get_email_template( 'admin-booking-cancelled', $data );
|
||||||
|
$result = self::send_email( get_option( 'admin_email' ), $admin_subject, $admin_message ) && $result;
|
||||||
|
|
||||||
|
// Send to guest if email exists.
|
||||||
|
if ( ! empty( $data['guest_email'] ) ) {
|
||||||
|
$guest_subject = sprintf(
|
||||||
|
/* translators: 1: Site name, 2: Booking reference */
|
||||||
|
__( '[%1$s] Booking Cancelled: %2$s', 'wp-bnb' ),
|
||||||
|
get_bloginfo( 'name' ),
|
||||||
|
$data['booking_reference']
|
||||||
|
);
|
||||||
|
|
||||||
|
$guest_message = self::get_email_template( 'booking-cancelled', $data );
|
||||||
|
$result = self::send_email( $data['guest_email'], $guest_subject, $guest_message ) && $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get booking data for email templates.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return array Booking data.
|
||||||
|
*/
|
||||||
|
private static function get_booking_data( int $booking_id ): array {
|
||||||
|
$booking = get_post( $booking_id );
|
||||||
|
$room = Booking::get_room( $booking_id );
|
||||||
|
$building = Booking::get_building( $booking_id );
|
||||||
|
|
||||||
|
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
$price = get_post_meta( $booking_id, '_bnb_booking_calculated_price', true );
|
||||||
|
$adults = get_post_meta( $booking_id, '_bnb_booking_adults', true );
|
||||||
|
$children = get_post_meta( $booking_id, '_bnb_booking_children', true );
|
||||||
|
|
||||||
|
$nights = 0;
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$nights = Booking::calculate_nights( $check_in, $check_out );
|
||||||
|
}
|
||||||
|
|
||||||
|
$statuses = Booking::get_booking_statuses();
|
||||||
|
|
||||||
|
// Get guest data - prefer Guest CPT if linked, fallback to booking meta.
|
||||||
|
$guest_data = self::get_guest_data( $booking_id );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'booking_id' => $booking_id,
|
||||||
|
'booking_reference' => $booking ? $booking->post_title : '',
|
||||||
|
'guest_name' => $guest_data['name'],
|
||||||
|
'guest_first_name' => $guest_data['first_name'],
|
||||||
|
'guest_last_name' => $guest_data['last_name'],
|
||||||
|
'guest_email' => $guest_data['email'],
|
||||||
|
'guest_phone' => $guest_data['phone'],
|
||||||
|
'guest_notes' => $guest_data['notes'],
|
||||||
|
'guest_full_address' => $guest_data['full_address'],
|
||||||
|
'adults' => $adults ?: 1,
|
||||||
|
'children' => $children ?: 0,
|
||||||
|
'room_name' => $room ? $room->post_title : '',
|
||||||
|
'room_id' => $room ? $room->ID : 0,
|
||||||
|
'building_name' => $building ? $building->post_title : '',
|
||||||
|
'building_id' => $building ? $building->ID : 0,
|
||||||
|
'check_in_date' => $check_in ? wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) : '',
|
||||||
|
'check_out_date' => $check_out ? wp_date( get_option( 'date_format' ), strtotime( $check_out ) ) : '',
|
||||||
|
'check_in_raw' => $check_in,
|
||||||
|
'check_out_raw' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
'total_price' => $price ? Calculator::formatPrice( (float) $price ) : '',
|
||||||
|
'status' => $statuses[ $status ] ?? $status,
|
||||||
|
'status_raw' => $status,
|
||||||
|
'site_name' => get_bloginfo( 'name' ),
|
||||||
|
'site_url' => home_url(),
|
||||||
|
'admin_email' => get_option( 'admin_email' ),
|
||||||
|
'booking_url' => admin_url( 'post.php?post=' . $booking_id . '&action=edit' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get 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.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email template with placeholders replaced.
|
||||||
|
*
|
||||||
|
* @param string $template_name Template name.
|
||||||
|
* @param array $data Template data.
|
||||||
|
* @return string HTML email content.
|
||||||
|
*/
|
||||||
|
private static function get_email_template( string $template_name, array $data ): string {
|
||||||
|
$template = self::get_template_content( $template_name );
|
||||||
|
|
||||||
|
// Replace placeholders.
|
||||||
|
foreach ( $data as $key => $value ) {
|
||||||
|
$template = str_replace( '{' . $key . '}', (string) $value, $template );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template content.
|
||||||
|
*
|
||||||
|
* @param string $template_name Template name.
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function get_template_content( string $template_name ): string {
|
||||||
|
// Built-in templates. Could be extended to load from files.
|
||||||
|
$templates = array(
|
||||||
|
'admin-new-booking' => self::template_admin_new_booking(),
|
||||||
|
'booking-confirmed' => self::template_booking_confirmed(),
|
||||||
|
'admin-booking-confirmed' => self::template_admin_booking_confirmed(),
|
||||||
|
'booking-cancelled' => self::template_booking_cancelled(),
|
||||||
|
'admin-booking-cancelled' => self::template_admin_booking_cancelled(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $templates[ $template_name ] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send HTML email.
|
||||||
|
*
|
||||||
|
* @param string $to Recipient email.
|
||||||
|
* @param string $subject Email subject.
|
||||||
|
* @param string $message HTML message.
|
||||||
|
* @return bool Whether email was sent.
|
||||||
|
*/
|
||||||
|
private static function send_email( string $to, string $subject, string $message ): bool {
|
||||||
|
$headers = array(
|
||||||
|
'Content-Type: text/html; charset=UTF-8',
|
||||||
|
'From: ' . get_bloginfo( 'name' ) . ' <' . get_option( 'admin_email' ) . '>',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter email recipients.
|
||||||
|
*
|
||||||
|
* @param string $to Recipient email.
|
||||||
|
* @param string $subject Email subject.
|
||||||
|
* @param string $message Email message.
|
||||||
|
*/
|
||||||
|
$to = apply_filters( 'wp_bnb_booking_email_recipients', $to, $subject, $message );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter email subject.
|
||||||
|
*
|
||||||
|
* @param string $subject Email subject.
|
||||||
|
*/
|
||||||
|
$subject = apply_filters( 'wp_bnb_booking_email_subject', $subject );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter email content.
|
||||||
|
*
|
||||||
|
* @param string $message Email message.
|
||||||
|
*/
|
||||||
|
$message = apply_filters( 'wp_bnb_booking_email_content', $message );
|
||||||
|
|
||||||
|
return wp_mail( $to, $subject, $message, $headers );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base email styles.
|
||||||
|
*
|
||||||
|
* @return string CSS styles.
|
||||||
|
*/
|
||||||
|
private static function get_email_styles(): string {
|
||||||
|
return '
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 14px; line-height: 1.6; color: #333; }
|
||||||
|
.email-container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.email-header { background: #135e96; color: #fff; padding: 20px; text-align: center; }
|
||||||
|
.email-header h1 { margin: 0; font-size: 24px; }
|
||||||
|
.email-body { background: #fff; padding: 30px; border: 1px solid #ddd; }
|
||||||
|
.email-footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
|
||||||
|
.booking-details { background: #f9f9f9; padding: 15px; margin: 20px 0; border-left: 4px solid #135e96; }
|
||||||
|
.booking-details h3 { margin-top: 0; color: #135e96; }
|
||||||
|
.detail-row { margin: 8px 0; }
|
||||||
|
.detail-label { font-weight: 600; display: inline-block; min-width: 120px; }
|
||||||
|
.btn { display: inline-block; padding: 10px 20px; background: #135e96; color: #fff; text-decoration: none; border-radius: 4px; margin-top: 15px; }
|
||||||
|
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
||||||
|
.status-pending { background: #dba617; color: #fff; }
|
||||||
|
.status-confirmed { background: #00a32a; color: #fff; }
|
||||||
|
.status-cancelled { background: #d63638; color: #fff; }
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Admin new booking notification.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_admin_new_booking(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>New Booking Received</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>A new booking has been created and is awaiting confirmation.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Booking Details</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Status:</span> <span class="status-badge status-{status_raw}">{status}</span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Building:</span> {building_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Nights:</span> {nights}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Guest Information</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Name:</span> {guest_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Phone:</span> {guest_phone}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Adults:</span> {adults}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Children:</span> {children}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{booking_url}" class="btn">View Booking</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>This email was sent from {site_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Guest booking confirmed.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_booking_confirmed(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>Booking Confirmed</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Dear {guest_name},</p>
|
||||||
|
|
||||||
|
<p>Great news! Your booking has been confirmed. We look forward to welcoming you.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Your Booking Details</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Confirmation:</span> {booking_reference}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Location:</span> {building_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-in:</span> {check_in_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Check-out:</span> {check_out_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Duration:</span> {nights} nights</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Guests:</span> {adults} adults, {children} children</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Total:</span> <strong>{total_price}</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>If you have any questions or need to make changes to your reservation, please contact us at {admin_email}.</p>
|
||||||
|
|
||||||
|
<p>Thank you for choosing us!</p>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>{site_name}<br>{site_url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Admin booking confirmed.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_admin_booking_confirmed(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>Booking Confirmed</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Booking {booking_reference} has been confirmed.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Booking Summary</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Total:</span> {total_price}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{booking_url}" class="btn">View Booking</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>This email was sent from {site_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Guest booking cancelled.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_booking_cancelled(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header" style="background: #d63638;">
|
||||||
|
<h1>Booking Cancelled</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Dear {guest_name},</p>
|
||||||
|
|
||||||
|
<p>We're writing to confirm that your booking has been cancelled.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Cancelled Booking</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Reference:</span> {booking_reference}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>If you have any questions or would like to make a new reservation, please contact us at {admin_email}.</p>
|
||||||
|
|
||||||
|
<p>We hope to welcome you in the future.</p>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>{site_name}<br>{site_url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template: Admin booking cancelled.
|
||||||
|
*
|
||||||
|
* @return string Template HTML.
|
||||||
|
*/
|
||||||
|
private static function template_admin_booking_cancelled(): string {
|
||||||
|
$styles = self::get_email_styles();
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>{$styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header" style="background: #d63638;">
|
||||||
|
<h1>Booking Cancelled</h1>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Booking {booking_reference} has been cancelled.</p>
|
||||||
|
|
||||||
|
<div class="booking-details">
|
||||||
|
<h3>Cancelled Booking</h3>
|
||||||
|
<div class="detail-row"><span class="detail-label">Guest:</span> {guest_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Email:</span> {guest_email}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Room:</span> {room_name}</div>
|
||||||
|
<div class="detail-row"><span class="detail-label">Dates:</span> {check_in_date} - {check_out_date}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{booking_url}" class="btn">View Booking</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
<p>This email was sent from {site_name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
src/Plugin.php
151
src/Plugin.php
@@ -9,10 +9,16 @@ declare( strict_types=1 );
|
|||||||
|
|
||||||
namespace Magdev\WpBnb;
|
namespace Magdev\WpBnb;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
use Magdev\WpBnb\PostTypes\Building;
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
use Magdev\WpBnb\PostTypes\Room;
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
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;
|
||||||
@@ -87,6 +93,8 @@ final class Plugin {
|
|||||||
private function register_post_types(): void {
|
private function register_post_types(): void {
|
||||||
Building::init();
|
Building::init();
|
||||||
Room::init();
|
Room::init();
|
||||||
|
Booking::init();
|
||||||
|
Guest::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +140,19 @@ final class Plugin {
|
|||||||
|
|
||||||
// Initialize seasons admin page.
|
// Initialize seasons admin page.
|
||||||
SeasonsAdmin::init();
|
SeasonsAdmin::init();
|
||||||
|
|
||||||
|
// Initialize calendar admin page.
|
||||||
|
CalendarAdmin::init();
|
||||||
|
|
||||||
|
// Initialize email notifier.
|
||||||
|
EmailNotifier::init();
|
||||||
|
|
||||||
|
// Initialize privacy manager for GDPR compliance.
|
||||||
|
PrivacyManager::init();
|
||||||
|
|
||||||
|
// Register AJAX handlers.
|
||||||
|
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' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,7 +188,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 ), true );
|
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::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 ) ) {
|
||||||
@@ -205,15 +226,26 @@ 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' ),
|
||||||
|
'available' => __( 'Available', 'wp-bnb' ),
|
||||||
|
'notAvailable' => __( 'Not available - conflicts with existing booking', 'wp-bnb' ),
|
||||||
|
'selectRoomAndDates' => __( 'Select room and dates to check availability', 'wp-bnb' ),
|
||||||
|
'nights' => __( 'nights', 'wp-bnb' ),
|
||||||
|
'night' => __( 'night', 'wp-bnb' ),
|
||||||
|
'calculating' => __( 'Calculating price...', 'wp-bnb' ),
|
||||||
|
'searchingGuests' => __( 'Searching...', 'wp-bnb' ),
|
||||||
|
'noGuestsFound' => __( 'No guests found', 'wp-bnb' ),
|
||||||
|
'selectGuest' => __( 'Select', 'wp-bnb' ),
|
||||||
|
'guestBlocked' => __( 'Blocked', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -817,6 +849,105 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for checking room availability.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_check_availability(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||||
|
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||||
|
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||||
|
$exclude = isset( $_POST['exclude_booking'] ) ? absint( $_POST['exclude_booking'] ) : null;
|
||||||
|
|
||||||
|
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( strtotime( $check_out ) <= strtotime( $check_in ) ) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array( 'message' => __( 'Check-out date must be after check-in date.', 'wp-bnb' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Availability::check_availability_with_price( $room_id, $check_in, $check_out, $exclude );
|
||||||
|
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*
|
*
|
||||||
|
|||||||
1272
src/PostTypes/Booking.php
Normal file
1272
src/PostTypes/Booking.php
Normal file
File diff suppressed because it is too large
Load Diff
1086
src/PostTypes/Guest.php
Normal file
1086
src/PostTypes/Guest.php
Normal file
File diff suppressed because it is too large
Load Diff
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 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.2.0
|
* Version: 0.4.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.2.0' );
|
define( 'WP_BNB_VERSION', '0.4.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