diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1cfaaa6..e6a298b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,51 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.7.0] - 2026-02-03
+
+### Added
+
+- Contact Form 7 Integration:
+ - New `src/Integration/CF7.php` class for CF7 integration
+ - Custom form tags: `[bnb_building_select]`, `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`, `[bnb_guests]`
+ - Server-side validation for all custom tags
+ - Availability checking before form submission
+ - Automatic booking creation on form submission with 'pending' status
+ - Guest record creation/linking using existing `find_or_create_guest` pattern
+ - Price calculation using existing Calculator class
+ - Email notifications via existing EmailNotifier
+- CF7 Frontend Assets:
+ - `assets/js/cf7-integration.js` for dynamic form behavior
+ - Building-based room filtering
+ - Date linking (checkout min = checkin + 1)
+ - Capacity validation against selected room
+ - AJAX availability checking with status display
+ - Dynamic price calculation display
+ - `assets/css/cf7-integration.css` for form styling
+ - Availability status indicators (checking/available/unavailable)
+ - Price display formatting
+ - Capacity warning styling
+ - Responsive design with dark mode support
+- Custom CF7 Mail Tags:
+ - `[_bnb_booking_reference]` - Generated booking reference
+ - `[_bnb_booking_id]` - Booking post ID
+ - `[_bnb_room_name]` - Selected room title
+ - `[_bnb_calculated_price]` - Formatted price
+ - `[_bnb_nights]` - Number of nights
+- Form Type Detection:
+ - Auto-detects booking forms by presence of `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`
+ - CSS class `wp-bnb-booking-form` for explicit form type declaration
+ - Inquiry forms use default CF7 email handling without booking creation
+
+### Changed
+
+- Plugin.php updated to conditionally initialize CF7 integration when CF7 is active
+- Frontend assets now include CF7-specific CSS and JavaScript when CF7 is detected
+
+### Dependencies
+
+- Contact Form 7 plugin required for CF7 integration features (optional)
+
## [0.6.1] - 2026-02-03
### Added
diff --git a/PLAN.md b/PLAN.md
index 361781d..709848f 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -149,20 +149,20 @@ This document outlines the implementation plan for the WP BnB Management plugin.
- [x] Building rooms widget
- [x] Availability calendar widget
-## Phase 7: Contact Form 7 Integration (v0.7.0)
+## Phase 7: Contact Form 7 Integration (v0.7.0) - Complete
### Booking Request Form
-- [ ] Custom CF7 tags for rooms/dates
-- [ ] Form validation
-- [ ] Booking creation on submission
-- [ ] Email notifications
+- [x] Custom CF7 tags for rooms/dates
+- [x] Form validation
+- [x] Booking creation on submission
+- [x] Email notifications
### Inquiry Form
-- [ ] General inquiry handling
-- [ ] Room-specific inquiries
-- [ ] Auto-response templates
+- [x] General inquiry handling
+- [x] Room-specific inquiries
+- [x] Auto-response templates (uses default CF7 mail templates)
## Phase 8: Dashboard & Reports (v0.8.0)
@@ -306,7 +306,7 @@ The plugin will provide extensive hooks for customization:
| 0.4.0 | Guests | Complete |
| 0.5.0 | Services | Complete |
| 0.6.0 | Frontend | Complete |
-| 0.7.0 | CF7 Integration | TBD |
+| 0.7.0 | CF7 Integration | Complete |
| 0.8.0 | Dashboard | TBD |
| 0.9.0 | Prometheus Metrics | TBD |
| 0.10.0 | Security Audit | TBD |
diff --git a/README.md b/README.md
index 2330018..1274208 100644
--- a/README.md
+++ b/README.md
@@ -18,13 +18,14 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
- **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes
- **Auto-Updates**: Automatic update checks and installation from license server
- **Development Mode**: License bypass for local development environments
-- **Contact Form 7 Integration**: Accept booking requests through forms (planned)
+- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
### Requirements
- WordPress 6.0 or higher
- PHP 8.3 or higher
- Valid license key
+- Contact Form 7 (optional, for booking forms)
## Installation
@@ -143,6 +144,152 @@ Available sidebar widgets:
- **Building Rooms** - List all rooms in a building
- **Availability Calendar** - Mini calendar showing booking status
+## Contact Form 7 Integration
+
+The plugin integrates with Contact Form 7 to accept booking requests and inquiries. Custom form tags are provided for room selection, date pickers, and guest counts.
+
+### Custom Form Tags
+
+Use these tags in your CF7 forms:
+
+- `[bnb_building_select name]` - Building dropdown (optional filter for rooms)
+- `[bnb_room_select* name]` - Room dropdown with capacity data
+- `[bnb_date_checkin* name]` - Check-in date picker
+- `[bnb_date_checkout* name]` - Check-out date picker
+- `[bnb_guests* name]` - Guest count input
+
+### Tag Options
+
+**`[bnb_building_select]`**:
+
+- `first_as_label:"text"` - Placeholder text (default: "All Locations")
+
+**`[bnb_room_select]`**:
+
+- `building_field:"name"` - Link to building field for filtering
+- `first_as_label:"text"` - Placeholder text (default: "Select Room")
+
+**`[bnb_guests]`**:
+
+- `min:N` - Minimum guests (default: 1)
+- `max:N` - Maximum guests (default: 10)
+- `default:N` - Default value (default: 1)
+
+### Example Booking Form
+
+```txt
+
+```
+
+### Example Inquiry Form
+
+For room-specific inquiries, add the `wp-bnb-inquiry-form` class:
+
+```txt
+
+```
+
+### Form Features
+
+- **Availability Checking**: Real-time AJAX validation shows room availability
+- **Price Display**: Estimated total calculated and displayed automatically
+- **Room Filtering**: Rooms filter by building selection
+- **Date Validation**: Check-out must be after check-in, no past dates
+- **Capacity Validation**: Guest count validated against room capacity
+- **Automatic Booking**: Booking record created with "pending" status on submission
+- **Guest Linking**: Guest records created or linked by email address
+
+### Custom Mail Tags
+
+Use these in your CF7 mail templates:
+
+- `[_bnb_room_name]` - Room title
+- `[_bnb_building_name]` - Building name
+- `[_bnb_calculated_price]` - Formatted price
+- `[_bnb_nights]` - Number of nights
+- `[_bnb_booking_reference]` - Booking reference (after creation)
+
## Hooks and Filters
Developers can customize behavior using these hooks:
diff --git a/assets/css/cf7-integration.css b/assets/css/cf7-integration.css
new file mode 100644
index 0000000..1dca1de
--- /dev/null
+++ b/assets/css/cf7-integration.css
@@ -0,0 +1,344 @@
+/**
+ * WP BnB Contact Form 7 Integration Styles
+ *
+ * Styling for CF7 booking forms.
+ *
+ * @package Magdev\WpBnb
+ */
+
+/* Custom Properties */
+:root {
+ --wp-bnb-cf7-primary: #2271b1;
+ --wp-bnb-cf7-success: #00a32a;
+ --wp-bnb-cf7-warning: #dba617;
+ --wp-bnb-cf7-error: #d63638;
+ --wp-bnb-cf7-text: #1d2327;
+ --wp-bnb-cf7-text-light: #646970;
+ --wp-bnb-cf7-border: #c3c4c7;
+ --wp-bnb-cf7-bg: #f0f0f1;
+ --wp-bnb-cf7-radius: 4px;
+ --wp-bnb-cf7-spacing: 1rem;
+}
+
+/* Form Layout */
+.wp-bnb-booking-form,
+.wp-bnb-inquiry-form {
+ max-width: 700px;
+ margin: 0 auto;
+}
+
+.wp-bnb-booking-form h3,
+.wp-bnb-booking-form h4,
+.wp-bnb-inquiry-form h3,
+.wp-bnb-inquiry-form h4 {
+ margin-top: 1.5em;
+ margin-bottom: 0.75em;
+ padding-bottom: 0.5em;
+ border-bottom: 1px solid var(--wp-bnb-cf7-border);
+}
+
+.wp-bnb-booking-form h3:first-child,
+.wp-bnb-inquiry-form h3:first-child {
+ margin-top: 0;
+}
+
+/* Form Rows */
+.wp-bnb-form-row {
+ margin-bottom: var(--wp-bnb-cf7-spacing);
+}
+
+.wp-bnb-form-row-2col {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--wp-bnb-cf7-spacing);
+}
+
+@media (max-width: 480px) {
+ .wp-bnb-form-row-2col {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Form Fields */
+.wp-bnb-form-field {
+ display: flex;
+ flex-direction: column;
+}
+
+.wp-bnb-form-field label {
+ display: block;
+ margin-bottom: 0.25rem;
+ font-weight: 600;
+ font-size: 0.875rem;
+ color: var(--wp-bnb-cf7-text);
+}
+
+/* Custom CF7 Tags Styling */
+.wp-bnb-building-select,
+.wp-bnb-room-select,
+.wp-bnb-date-checkin,
+.wp-bnb-date-checkout,
+.wp-bnb-guests {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ border: 1px solid var(--wp-bnb-cf7-border);
+ border-radius: var(--wp-bnb-cf7-radius);
+ background-color: #fff;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.wp-bnb-building-select:focus,
+.wp-bnb-room-select:focus,
+.wp-bnb-date-checkin:focus,
+.wp-bnb-date-checkout:focus,
+.wp-bnb-guests:focus {
+ outline: none;
+ border-color: var(--wp-bnb-cf7-primary);
+ box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.25);
+}
+
+/* Select dropdown */
+.wp-bnb-room-select optgroup {
+ font-weight: 600;
+ font-style: normal;
+ color: var(--wp-bnb-cf7-text);
+}
+
+/* Date inputs */
+.wp-bnb-date-checkin,
+.wp-bnb-date-checkout {
+ cursor: pointer;
+}
+
+/* Number input */
+.wp-bnb-guests {
+ max-width: 120px;
+}
+
+/* Availability Status */
+.wp-bnb-availability-status {
+ padding: var(--wp-bnb-cf7-spacing);
+ margin: var(--wp-bnb-cf7-spacing) 0;
+ background-color: var(--wp-bnb-cf7-bg);
+ border-radius: var(--wp-bnb-cf7-radius);
+ text-align: center;
+ min-height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.wp-bnb-availability-status:empty {
+ display: none;
+}
+
+.wp-bnb-checking {
+ color: var(--wp-bnb-cf7-text-light);
+ font-style: italic;
+}
+
+.wp-bnb-checking::before {
+ content: "";
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+ border: 2px solid var(--wp-bnb-cf7-border);
+ border-top-color: var(--wp-bnb-cf7-primary);
+ border-radius: 50%;
+ animation: wp-bnb-spin 0.8s linear infinite;
+ vertical-align: middle;
+}
+
+@keyframes wp-bnb-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.wp-bnb-available {
+ color: var(--wp-bnb-cf7-success);
+ font-weight: 600;
+}
+
+.wp-bnb-available::before {
+ content: "\2713";
+ display: inline-block;
+ margin-right: 8px;
+ font-size: 1.25em;
+}
+
+.wp-bnb-unavailable {
+ color: var(--wp-bnb-cf7-error);
+ font-weight: 600;
+}
+
+.wp-bnb-unavailable::before {
+ content: "\2717";
+ display: inline-block;
+ margin-right: 8px;
+ font-size: 1.25em;
+}
+
+/* Price Display */
+.wp-bnb-price-display {
+ padding: var(--wp-bnb-cf7-spacing);
+ margin: var(--wp-bnb-cf7-spacing) 0;
+ background-color: #e7f5ea;
+ border: 1px solid var(--wp-bnb-cf7-success);
+ border-radius: var(--wp-bnb-cf7-radius);
+ text-align: center;
+}
+
+.wp-bnb-price-display:empty {
+ display: none;
+}
+
+.wp-bnb-price-label {
+ color: var(--wp-bnb-cf7-text-light);
+ font-size: 0.875rem;
+}
+
+.wp-bnb-price-amount {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--wp-bnb-cf7-success);
+ margin: 0 0.5rem;
+}
+
+.wp-bnb-nights {
+ color: var(--wp-bnb-cf7-text-light);
+ font-size: 0.875rem;
+}
+
+/* Capacity Warning */
+.wp-bnb-capacity-warning {
+ display: block;
+ margin-top: 0.25rem;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8125rem;
+ color: var(--wp-bnb-cf7-error);
+ background-color: #fcf0f1;
+ border-radius: var(--wp-bnb-cf7-radius);
+}
+
+/* Validation Errors */
+.wpcf7-form-control-wrap .wpcf7-not-valid-tip {
+ color: var(--wp-bnb-cf7-error);
+ font-size: 0.8125rem;
+ margin-top: 0.25rem;
+}
+
+.wpcf7-form-control.wpcf7-not-valid {
+ border-color: var(--wp-bnb-cf7-error);
+}
+
+/* Response Messages */
+.wpcf7 form.sent .wpcf7-response-output {
+ border-color: var(--wp-bnb-cf7-success);
+ background-color: #e7f5ea;
+ color: var(--wp-bnb-cf7-success);
+}
+
+.wpcf7 form.failed .wpcf7-response-output,
+.wpcf7 form.aborted .wpcf7-response-output,
+.wpcf7 form.spam .wpcf7-response-output,
+.wpcf7 form.invalid .wpcf7-response-output {
+ border-color: var(--wp-bnb-cf7-error);
+ background-color: #fcf0f1;
+ color: var(--wp-bnb-cf7-error);
+}
+
+/* Submit Button */
+.wp-bnb-booking-form .wpcf7-submit,
+.wp-bnb-inquiry-form .wpcf7-submit {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #fff;
+ background-color: var(--wp-bnb-cf7-primary);
+ border: none;
+ border-radius: var(--wp-bnb-cf7-radius);
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out;
+}
+
+.wp-bnb-booking-form .wpcf7-submit:hover,
+.wp-bnb-inquiry-form .wpcf7-submit:hover {
+ background-color: #135e96;
+}
+
+.wp-bnb-booking-form .wpcf7-submit:disabled,
+.wp-bnb-inquiry-form .wpcf7-submit:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+}
+
+/* Spinner */
+.wpcf7 .wpcf7-spinner {
+ margin-left: 0.5rem;
+}
+
+/* Hidden Room Field (for inquiry forms) */
+.wp-bnb-inquiry-form input[type="hidden"] + .wpcf7-form-control-wrap {
+ display: none;
+}
+
+/* Form Section Headers */
+.wp-bnb-booking-form hr,
+.wp-bnb-inquiry-form hr {
+ margin: 1.5rem 0;
+ border: none;
+ border-top: 1px solid var(--wp-bnb-cf7-border);
+}
+
+/* Dark Mode Support */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --wp-bnb-cf7-text: #f0f0f1;
+ --wp-bnb-cf7-text-light: #a7aaad;
+ --wp-bnb-cf7-border: #50575e;
+ --wp-bnb-cf7-bg: #2c3338;
+ }
+
+ .wp-bnb-building-select,
+ .wp-bnb-room-select,
+ .wp-bnb-date-checkin,
+ .wp-bnb-date-checkout,
+ .wp-bnb-guests {
+ background-color: #3c434a;
+ color: var(--wp-bnb-cf7-text);
+ }
+
+ .wp-bnb-price-display {
+ background-color: #1a3320;
+ border-color: var(--wp-bnb-cf7-success);
+ }
+
+ .wp-bnb-capacity-warning {
+ background-color: #3c1618;
+ }
+
+ .wpcf7 form.sent .wpcf7-response-output {
+ background-color: #1a3320;
+ }
+
+ .wpcf7 form.failed .wpcf7-response-output,
+ .wpcf7 form.aborted .wpcf7-response-output,
+ .wpcf7 form.spam .wpcf7-response-output,
+ .wpcf7 form.invalid .wpcf7-response-output {
+ background-color: #3c1618;
+ }
+}
+
+/* Print Styles */
+@media print {
+ .wp-bnb-availability-status,
+ .wp-bnb-price-display,
+ .wpcf7-submit {
+ display: none;
+ }
+}
diff --git a/assets/js/cf7-integration.js b/assets/js/cf7-integration.js
new file mode 100644
index 0000000..e9342b2
--- /dev/null
+++ b/assets/js/cf7-integration.js
@@ -0,0 +1,375 @@
+/**
+ * WP BnB Contact Form 7 Integration
+ *
+ * Handles dynamic form behavior for booking forms.
+ *
+ * @package Magdev\WpBnb
+ */
+(function() {
+ 'use strict';
+
+ const WpBnbCF7 = {
+ config: window.wpBnbCF7 || {},
+
+ /**
+ * Initialize all CF7 integration features.
+ */
+ init: function() {
+ this.initBuildingRoomFilter();
+ this.initDateValidation();
+ this.initCapacityValidation();
+ this.initAvailabilityCheck();
+ this.initPriceDisplay();
+ },
+
+ /**
+ * Filter rooms dropdown when building is selected.
+ */
+ initBuildingRoomFilter: function() {
+ document.querySelectorAll('[data-bnb-building-select]').forEach(function(buildingSelect) {
+ const form = buildingSelect.closest('form');
+ const roomSelect = form ? form.querySelector('[data-bnb-room-select]') : null;
+
+ if (!roomSelect) return;
+
+ // Store all options for filtering
+ const allOptions = Array.from(roomSelect.querySelectorAll('option, optgroup'));
+ const originalHTML = roomSelect.innerHTML;
+
+ buildingSelect.addEventListener('change', function() {
+ const selectedBuilding = buildingSelect.value;
+
+ // Show all options if no building selected
+ if (!selectedBuilding) {
+ roomSelect.innerHTML = originalHTML;
+ roomSelect.dispatchEvent(new Event('change'));
+ return;
+ }
+
+ // Filter options by building
+ roomSelect.innerHTML = '';
+
+ // Add placeholder option
+ const placeholder = document.createElement('option');
+ placeholder.value = '';
+ placeholder.textContent = WpBnbCF7.config.i18n?.selectRoom || '-- Select Room --';
+ roomSelect.appendChild(placeholder);
+
+ allOptions.forEach(function(el) {
+ if (el.tagName === 'OPTGROUP') {
+ // Check if any options in this optgroup match
+ const matchingOptions = Array.from(el.querySelectorAll('option')).filter(function(opt) {
+ return opt.dataset.building === selectedBuilding;
+ });
+
+ if (matchingOptions.length > 0) {
+ const clonedGroup = el.cloneNode(false);
+ matchingOptions.forEach(function(opt) {
+ clonedGroup.appendChild(opt.cloneNode(true));
+ });
+ roomSelect.appendChild(clonedGroup);
+ }
+ }
+ });
+
+ // Trigger change to update dependent fields
+ roomSelect.dispatchEvent(new Event('change'));
+ });
+ });
+ },
+
+ /**
+ * Validate and link check-in/check-out dates.
+ */
+ initDateValidation: function() {
+ document.querySelectorAll('[data-bnb-checkin]').forEach(function(checkinInput) {
+ const form = checkinInput.closest('form');
+ const checkoutInput = form ? form.querySelector('[data-bnb-checkout]') : null;
+
+ if (!checkoutInput) return;
+
+ // Set minimum check-in to today
+ const today = WpBnbCF7.formatDate(new Date());
+ if (!checkinInput.getAttribute('min') || checkinInput.getAttribute('min') < today) {
+ checkinInput.setAttribute('min', today);
+ }
+
+ checkinInput.addEventListener('change', function() {
+ if (checkinInput.value) {
+ // Set checkout minimum to checkin + 1 day
+ const minCheckout = new Date(checkinInput.value);
+ minCheckout.setDate(minCheckout.getDate() + 1);
+ checkoutInput.setAttribute('min', WpBnbCF7.formatDate(minCheckout));
+
+ // Clear checkout if it's now invalid
+ if (checkoutInput.value && checkoutInput.value <= checkinInput.value) {
+ checkoutInput.value = '';
+ }
+
+ // Trigger availability check
+ WpBnbCF7.triggerAvailabilityCheck(form);
+ }
+ });
+
+ checkoutInput.addEventListener('change', function() {
+ if (checkoutInput.value && checkinInput.value) {
+ if (checkoutInput.value <= checkinInput.value) {
+ alert(WpBnbCF7.config.i18n?.invalidDateRange || 'Check-out must be after check-in');
+ checkoutInput.value = '';
+ return;
+ }
+
+ // Trigger availability check
+ WpBnbCF7.triggerAvailabilityCheck(form);
+ }
+ });
+ });
+ },
+
+ /**
+ * Validate guest count against room capacity.
+ */
+ initCapacityValidation: function() {
+ document.querySelectorAll('[data-bnb-guests]').forEach(function(guestsInput) {
+ const form = guestsInput.closest('form');
+ const roomSelect = form ? form.querySelector('[data-bnb-room-select]') : null;
+
+ if (!roomSelect) return;
+
+ const validateCapacity = function() {
+ const selectedOption = roomSelect.selectedOptions[0];
+ const capacity = parseInt(selectedOption?.dataset.capacity || 99, 10);
+ const guests = parseInt(guestsInput.value || 0, 10);
+
+ // Update max attribute
+ guestsInput.setAttribute('max', capacity);
+
+ // Show warning if over capacity
+ const wrapper = guestsInput.closest('.wpcf7-form-control-wrap');
+ let warning = wrapper ? wrapper.querySelector('.wp-bnb-capacity-warning') : null;
+
+ if (guests > capacity) {
+ if (!warning && wrapper) {
+ warning = document.createElement('span');
+ warning.className = 'wp-bnb-capacity-warning';
+ wrapper.appendChild(warning);
+ }
+ if (warning) {
+ warning.textContent = (WpBnbCF7.config.i18n?.capacityExceeded || 'Maximum %d guests for this room').replace('%d', capacity);
+ }
+ } else if (warning) {
+ warning.remove();
+ }
+ };
+
+ roomSelect.addEventListener('change', validateCapacity);
+ guestsInput.addEventListener('change', validateCapacity);
+ guestsInput.addEventListener('input', validateCapacity);
+ });
+ },
+
+ /**
+ * Initialize AJAX availability checking.
+ */
+ initAvailabilityCheck: function() {
+ // Find forms with availability display
+ document.querySelectorAll('.wp-bnb-availability-status').forEach(function(statusEl) {
+ const form = statusEl.closest('form');
+ if (form) {
+ form._availabilityStatus = statusEl;
+ }
+ });
+ },
+
+ /**
+ * Trigger availability check for a form.
+ *
+ * @param {HTMLFormElement} form Form element.
+ */
+ triggerAvailabilityCheck: function(form) {
+ const roomSelect = form.querySelector('[data-bnb-room-select]');
+ const checkinInput = form.querySelector('[data-bnb-checkin]');
+ const checkoutInput = form.querySelector('[data-bnb-checkout]');
+ const statusEl = form._availabilityStatus || form.querySelector('.wp-bnb-availability-status');
+ const priceEl = form.querySelector('.wp-bnb-price-display');
+
+ if (!roomSelect || !checkinInput || !checkoutInput) return;
+
+ const roomId = roomSelect.value;
+ const checkIn = checkinInput.value;
+ const checkOut = checkoutInput.value;
+
+ if (!roomId || !checkIn || !checkOut) {
+ if (statusEl) statusEl.innerHTML = '';
+ if (priceEl) priceEl.innerHTML = '';
+ return;
+ }
+
+ // Show loading state
+ if (statusEl) {
+ statusEl.innerHTML = '' + (WpBnbCF7.config.i18n?.checking || 'Checking availability...') + ' ';
+ }
+
+ // Make AJAX request
+ WpBnbCF7.ajax('wp_bnb_get_availability', {
+ room_id: roomId,
+ check_in: checkIn,
+ check_out: checkOut
+ })
+ .then(function(response) {
+ if (statusEl) {
+ if (response.available) {
+ let html = '' + (WpBnbCF7.config.i18n?.available || 'Room is available!') + ' ';
+ statusEl.innerHTML = html;
+ } else {
+ statusEl.innerHTML = '' + (WpBnbCF7.config.i18n?.unavailable || 'Room is not available for these dates') + ' ';
+ }
+ }
+
+ // Update price display
+ if (priceEl && response.available && response.price_formatted) {
+ priceEl.innerHTML = '' + (WpBnbCF7.config.i18n?.estimatedTotal || 'Estimated Total') + ': ' +
+ '' + response.price_formatted + ' ' +
+ '(' + response.nights + ' ' + (WpBnbCF7.config.i18n?.nights || 'nights') + ') ';
+ } else if (priceEl) {
+ priceEl.innerHTML = '';
+ }
+ })
+ .catch(function(error) {
+ console.error('Availability check failed:', error);
+ if (statusEl) {
+ statusEl.innerHTML = '';
+ }
+ });
+ },
+
+ /**
+ * Initialize price display updates.
+ */
+ initPriceDisplay: function() {
+ const self = this;
+
+ document.querySelectorAll('.wp-bnb-price-display').forEach(function(priceEl) {
+ const form = priceEl.closest('form');
+ if (!form) return;
+
+ const updatePrice = self.debounce(function() {
+ const roomSelect = form.querySelector('[data-bnb-room-select]');
+ const checkinInput = form.querySelector('[data-bnb-checkin]');
+ const checkoutInput = form.querySelector('[data-bnb-checkout]');
+
+ if (!roomSelect?.value || !checkinInput?.value || !checkoutInput?.value) {
+ priceEl.innerHTML = '';
+ return;
+ }
+
+ self.ajax('wp_bnb_calculate_price', {
+ room_id: roomSelect.value,
+ check_in: checkinInput.value,
+ check_out: checkoutInput.value
+ })
+ .then(function(response) {
+ priceEl.innerHTML = '' + (self.config.i18n?.estimatedTotal || 'Estimated Total') + ': ' +
+ '' + response.price_formatted + ' ' +
+ '(' + response.nights + ' ' + (self.config.i18n?.nights || 'nights') + ') ';
+ })
+ .catch(function() {
+ priceEl.innerHTML = '';
+ });
+ }, 500);
+
+ // Bind to relevant field changes
+ form.querySelectorAll('[data-bnb-room-select], [data-bnb-checkin], [data-bnb-checkout]')
+ .forEach(function(input) {
+ input.addEventListener('change', updatePrice);
+ });
+ });
+ },
+
+ /**
+ * Make AJAX request.
+ *
+ * @param {string} action AJAX action name.
+ * @param {object} data Request data.
+ * @return {Promise}
+ */
+ ajax: function(action, data) {
+ data = data || {};
+ const formData = new FormData();
+ formData.append('action', action);
+ formData.append('nonce', this.config.nonce || '');
+
+ Object.keys(data).forEach(function(key) {
+ if (data[key] !== null && data[key] !== undefined) {
+ formData.append(key, data[key]);
+ }
+ });
+
+ return fetch(this.config.ajaxUrl || '/wp-admin/admin-ajax.php', {
+ method: 'POST',
+ body: formData,
+ credentials: 'same-origin'
+ })
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(responseData) {
+ if (!responseData.success) {
+ throw new Error(responseData.data?.message || 'Request failed');
+ }
+ return responseData.data;
+ });
+ },
+
+ /**
+ * Format date as YYYY-MM-DD.
+ *
+ * @param {Date} date Date object.
+ * @return {string}
+ */
+ formatDate: function(date) {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return year + '-' + month + '-' + day;
+ },
+
+ /**
+ * Debounce function.
+ *
+ * @param {function} func Function to debounce.
+ * @param {number} wait Milliseconds to wait.
+ * @return {function}
+ */
+ debounce: function(func, wait) {
+ let timeout;
+ return function() {
+ const context = this;
+ const args = arguments;
+ clearTimeout(timeout);
+ timeout = setTimeout(function() {
+ func.apply(context, args);
+ }, wait);
+ };
+ }
+ };
+
+ // Initialize on DOM ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ WpBnbCF7.init();
+ });
+ } else {
+ WpBnbCF7.init();
+ }
+
+ // Re-initialize on CF7 form reset
+ document.addEventListener('wpcf7reset', function(event) {
+ setTimeout(function() {
+ WpBnbCF7.init();
+ }, 100);
+ });
+
+ // Export to window for external access
+ window.WpBnbCF7 = WpBnbCF7;
+})();
diff --git a/src/Integration/CF7.php b/src/Integration/CF7.php
new file mode 100644
index 0000000..c245317
--- /dev/null
+++ b/src/Integration/CF7.php
@@ -0,0 +1,1087 @@
+ true )
+ );
+
+ // Room selector.
+ wpcf7_add_form_tag(
+ array( 'bnb_room_select', 'bnb_room_select*' ),
+ array( self::class, 'render_room_select_tag' ),
+ array( 'name-attr' => true )
+ );
+
+ // Check-in date.
+ wpcf7_add_form_tag(
+ array( 'bnb_date_checkin', 'bnb_date_checkin*' ),
+ array( self::class, 'render_date_checkin_tag' ),
+ array( 'name-attr' => true )
+ );
+
+ // Check-out date.
+ wpcf7_add_form_tag(
+ array( 'bnb_date_checkout', 'bnb_date_checkout*' ),
+ array( self::class, 'render_date_checkout_tag' ),
+ array( 'name-attr' => true )
+ );
+
+ // Guests count.
+ wpcf7_add_form_tag(
+ array( 'bnb_guests', 'bnb_guests*' ),
+ array( self::class, 'render_guests_tag' ),
+ array( 'name-attr' => true )
+ );
+ }
+
+ /**
+ * Render building select tag.
+ *
+ * @param \WPCF7_FormTag $tag Form tag object.
+ * @return string HTML output.
+ */
+ public static function render_building_select_tag( $tag ): string {
+ if ( empty( $tag->name ) ) {
+ return '';
+ }
+
+ $validation_error = wpcf7_get_validation_error( $tag->name );
+ $class = wpcf7_form_controls_class( $tag->type );
+
+ if ( $validation_error ) {
+ $class .= ' wpcf7-not-valid';
+ }
+
+ $atts = array(
+ 'class' => trim( $class . ' wp-bnb-building-select' ),
+ 'id' => $tag->get_id_option(),
+ 'name' => $tag->name,
+ 'aria-required' => $tag->is_required() ? 'true' : 'false',
+ 'aria-invalid' => $validation_error ? 'true' : 'false',
+ 'data-bnb-building-select' => 'true',
+ );
+
+ // Get first_as_label option.
+ $first_label = __( '-- Select Building --', 'wp-bnb' );
+ foreach ( $tag->options as $option ) {
+ if ( strpos( $option, 'first_as_label:' ) === 0 ) {
+ $first_label = str_replace( array( 'first_as_label:', '"', "'" ), '', $option );
+ break;
+ }
+ }
+
+ // Get buildings that have rooms.
+ $buildings = get_posts(
+ array(
+ 'post_type' => Building::POST_TYPE,
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ )
+ );
+
+ // Build options.
+ $options = sprintf( '%s ', esc_html( $first_label ) );
+
+ foreach ( $buildings as $building ) {
+ $room_count = count( Room::get_rooms_for_building( $building->ID ) );
+ if ( $room_count > 0 ) {
+ $options .= sprintf(
+ '%s ',
+ esc_attr( $building->ID ),
+ esc_html( $building->post_title )
+ );
+ }
+ }
+
+ $atts_html = wpcf7_format_atts( $atts );
+
+ $html = sprintf( '', esc_attr( $tag->name ) );
+ $html .= sprintf( '%s ', $atts_html, $options );
+ $html .= $validation_error;
+ $html .= ' ';
+
+ return $html;
+ }
+
+ /**
+ * Render room select tag.
+ *
+ * @param \WPCF7_FormTag $tag Form tag object.
+ * @return string HTML output.
+ */
+ public static function render_room_select_tag( $tag ): string {
+ if ( empty( $tag->name ) ) {
+ return '';
+ }
+
+ $validation_error = wpcf7_get_validation_error( $tag->name );
+ $class = wpcf7_form_controls_class( $tag->type );
+
+ if ( $validation_error ) {
+ $class .= ' wpcf7-not-valid';
+ }
+
+ // Parse options.
+ $building_field = '';
+ $include_price = false;
+
+ foreach ( $tag->options as $option ) {
+ if ( strpos( $option, 'building_field:' ) === 0 ) {
+ $building_field = str_replace( array( 'building_field:', '"', "'" ), '', $option );
+ }
+ if ( 'include_price:true' === $option || 'include_price' === $option ) {
+ $include_price = true;
+ }
+ }
+
+ $atts = array(
+ 'class' => trim( $class . ' wp-bnb-room-select' ),
+ 'id' => $tag->get_id_option(),
+ 'name' => $tag->name,
+ 'aria-required' => $tag->is_required() ? 'true' : 'false',
+ 'aria-invalid' => $validation_error ? 'true' : 'false',
+ 'data-bnb-room-select' => 'true',
+ );
+
+ if ( $building_field ) {
+ $atts['data-building-field'] = $building_field;
+ }
+
+ if ( $tag->is_required() ) {
+ $atts['required'] = 'required';
+ }
+
+ // Get all rooms grouped by building.
+ $rooms = get_posts(
+ array(
+ 'post_type' => Room::POST_TYPE,
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ )
+ );
+
+ $rooms_by_building = array();
+ foreach ( $rooms as $room ) {
+ $building = Room::get_building( $room->ID );
+ $building_id = $building ? $building->ID : 0;
+
+ if ( ! isset( $rooms_by_building[ $building_id ] ) ) {
+ $rooms_by_building[ $building_id ] = array(
+ 'name' => $building ? $building->post_title : __( 'No Building', 'wp-bnb' ),
+ 'rooms' => array(),
+ );
+ }
+
+ $rooms_by_building[ $building_id ]['rooms'][] = $room;
+ }
+
+ // Build options.
+ $options = sprintf( '%s ', esc_html__( '-- Select Room --', 'wp-bnb' ) );
+ $currency = get_option( 'wp_bnb_currency', 'CHF' );
+
+ foreach ( $rooms_by_building as $building_id => $data ) {
+ $options .= sprintf( '', esc_attr( $data['name'] ) );
+
+ foreach ( $data['rooms'] as $room ) {
+ $capacity = (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ) ?: 2;
+ $nightly_rate = (float) get_post_meta( $room->ID, '_bnb_room_price_short_term', true );
+ $room_status = get_post_meta( $room->ID, '_bnb_room_status', true ) ?: 'available';
+
+ // Skip rooms that are not available.
+ if ( 'available' !== $room_status ) {
+ continue;
+ }
+
+ $label = $room->post_title;
+ $label .= sprintf( ' (%d %s)', $capacity, _n( 'guest', 'guests', $capacity, 'wp-bnb' ) );
+
+ if ( $include_price && $nightly_rate > 0 ) {
+ $label .= sprintf( ' - %s %s/%s', $currency, number_format( $nightly_rate, 2 ), __( 'night', 'wp-bnb' ) );
+ }
+
+ $options .= sprintf(
+ '%s ',
+ esc_attr( $room->ID ),
+ esc_attr( $capacity ),
+ esc_attr( $building_id ),
+ esc_attr( $nightly_rate ),
+ esc_html( $label )
+ );
+ }
+
+ $options .= ' ';
+ }
+
+ $atts_html = wpcf7_format_atts( $atts );
+
+ $html = sprintf( '', esc_attr( $tag->name ) );
+ $html .= sprintf( '%s ', $atts_html, $options );
+ $html .= $validation_error;
+ $html .= ' ';
+
+ return $html;
+ }
+
+ /**
+ * Render check-in date tag.
+ *
+ * @param \WPCF7_FormTag $tag Form tag object.
+ * @return string HTML output.
+ */
+ public static function render_date_checkin_tag( $tag ): string {
+ if ( empty( $tag->name ) ) {
+ return '';
+ }
+
+ $validation_error = wpcf7_get_validation_error( $tag->name );
+ $class = wpcf7_form_controls_class( $tag->type, 'wpcf7-date' );
+
+ if ( $validation_error ) {
+ $class .= ' wpcf7-not-valid';
+ }
+
+ // Parse options.
+ $min_advance = 0;
+ $max_advance = 365;
+
+ foreach ( $tag->options as $option ) {
+ if ( strpos( $option, 'min_advance:' ) === 0 ) {
+ $min_advance = (int) str_replace( 'min_advance:', '', $option );
+ }
+ if ( strpos( $option, 'max_advance:' ) === 0 ) {
+ $max_advance = (int) str_replace( 'max_advance:', '', $option );
+ }
+ }
+
+ $min_date = gmdate( 'Y-m-d', strtotime( "+{$min_advance} days" ) );
+ $max_date = gmdate( 'Y-m-d', strtotime( "+{$max_advance} days" ) );
+
+ $atts = array(
+ 'type' => 'date',
+ 'class' => trim( $class . ' wp-bnb-date-checkin' ),
+ 'id' => $tag->get_id_option(),
+ 'name' => $tag->name,
+ 'min' => $min_date,
+ 'max' => $max_date,
+ 'aria-required' => $tag->is_required() ? 'true' : 'false',
+ 'aria-invalid' => $validation_error ? 'true' : 'false',
+ 'data-bnb-checkin' => 'true',
+ );
+
+ if ( $tag->is_required() ) {
+ $atts['required'] = 'required';
+ }
+
+ // Handle default value from POST.
+ if ( isset( $_POST[ $tag->name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $atts['value'] = sanitize_text_field( wp_unslash( $_POST[ $tag->name ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ }
+
+ $atts_html = wpcf7_format_atts( $atts );
+
+ $html = sprintf( '', esc_attr( $tag->name ) );
+ $html .= sprintf( ' ', $atts_html );
+ $html .= $validation_error;
+ $html .= ' ';
+
+ return $html;
+ }
+
+ /**
+ * Render check-out date tag.
+ *
+ * @param \WPCF7_FormTag $tag Form tag object.
+ * @return string HTML output.
+ */
+ public static function render_date_checkout_tag( $tag ): string {
+ if ( empty( $tag->name ) ) {
+ return '';
+ }
+
+ $validation_error = wpcf7_get_validation_error( $tag->name );
+ $class = wpcf7_form_controls_class( $tag->type, 'wpcf7-date' );
+
+ if ( $validation_error ) {
+ $class .= ' wpcf7-not-valid';
+ }
+
+ // Parse options.
+ $checkin_field = 'check_in';
+ $min_nights = 1;
+ $max_nights = 365;
+
+ foreach ( $tag->options as $option ) {
+ if ( strpos( $option, 'checkin_field:' ) === 0 ) {
+ $checkin_field = str_replace( array( 'checkin_field:', '"', "'" ), '', $option );
+ }
+ if ( strpos( $option, 'min_nights:' ) === 0 ) {
+ $min_nights = (int) str_replace( 'min_nights:', '', $option );
+ }
+ if ( strpos( $option, 'max_nights:' ) === 0 ) {
+ $max_nights = (int) str_replace( 'max_nights:', '', $option );
+ }
+ }
+
+ // Default min is tomorrow.
+ $min_date = gmdate( 'Y-m-d', strtotime( '+1 day' ) );
+ $max_date = gmdate( 'Y-m-d', strtotime( '+366 days' ) );
+
+ $atts = array(
+ 'type' => 'date',
+ 'class' => trim( $class . ' wp-bnb-date-checkout' ),
+ 'id' => $tag->get_id_option(),
+ 'name' => $tag->name,
+ 'min' => $min_date,
+ 'max' => $max_date,
+ 'aria-required' => $tag->is_required() ? 'true' : 'false',
+ 'aria-invalid' => $validation_error ? 'true' : 'false',
+ 'data-bnb-checkout' => 'true',
+ 'data-checkin-field' => $checkin_field,
+ 'data-min-nights' => $min_nights,
+ 'data-max-nights' => $max_nights,
+ );
+
+ if ( $tag->is_required() ) {
+ $atts['required'] = 'required';
+ }
+
+ // Handle default value from POST.
+ if ( isset( $_POST[ $tag->name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $atts['value'] = sanitize_text_field( wp_unslash( $_POST[ $tag->name ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ }
+
+ $atts_html = wpcf7_format_atts( $atts );
+
+ $html = sprintf( '', esc_attr( $tag->name ) );
+ $html .= sprintf( ' ', $atts_html );
+ $html .= $validation_error;
+ $html .= ' ';
+
+ return $html;
+ }
+
+ /**
+ * Render guests count tag.
+ *
+ * @param \WPCF7_FormTag $tag Form tag object.
+ * @return string HTML output.
+ */
+ public static function render_guests_tag( $tag ): string {
+ if ( empty( $tag->name ) ) {
+ return '';
+ }
+
+ $validation_error = wpcf7_get_validation_error( $tag->name );
+ $class = wpcf7_form_controls_class( $tag->type, 'wpcf7-number' );
+
+ if ( $validation_error ) {
+ $class .= ' wpcf7-not-valid';
+ }
+
+ // Parse options.
+ $min = 1;
+ $max = 10;
+ $default = 1;
+ $room_field = '';
+
+ foreach ( $tag->options as $option ) {
+ if ( strpos( $option, 'min:' ) === 0 ) {
+ $min = (int) str_replace( 'min:', '', $option );
+ }
+ if ( strpos( $option, 'max:' ) === 0 ) {
+ $max = (int) str_replace( 'max:', '', $option );
+ }
+ if ( strpos( $option, 'default:' ) === 0 ) {
+ $default = (int) str_replace( 'default:', '', $option );
+ }
+ if ( strpos( $option, 'room_field:' ) === 0 ) {
+ $room_field = str_replace( array( 'room_field:', '"', "'" ), '', $option );
+ }
+ }
+
+ $atts = array(
+ 'type' => 'number',
+ 'class' => trim( $class . ' wp-bnb-guests' ),
+ 'id' => $tag->get_id_option(),
+ 'name' => $tag->name,
+ 'min' => $min,
+ 'max' => $max,
+ 'value' => $default,
+ 'aria-required' => $tag->is_required() ? 'true' : 'false',
+ 'aria-invalid' => $validation_error ? 'true' : 'false',
+ 'data-bnb-guests' => 'true',
+ );
+
+ if ( $room_field ) {
+ $atts['data-room-field'] = $room_field;
+ }
+
+ if ( $tag->is_required() ) {
+ $atts['required'] = 'required';
+ }
+
+ // Handle default value from POST.
+ if ( isset( $_POST[ $tag->name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $atts['value'] = absint( $_POST[ $tag->name ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ }
+
+ $atts_html = wpcf7_format_atts( $atts );
+
+ $html = sprintf( '', esc_attr( $tag->name ) );
+ $html .= sprintf( ' ', $atts_html );
+ $html .= $validation_error;
+ $html .= ' ';
+
+ return $html;
+ }
+
+ /**
+ * Validate room select field.
+ *
+ * @param \WPCF7_Validation $result Validation result.
+ * @param \WPCF7_FormTag $tag Form tag.
+ * @return \WPCF7_Validation
+ */
+ public static function validate_room_select( $result, $tag ) {
+ $name = $tag->name;
+ $room_id = isset( $_POST[ $name ] ) ? absint( $_POST[ $name ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ if ( $tag->is_required() && ! $room_id ) {
+ $result->invalidate( $tag, __( 'Please select a room.', 'wp-bnb' ) );
+ return $result;
+ }
+
+ if ( $room_id ) {
+ $room = get_post( $room_id );
+ if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
+ $result->invalidate( $tag, __( 'Invalid room selected.', 'wp-bnb' ) );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Validate check-in date field.
+ *
+ * @param \WPCF7_Validation $result Validation result.
+ * @param \WPCF7_FormTag $tag Form tag.
+ * @return \WPCF7_Validation
+ */
+ public static function validate_date_checkin( $result, $tag ) {
+ $name = $tag->name;
+ $date = isset( $_POST[ $name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $name ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ if ( $tag->is_required() && empty( $date ) ) {
+ $result->invalidate( $tag, __( 'Please select a check-in date.', 'wp-bnb' ) );
+ return $result;
+ }
+
+ if ( $date ) {
+ // Validate format.
+ if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
+ $result->invalidate( $tag, __( 'Invalid date format.', 'wp-bnb' ) );
+ return $result;
+ }
+
+ // Validate not in past.
+ $check_in = strtotime( $date );
+ $today = strtotime( 'today' );
+
+ if ( $check_in < $today ) {
+ $result->invalidate( $tag, __( 'Check-in date cannot be in the past.', 'wp-bnb' ) );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Validate check-out date field.
+ *
+ * @param \WPCF7_Validation $result Validation result.
+ * @param \WPCF7_FormTag $tag Form tag.
+ * @return \WPCF7_Validation
+ */
+ public static function validate_date_checkout( $result, $tag ) {
+ $name = $tag->name;
+ $check_out = isset( $_POST[ $name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $name ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ // Try common check-in field names.
+ $check_in = '';
+ $checkin_fields = array( 'check_in', 'checkin', 'check-in', 'arrival' );
+ foreach ( $checkin_fields as $field ) {
+ if ( isset( $_POST[ $field ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $check_in = sanitize_text_field( wp_unslash( $_POST[ $field ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ break;
+ }
+ }
+
+ if ( $tag->is_required() && empty( $check_out ) ) {
+ $result->invalidate( $tag, __( 'Please select a check-out date.', 'wp-bnb' ) );
+ return $result;
+ }
+
+ if ( $check_out ) {
+ // Validate format.
+ if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $check_out ) ) {
+ $result->invalidate( $tag, __( 'Invalid date format.', 'wp-bnb' ) );
+ return $result;
+ }
+
+ // Validate check-out is after check-in.
+ if ( $check_in && strtotime( $check_out ) <= strtotime( $check_in ) ) {
+ $result->invalidate( $tag, __( 'Check-out must be after check-in.', 'wp-bnb' ) );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Validate guests field.
+ *
+ * @param \WPCF7_Validation $result Validation result.
+ * @param \WPCF7_FormTag $tag Form tag.
+ * @return \WPCF7_Validation
+ */
+ public static function validate_guests( $result, $tag ) {
+ $name = $tag->name;
+ $guests = isset( $_POST[ $name ] ) ? absint( $_POST[ $name ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ if ( $tag->is_required() && $guests < 1 ) {
+ $result->invalidate( $tag, __( 'Please enter number of guests.', 'wp-bnb' ) );
+ return $result;
+ }
+
+ // Try to get room_id for capacity validation.
+ $room_id = 0;
+ $room_fields = array( 'room', 'room_id', 'room-id' );
+ foreach ( $room_fields as $field ) {
+ if ( isset( $_POST[ $field ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $room_id = absint( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ break;
+ }
+ }
+
+ // Validate against room capacity if room is selected.
+ if ( $room_id && $guests > 0 ) {
+ $capacity = (int) get_post_meta( $room_id, '_bnb_room_capacity', true );
+ if ( $capacity > 0 && $guests > $capacity ) {
+ $result->invalidate(
+ $tag,
+ sprintf(
+ /* translators: %d: Maximum guest capacity */
+ __( 'This room has a maximum capacity of %d guests.', 'wp-bnb' ),
+ $capacity
+ )
+ );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Validate availability before sending mail.
+ *
+ * @param \WPCF7_ContactForm $contact_form Contact form object.
+ * @param bool $abort Whether to abort.
+ * @param \WPCF7_Submission $submission Submission object.
+ * @return void
+ */
+ public static function validate_availability_before_mail( $contact_form, &$abort, $submission ): void {
+ if ( ! self::is_booking_form( $contact_form ) ) {
+ return;
+ }
+
+ $posted_data = $submission->get_posted_data();
+
+ // Get room and dates.
+ $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' );
+ $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) );
+ $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) );
+
+ if ( ! $room_id || ! $check_in || ! $check_out ) {
+ return;
+ }
+
+ // Check availability.
+ if ( ! Availability::is_available( $room_id, $check_in, $check_out ) ) {
+ $abort = true;
+ $submission->set_status( 'validation_failed' );
+ $submission->set_response(
+ __( 'Sorry, this room is not available for the selected dates. Please choose different dates.', 'wp-bnb' )
+ );
+ }
+ }
+
+ /**
+ * Handle mail sent event - create booking.
+ *
+ * @param \WPCF7_ContactForm $contact_form Contact form object.
+ * @return void
+ */
+ public static function on_mail_sent( $contact_form ): void {
+ if ( ! self::is_booking_form( $contact_form ) ) {
+ return;
+ }
+
+ $submission = \WPCF7_Submission::get_instance();
+ if ( ! $submission ) {
+ return;
+ }
+
+ $posted_data = $submission->get_posted_data();
+
+ // Extract booking data.
+ $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' );
+ $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) );
+ $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) );
+
+ if ( ! $room_id || ! $check_in || ! $check_out ) {
+ return;
+ }
+
+ // Guest data.
+ $guest_name = self::get_field_value( $posted_data, array( 'your-name', 'name', 'guest_name', 'guest-name' ) );
+ $guest_email = self::get_field_value( $posted_data, array( 'your-email', 'email', 'guest_email', 'guest-email' ) );
+ $guest_phone = self::get_field_value( $posted_data, array( 'your-phone', 'phone', 'tel', 'guest_phone', 'guest-phone' ) );
+
+ // Guest counts.
+ $guests = self::get_field_value( $posted_data, array( 'guests', 'guest_count', 'guest-count' ), 'int' );
+ $adults = self::get_field_value( $posted_data, array( 'adults', 'adult_count' ), 'int' );
+ $children = self::get_field_value( $posted_data, array( 'children', 'child_count' ), 'int' );
+
+ // If single guests field, use it as adults.
+ if ( $guests && ! $adults ) {
+ $adults = $guests;
+ }
+ if ( ! $adults ) {
+ $adults = 1;
+ }
+ if ( ! $children ) {
+ $children = 0;
+ }
+
+ // Notes from message field.
+ $notes = self::get_field_value( $posted_data, array( 'your-message', 'message', 'notes', 'special_requests', 'special-requests' ) );
+
+ // Create the booking.
+ $booking_id = self::create_booking(
+ array(
+ 'room_id' => $room_id,
+ 'check_in' => $check_in,
+ 'check_out' => $check_out,
+ 'guest_name' => $guest_name,
+ 'guest_email' => $guest_email,
+ 'guest_phone' => $guest_phone,
+ 'adults' => $adults,
+ 'children' => $children,
+ 'notes' => $notes,
+ 'source' => 'cf7_form_' . $contact_form->id(),
+ )
+ );
+
+ if ( $booking_id ) {
+ // Store booking ID for potential use in mail tags.
+ $submission->add_extra_var( 'bnb_booking_id', (string) $booking_id );
+ $submission->add_extra_var( 'bnb_booking_reference', get_the_title( $booking_id ) );
+
+ /**
+ * Fires after a booking is created from a CF7 form.
+ *
+ * @param int $booking_id Created booking post ID.
+ * @param \WPCF7_ContactForm $contact_form CF7 form object.
+ * @param array $posted_data Form submission data.
+ */
+ do_action( 'wp_bnb_cf7_booking_created', $booking_id, $contact_form, $posted_data );
+ }
+ }
+
+ /**
+ * Create a booking from form data.
+ *
+ * @param array $data Booking data.
+ * @return int|false Booking post ID or false on failure.
+ */
+ private static function create_booking( array $data ) {
+ // Find or create guest.
+ $guest_id = null;
+ if ( ! empty( $data['guest_name'] ) ) {
+ $guest_id = self::find_or_create_guest(
+ $data['guest_name'],
+ $data['guest_email'] ?? '',
+ $data['guest_phone'] ?? ''
+ );
+ }
+
+ // Calculate price.
+ $price = 0;
+ $breakdown = array();
+ if ( $data['room_id'] && $data['check_in'] && $data['check_out'] ) {
+ try {
+ $calculator = new Calculator( $data['room_id'], $data['check_in'], $data['check_out'] );
+ $price = $calculator->calculate();
+ $breakdown = $calculator->getBreakdown();
+ } catch ( \Exception $e ) {
+ // Price calculation failed, continue without price.
+ }
+ }
+
+ // Generate booking reference.
+ $reference = Booking::generate_reference();
+
+ // Create booking post.
+ $booking_id = wp_insert_post(
+ array(
+ 'post_type' => Booking::POST_TYPE,
+ 'post_status' => 'publish',
+ 'post_title' => $reference,
+ )
+ );
+
+ if ( is_wp_error( $booking_id ) || ! $booking_id ) {
+ return false;
+ }
+
+ // Save booking meta.
+ update_post_meta( $booking_id, self::META_PREFIX . 'room_id', $data['room_id'] );
+ update_post_meta( $booking_id, self::META_PREFIX . 'check_in', $data['check_in'] );
+ update_post_meta( $booking_id, self::META_PREFIX . 'check_out', $data['check_out'] );
+ update_post_meta( $booking_id, self::META_PREFIX . 'status', 'pending' );
+ update_post_meta( $booking_id, self::META_PREFIX . 'guest_name', $data['guest_name'] ?? '' );
+ update_post_meta( $booking_id, self::META_PREFIX . 'guest_email', $data['guest_email'] ?? '' );
+ update_post_meta( $booking_id, self::META_PREFIX . 'guest_phone', $data['guest_phone'] ?? '' );
+ update_post_meta( $booking_id, self::META_PREFIX . 'adults', $data['adults'] ?? 1 );
+ update_post_meta( $booking_id, self::META_PREFIX . 'children', $data['children'] ?? 0 );
+ update_post_meta( $booking_id, self::META_PREFIX . 'guest_notes', $data['notes'] ?? '' );
+ update_post_meta( $booking_id, self::META_PREFIX . 'calculated_price', $price );
+ update_post_meta( $booking_id, self::META_PREFIX . 'price_breakdown', $breakdown );
+ update_post_meta( $booking_id, self::META_PREFIX . 'source', $data['source'] ?? 'cf7' );
+
+ if ( $guest_id ) {
+ update_post_meta( $booking_id, self::META_PREFIX . 'guest_id', $guest_id );
+ }
+
+ // Generate comprehensive title (Guest Name (dates)).
+ self::update_booking_title( $booking_id, $data );
+
+ // Trigger the booking status changed action (for email notifications).
+ do_action( 'wp_bnb_booking_status_changed', $booking_id, '', 'pending' );
+
+ return $booking_id;
+ }
+
+ /**
+ * Update booking title with guest name and dates.
+ *
+ * @param int $booking_id Booking post ID.
+ * @param array $data Booking data.
+ * @return void
+ */
+ private static function update_booking_title( int $booking_id, array $data ): void {
+ $guest_name = $data['guest_name'] ?? __( 'Unknown Guest', 'wp-bnb' );
+
+ // Format dates.
+ $date_part = '';
+ if ( ! empty( $data['check_in'] ) && ! empty( $data['check_out'] ) ) {
+ $check_in_date = \DateTime::createFromFormat( 'Y-m-d', $data['check_in'] );
+ $check_out_date = \DateTime::createFromFormat( 'Y-m-d', $data['check_out'] );
+
+ if ( $check_in_date && $check_out_date ) {
+ if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) {
+ $date_part = sprintf(
+ '%s - %s',
+ $check_in_date->format( 'd.m' ),
+ $check_out_date->format( 'd.m.Y' )
+ );
+ } else {
+ $date_part = sprintf(
+ '%s - %s',
+ $check_in_date->format( 'd.m.Y' ),
+ $check_out_date->format( 'd.m.Y' )
+ );
+ }
+ }
+ }
+
+ $title = $guest_name;
+ if ( $date_part ) {
+ $title .= sprintf( ' (%s)', $date_part );
+ }
+
+ // Update the post title directly.
+ global $wpdb;
+ $wpdb->update(
+ $wpdb->posts,
+ array( 'post_title' => $title ),
+ array( 'ID' => $booking_id ),
+ array( '%s' ),
+ array( '%d' )
+ );
+
+ clean_post_cache( $booking_id );
+ }
+
+ /**
+ * Find an existing guest by email or create a new one.
+ *
+ * @param string $name Guest full name.
+ * @param string $email Guest email.
+ * @param string $phone Guest phone (optional).
+ * @return int|null Guest post ID or null on failure.
+ */
+ private static function find_or_create_guest( string $name, string $email, string $phone = '' ): ?int {
+ if ( empty( $name ) ) {
+ return null;
+ }
+
+ // Try to find existing guest by email.
+ if ( ! empty( $email ) ) {
+ $existing_guest = Guest::get_by_email( $email );
+ if ( $existing_guest ) {
+ return $existing_guest->ID;
+ }
+ }
+
+ // Parse name into first/last name.
+ $name_parts = explode( ' ', trim( $name ), 2 );
+ $first_name = $name_parts[0] ?? '';
+ $last_name = $name_parts[1] ?? '';
+
+ // Create new guest post.
+ $guest_id = wp_insert_post(
+ array(
+ 'post_type' => Guest::POST_TYPE,
+ 'post_status' => 'publish',
+ 'post_title' => $name,
+ )
+ );
+
+ if ( is_wp_error( $guest_id ) || ! $guest_id ) {
+ return null;
+ }
+
+ // Save guest meta.
+ update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name );
+ update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name );
+
+ if ( ! empty( $email ) ) {
+ update_post_meta( $guest_id, '_bnb_guest_email', $email );
+ }
+
+ if ( ! empty( $phone ) ) {
+ update_post_meta( $guest_id, '_bnb_guest_phone', $phone );
+ }
+
+ // Set default status.
+ update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
+
+ return $guest_id;
+ }
+
+ /**
+ * Check if a contact form is a booking form.
+ *
+ * @param \WPCF7_ContactForm $contact_form Contact form object.
+ * @return bool
+ */
+ private static function is_booking_form( $contact_form ): bool {
+ // Check for CSS class.
+ $additional_settings = $contact_form->additional_setting( 'class', false );
+ if ( is_array( $additional_settings ) ) {
+ foreach ( $additional_settings as $setting ) {
+ if ( strpos( $setting, self::BOOKING_FORM_CLASS ) !== false ) {
+ return true;
+ }
+ }
+ }
+
+ // Auto-detect by checking for required booking fields.
+ $form_content = $contact_form->prop( 'form' );
+ $has_room = strpos( $form_content, '[bnb_room_select' ) !== false;
+ $has_checkin = strpos( $form_content, '[bnb_date_checkin' ) !== false;
+ $has_checkout = strpos( $form_content, '[bnb_date_checkout' ) !== false;
+
+ return $has_room && $has_checkin && $has_checkout;
+ }
+
+ /**
+ * Get field value from posted data.
+ *
+ * @param array $posted_data Posted form data.
+ * @param array $field_names Possible field names.
+ * @param string $type Type cast ('int', 'string').
+ * @return mixed
+ */
+ private static function get_field_value( array $posted_data, array $field_names, string $type = 'string' ) {
+ foreach ( $field_names as $field ) {
+ if ( isset( $posted_data[ $field ] ) && '' !== $posted_data[ $field ] ) {
+ $value = is_array( $posted_data[ $field ] )
+ ? $posted_data[ $field ][0]
+ : $posted_data[ $field ];
+
+ if ( 'int' === $type ) {
+ return absint( $value );
+ }
+
+ return sanitize_text_field( $value );
+ }
+ }
+
+ return 'int' === $type ? 0 : '';
+ }
+
+ /**
+ * Handle custom mail tags.
+ *
+ * @param string|null $output Output value.
+ * @param string $name Tag name.
+ * @param bool $html Whether HTML is allowed.
+ * @param array $mail_tag Mail tag data.
+ * @return string|null
+ */
+ public static function custom_mail_tags( $output, $name, $html, $mail_tag ) {
+ $submission = \WPCF7_Submission::get_instance();
+ if ( ! $submission ) {
+ return $output;
+ }
+
+ switch ( $name ) {
+ case '_bnb_booking_reference':
+ return $submission->get_extra_var( 'bnb_booking_reference' ) ?: '';
+
+ case '_bnb_booking_id':
+ return $submission->get_extra_var( 'bnb_booking_id' ) ?: '';
+
+ case '_bnb_room_name':
+ $posted_data = $submission->get_posted_data();
+ $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' );
+ if ( $room_id ) {
+ $room = get_post( $room_id );
+ return $room ? $room->post_title : '';
+ }
+ return '';
+
+ case '_bnb_calculated_price':
+ $posted_data = $submission->get_posted_data();
+ $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' );
+ $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) );
+ $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) );
+
+ if ( $room_id && $check_in && $check_out ) {
+ try {
+ $calculator = new Calculator( $room_id, $check_in, $check_out );
+ $price = $calculator->calculate();
+ return Calculator::formatPrice( $price );
+ } catch ( \Exception $e ) {
+ return '';
+ }
+ }
+ return '';
+
+ case '_bnb_nights':
+ $posted_data = $submission->get_posted_data();
+ $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) );
+ $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) );
+
+ if ( $check_in && $check_out ) {
+ $nights = Booking::calculate_nights( $check_in, $check_out );
+ return (string) $nights;
+ }
+ return '';
+ }
+
+ return $output;
+ }
+}
diff --git a/src/Plugin.php b/src/Plugin.php
index 144e132..15c29f6 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -16,6 +16,7 @@ use Magdev\WpBnb\Booking\Availability;
use Magdev\WpBnb\Booking\EmailNotifier;
use Magdev\WpBnb\Frontend\Search;
use Magdev\WpBnb\Frontend\Shortcodes;
+use Magdev\WpBnb\Integration\CF7;
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
@@ -202,6 +203,11 @@ final class Plugin {
// Register widgets.
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
+
+ // Initialize Contact Form 7 integration if CF7 is active.
+ if ( class_exists( 'WPCF7' ) ) {
+ CF7::init();
+ }
}
/**
@@ -362,6 +368,43 @@ final class Plugin {
),
)
);
+
+ // Load CF7 integration assets if CF7 is active.
+ if ( class_exists( 'WPCF7' ) ) {
+ wp_enqueue_style(
+ 'wp-bnb-cf7',
+ WP_BNB_URL . 'assets/css/cf7-integration.css',
+ array( 'contact-form-7' ),
+ WP_BNB_VERSION
+ );
+
+ wp_enqueue_script(
+ 'wp-bnb-cf7',
+ WP_BNB_URL . 'assets/js/cf7-integration.js',
+ array( 'contact-form-7' ),
+ WP_BNB_VERSION,
+ true
+ );
+
+ wp_localize_script(
+ 'wp-bnb-cf7',
+ 'wpBnbCF7',
+ array(
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'wp_bnb_frontend_nonce' ),
+ 'i18n' => array(
+ 'selectRoom' => __( '-- Select Room --', 'wp-bnb' ),
+ 'checking' => __( 'Checking availability...', 'wp-bnb' ),
+ 'available' => __( 'Room is available!', 'wp-bnb' ),
+ 'unavailable' => __( 'Room is not available for these dates', 'wp-bnb' ),
+ 'invalidDateRange' => __( 'Check-out must be after check-in', 'wp-bnb' ),
+ 'capacityExceeded' => __( 'Maximum %d guests for this room', 'wp-bnb' ),
+ 'estimatedTotal' => __( 'Estimated Total', 'wp-bnb' ),
+ 'nights' => __( 'nights', 'wp-bnb' ),
+ ),
+ )
+ );
+ }
}
/**
diff --git a/wp-bnb.php b/wp-bnb.php
index 3b391e9..07d6b3a 100644
--- a/wp-bnb.php
+++ b/wp-bnb.php
@@ -3,7 +3,7 @@
* Plugin Name: WP BnB Management
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
- * Version: 0.6.1
+ * Version: 0.7.0
* Requires at least: 6.0
* Requires PHP: 8.3
* Author: Marco Graetsch
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
// Plugin version constant - MUST match Version in header above.
-define( 'WP_BNB_VERSION', '0.6.1' );
+define( 'WP_BNB_VERSION', '0.7.0' );
// Plugin path constants.
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );