diff --git a/CHANGELOG.md b/CHANGELOG.md index 800929a..0b63cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ 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.9.0] - 2026-02-03 + +### Added + +- Prometheus Metrics Integration: + - New `src/Integration/Prometheus.php` class for metrics collection + - Integration with wp-prometheus plugin via `wp_prometheus_collect_metrics` hook + - Inventory metrics: buildings total, rooms by status, services by status + - Booking metrics: bookings by status, check-ins/check-outs today, upcoming 7 days, avg duration + - Guest metrics: total guests, guests by status, repeat guests, new guests this month + - Occupancy metrics: current rate, monthly rate, occupied rooms, total bed capacity + - Revenue metrics: this month, YTD, average booking value, services revenue +- Grafana Dashboard: + - Pre-configured dashboard at `assets/grafana/wp-bnb-dashboard.json` + - Automatic registration with wp-prometheus dashboard provider + - Occupancy gauges with color-coded thresholds + - Pie charts for bookings, rooms, and guests by status + - Revenue and guest statistics panels + - Responsive grid layout with 24 panels +- Settings page Metrics tab: + - Enable/disable metrics collection toggle + - WP Prometheus detection with status indicator + - Complete metrics reference table + - Dashboard file location and export info + +### Changed + +- Plugin.php updated to initialize Prometheus integration +- Settings page now has six tabs: General, Pricing, License, Updates, Metrics + ## [0.8.0] - 2026-02-03 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index e794c8f..e4f209e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -935,3 +935,60 @@ Admin features always work; frontend requires valid license. - CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility - Guest data aggregation from bookings uses unique key pattern for anonymous guests - Occupancy calculation: (booked nights / total room nights) * 100 + +### 2026-02-03 - Version 0.9.0 (Prometheus Metrics) + +**Completed:** + +- Created `src/Integration/Prometheus.php` class (~700 lines) + - Integration with wp-prometheus via `wp_prometheus_collect_metrics` hook + - Dashboard registration via `wp_prometheus_register_dashboards` hook + - Option to enable/disable metrics collection + - Inventory metrics: buildings total, rooms by status, services by status + - Booking metrics: by status, check-ins/outs today, upcoming 7 days, avg duration + - Guest metrics: total, by status, repeat guests, new this month + - Occupancy metrics: current rate, monthly rate, occupied rooms, bed capacity + - Revenue metrics: this month, YTD, avg booking value, services revenue + - Optimized SQL queries using `$wpdb->prepare()` throughout +- Created `assets/grafana/wp-bnb-dashboard.json` Grafana dashboard + - 24 panels with responsive grid layout + - Occupancy gauges with color-coded thresholds (red < 30%, orange < 50%, yellow < 70%, green ≥ 70%) + - Pie charts for bookings, rooms, and guests by status + - Revenue stat panels (this month, YTD, avg value, services) + - Guest stat panels (total, new, repeat, active services) + - Today's activity panels (check-ins, check-outs, upcoming) + - Prometheus datasource variable for flexibility + - Auto-refresh every 5 minutes +- Updated `src/Plugin.php` + - Added Prometheus class import + - Initialized Prometheus integration in `init_components()` + - Added "Metrics" tab to settings page (6 tabs total) + - Added `render_metrics_settings()` method with WP Prometheus detection + - Added `save_metrics_settings()` method + - Metrics reference table showing all available metrics +- Updated version to 0.9.0 + +**Files Created:** + +- `src/Integration/Prometheus.php` - Prometheus metrics integration class +- `assets/grafana/wp-bnb-dashboard.json` - Pre-configured Grafana dashboard + +**Files Changed:** + +- `src/Plugin.php` - Prometheus initialization, metrics settings tab +- `wp-bnb.php` - Version bump to 0.9.0 (header and constant) +- `CHANGELOG.md` - Added v0.9.0 release notes +- `PLAN.md` - Marked Phase 9 as complete +- `README.md` - Added Prometheus metrics documentation + +**Learnings:** + +- wp-prometheus uses `wp_prometheus_collect_metrics` action with collector object +- Collector provides `register_gauge()` for fluctuating values +- Labels are passed as array to `register_gauge()`, values to `set()` +- Grafana dashboard JSON requires proper panel IDs and grid positions +- Occupancy queries need careful date range handling for month boundaries +- Revenue queries use `DECIMAL(10,2)` casting for accurate sums +- Metrics should be cached or computed efficiently as they're scraped frequently +- Dashboard registration requires file path, title, description, icon, and plugin name +- Settings tab detection uses `$prometheus_active` to show WP Prometheus status diff --git a/PLAN.md b/PLAN.md index 70e6f20..ef1f2db 100644 --- a/PLAN.md +++ b/PLAN.md @@ -180,15 +180,23 @@ This document outlines the implementation plan for the WP BnB Management plugin. - [x] Guest statistics - [x] Export functionality (CSV, PDF) -## Phase 9: Prometheus Metrics (v0.9.0) +## Phase 9: Prometheus Metrics (v0.9.0) - Complete -- [ ] Meanigful Metrics for this Plugin, see for implementation details -- [ ] Example Grafana-Dashboard, see for implementation details -- [ ] Update settings page to enable/disable metrics +- [x] Meaningful Metrics for this Plugin: + - Inventory: buildings, rooms by status, services by status + - Bookings: by status, check-ins/check-outs today, upcoming, avg duration + - Guests: total, by status, repeat guests, new this month + - Occupancy: current rate, monthly rate, occupied rooms, bed capacity + - Revenue: this month, YTD, average booking value, services revenue +- [x] Example Grafana Dashboard: + - Pre-configured dashboard JSON at `assets/grafana/wp-bnb-dashboard.json` + - Automatic registration with wp-prometheus + - 24 panels with gauges, pie charts, and stat displays +- [x] Update settings page to enable/disable metrics ## Phase 10: Security Audit (v0.10.0) -- [ ] Check for Wordpress best-practises +- [ ] Check for Wordpress best-practices - [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads ## Future Considerations (v1.0.0+) @@ -308,6 +316,6 @@ The plugin will provide extensive hooks for customization: | 0.6.0 | Frontend | Complete | | 0.7.0 | CF7 Integration | Complete | | 0.8.0 | Dashboard | Complete | -| 0.9.0 | Prometheus Metrics | TBD | +| 0.9.0 | Prometheus Metrics | Complete | | 0.10.0 | Security Audit | TBD | | 1.0.0 | Stable Release | TBD | diff --git a/README.md b/README.md index 43ec4b3..37f0099 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ WP BnB Management enables WordPress to act as a full management system for B&B h - **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms - **Dashboard**: Comprehensive admin dashboard with statistics and charts - **Reports**: Detailed reports with CSV and PDF export +- **Prometheus Metrics**: Expose operational metrics for monitoring with Grafana ### Requirements @@ -383,6 +384,66 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) { } ); ``` +## Prometheus Metrics + +The plugin integrates with [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) to expose operational metrics for monitoring with Prometheus and Grafana. + +### Enabling Metrics + +1. Install and activate the WP Prometheus plugin +2. Navigate to **WP BnB → Settings → Metrics** +3. Enable "Expose BnB metrics via Prometheus" +4. Metrics will be available at your site's `/metrics/` endpoint + +### Available Metrics + +**Inventory Metrics:** + +- `wp_bnb_buildings_total` - Total number of buildings +- `wp_bnb_rooms_total{status}` - Rooms by status (available, occupied, maintenance, inactive) +- `wp_bnb_services_total{status}` - Services by status (active, inactive) +- `wp_bnb_total_capacity_beds` - Total bed capacity across all rooms + +**Booking Metrics:** + +- `wp_bnb_bookings_total{status}` - Bookings by status (pending, confirmed, checked_in, checked_out, cancelled) +- `wp_bnb_checkins_today` - Check-ins scheduled for today +- `wp_bnb_checkouts_today` - Check-outs scheduled for today +- `wp_bnb_bookings_upcoming_7days` - Bookings starting in next 7 days +- `wp_bnb_booking_avg_duration_nights` - Average booking duration + +**Occupancy Metrics:** + +- `wp_bnb_occupancy_rate_current` - Current room occupancy rate (percentage) +- `wp_bnb_occupancy_rate_this_month` - Monthly occupancy rate (percentage) +- `wp_bnb_rooms_currently_occupied` - Rooms currently occupied + +**Revenue Metrics:** + +- `wp_bnb_revenue_this_month{currency}` - Revenue for current month +- `wp_bnb_revenue_ytd{currency}` - Revenue year to date +- `wp_bnb_booking_avg_value{currency}` - Average booking value +- `wp_bnb_services_revenue_this_month{currency}` - Services revenue this month + +**Guest Metrics:** + +- `wp_bnb_guests_total` - Total registered guests +- `wp_bnb_guests_by_status{status}` - Guests by status (active, blocked, vip) +- `wp_bnb_guests_repeat` - Guests with more than one booking +- `wp_bnb_guests_new_this_month` - New guests this month + +### Grafana Dashboard + +A pre-configured Grafana dashboard is included at `assets/grafana/wp-bnb-dashboard.json`. If WP Prometheus is installed, the dashboard is automatically registered and available for export. + +The dashboard includes: + +- Occupancy gauges with color-coded thresholds +- Bookings, rooms, and guests pie charts by status +- Revenue and guest statistics panels +- Today's check-ins/check-outs +- Trend indicators + ## Frequently Asked Questions ### Do I need a license to use this plugin? diff --git a/assets/grafana/wp-bnb-dashboard.json b/assets/grafana/wp-bnb-dashboard.json new file mode 100644 index 0000000..d85781a --- /dev/null +++ b/assets/grafana/wp-bnb-dashboard.json @@ -0,0 +1,1580 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "WP BnB Manager - Monitor occupancy, bookings, revenue, and guest statistics for your Bed & Breakfast", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "Occupancy Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 30 + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 70 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_occupancy_rate_current", + "refId": "A" + } + ], + "title": "Current Occupancy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 30 + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 70 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 3, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_occupancy_rate_this_month", + "refId": "A" + } + ], + "title": "Monthly Occupancy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 12, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_rooms_currently_occupied", + "refId": "A" + } + ], + "title": "Rooms Occupied", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 15, + "y": 1 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_rooms_total{status=\"available\"}", + "refId": "A" + } + ], + "title": "Rooms Available", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 18, + "y": 1 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_total_capacity_beds", + "refId": "A" + } + ], + "title": "Total Bed Capacity", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "orange", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 21, + "y": 1 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_buildings_total", + "refId": "A" + } + ], + "title": "Buildings", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 12, + "y": 4 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_checkins_today", + "refId": "A" + } + ], + "title": "Check-ins Today", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 15, + "y": 4 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_checkouts_today", + "refId": "A" + } + ], + "title": "Check-outs Today", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "yellow", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 18, + "y": 4 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_bookings_upcoming_7days", + "refId": "A" + } + ], + "title": "Upcoming (7 days)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "semi-dark-blue", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 21, + "y": 4 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_booking_avg_duration_nights", + "refId": "A" + } + ], + "title": "Avg Stay (nights)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 12, + "panels": [], + "title": "Bookings", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "pending" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "confirmed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "checked_in" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "checked_out" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cancelled" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 13, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_bookings_total", + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Bookings by Status", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "available" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "occupied" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "maintenance" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 14, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_rooms_total", + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Rooms by Status", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "blocked" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vip" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 15, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_guests_by_status", + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Guests by Status", + "type": "piechart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 16, + "panels": [], + "title": "Revenue & Guests", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 17 + }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_revenue_this_month", + "refId": "A" + } + ], + "title": "Revenue This Month", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 17 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_revenue_ytd", + "refId": "A" + } + ], + "title": "Revenue Year to Date", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 17 + }, + "id": 19, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_booking_avg_value", + "refId": "A" + } + ], + "title": "Avg Booking Value", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "orange", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 17 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_services_revenue_this_month", + "refId": "A" + } + ], + "title": "Services Revenue", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 21 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_guests_total", + "refId": "A" + } + ], + "title": "Total Guests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 21 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_guests_new_this_month", + "refId": "A" + } + ], + "title": "New Guests This Month", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "yellow", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 21 + }, + "id": 23, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_guests_repeat", + "refId": "A" + } + ], + "title": "Repeat Guests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "semi-dark-green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 21 + }, + "id": 24, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "wp_bnb_services_total{status=\"active\"}", + "refId": "A" + } + ], + "title": "Active Services", + "type": "stat" + } + ], + "refresh": "5m", + "schemaVersion": 38, + "tags": [ + "wp-bnb", + "wordpress", + "hospitality" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "WP BnB Dashboard", + "uid": "wp-bnb-dashboard", + "version": 1, + "weekStart": "" +} diff --git a/src/Integration/Prometheus.php b/src/Integration/Prometheus.php new file mode 100644 index 0000000..85e150f --- /dev/null +++ b/src/Integration/Prometheus.php @@ -0,0 +1,877 @@ +register_gauge( + 'wp_bnb_buildings_total', + 'Total number of buildings', + array() + ); + $gauge->set( (int) $buildings_total->publish, array() ); + + // Rooms by status. + $rooms_gauge = $collector->register_gauge( + 'wp_bnb_rooms_total', + 'Total number of rooms by status', + array( 'status' ) + ); + + $room_statuses = array( 'available', 'occupied', 'maintenance', 'inactive' ); + foreach ( $room_statuses as $status ) { + $count = self::count_rooms_by_status( $status ); + $rooms_gauge->set( $count, array( $status ) ); + } + + // Services by status. + $services_gauge = $collector->register_gauge( + 'wp_bnb_services_total', + 'Total number of services by status', + array( 'status' ) + ); + + $service_statuses = array( 'active', 'inactive' ); + foreach ( $service_statuses as $status ) { + $count = self::count_services_by_status( $status ); + $services_gauge->set( $count, array( $status ) ); + } + } + + /** + * Collect booking metrics. + * + * @param object $collector The wp-prometheus collector instance. + * @return void + */ + private static function collect_booking_metrics( $collector ): void { + // Bookings by status. + $bookings_gauge = $collector->register_gauge( + 'wp_bnb_bookings_total', + 'Total number of bookings by status', + array( 'status' ) + ); + + $booking_statuses = array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' ); + foreach ( $booking_statuses as $status ) { + $count = self::count_bookings_by_status( $status ); + $bookings_gauge->set( $count, array( $status ) ); + } + + // Today's check-ins. + $checkins_gauge = $collector->register_gauge( + 'wp_bnb_checkins_today', + 'Number of check-ins scheduled for today', + array() + ); + $checkins_gauge->set( self::count_todays_checkins(), array() ); + + // Today's check-outs. + $checkouts_gauge = $collector->register_gauge( + 'wp_bnb_checkouts_today', + 'Number of check-outs scheduled for today', + array() + ); + $checkouts_gauge->set( self::count_todays_checkouts(), array() ); + + // Upcoming bookings (next 7 days). + $upcoming_gauge = $collector->register_gauge( + 'wp_bnb_bookings_upcoming_7days', + 'Number of bookings starting in the next 7 days', + array() + ); + $upcoming_gauge->set( self::count_upcoming_bookings( 7 ), array() ); + + // Average booking duration (nights). + $avg_duration = $collector->register_gauge( + 'wp_bnb_booking_avg_duration_nights', + 'Average booking duration in nights', + array() + ); + $avg_duration->set( self::get_average_booking_duration(), array() ); + } + + /** + * Collect guest metrics. + * + * @param object $collector The wp-prometheus collector instance. + * @return void + */ + private static function collect_guest_metrics( $collector ): void { + // Total guests. + $guests_total = wp_count_posts( Guest::POST_TYPE ); + $guests_gauge = $collector->register_gauge( + 'wp_bnb_guests_total', + 'Total number of registered guests', + array() + ); + $guests_gauge->set( (int) $guests_total->publish, array() ); + + // Guests by status. + $guests_status_gauge = $collector->register_gauge( + 'wp_bnb_guests_by_status', + 'Number of guests by status', + array( 'status' ) + ); + + $guest_statuses = array( 'active', 'blocked', 'vip' ); + foreach ( $guest_statuses as $status ) { + $count = self::count_guests_by_status( $status ); + $guests_status_gauge->set( $count, array( $status ) ); + } + + // Repeat guests (guests with more than one booking). + $repeat_gauge = $collector->register_gauge( + 'wp_bnb_guests_repeat', + 'Number of guests with more than one booking', + array() + ); + $repeat_gauge->set( self::count_repeat_guests(), array() ); + + // New guests this month. + $new_guests_gauge = $collector->register_gauge( + 'wp_bnb_guests_new_this_month', + 'Number of new guests registered this month', + array() + ); + $new_guests_gauge->set( self::count_new_guests_this_month(), array() ); + } + + /** + * Collect occupancy metrics. + * + * @param object $collector The wp-prometheus collector instance. + * @return void + */ + private static function collect_occupancy_metrics( $collector ): void { + // Current occupancy rate (percentage). + $occupancy_gauge = $collector->register_gauge( + 'wp_bnb_occupancy_rate_current', + 'Current room occupancy rate (percentage)', + array() + ); + $occupancy_gauge->set( self::get_current_occupancy_rate(), array() ); + + // Occupancy rate this month. + $occupancy_month_gauge = $collector->register_gauge( + 'wp_bnb_occupancy_rate_this_month', + 'Room occupancy rate for the current month (percentage)', + array() + ); + $occupancy_month_gauge->set( self::get_monthly_occupancy_rate(), array() ); + + // Rooms currently occupied. + $occupied_gauge = $collector->register_gauge( + 'wp_bnb_rooms_currently_occupied', + 'Number of rooms currently occupied', + array() + ); + $occupied_gauge->set( self::count_currently_occupied_rooms(), array() ); + + // Total room capacity (beds). + $capacity_gauge = $collector->register_gauge( + 'wp_bnb_total_capacity_beds', + 'Total bed capacity across all rooms', + array() + ); + $capacity_gauge->set( self::get_total_bed_capacity(), array() ); + } + + /** + * Collect revenue metrics. + * + * @param object $collector The wp-prometheus collector instance. + * @return void + */ + private static function collect_revenue_metrics( $collector ): void { + $currency = get_option( 'wp_bnb_currency', 'CHF' ); + + // Revenue this month. + $revenue_month_gauge = $collector->register_gauge( + 'wp_bnb_revenue_this_month', + 'Total revenue for the current month', + array( 'currency' ) + ); + $revenue_month_gauge->set( self::get_revenue_this_month(), array( $currency ) ); + + // Revenue year to date. + $revenue_ytd_gauge = $collector->register_gauge( + 'wp_bnb_revenue_ytd', + 'Total revenue year to date', + array( 'currency' ) + ); + $revenue_ytd_gauge->set( self::get_revenue_ytd(), array( $currency ) ); + + // Average booking value. + $avg_value_gauge = $collector->register_gauge( + 'wp_bnb_booking_avg_value', + 'Average booking value', + array( 'currency' ) + ); + $avg_value_gauge->set( self::get_average_booking_value(), array( $currency ) ); + + // Revenue from services this month. + $services_revenue_gauge = $collector->register_gauge( + 'wp_bnb_services_revenue_this_month', + 'Revenue from additional services this month', + array( 'currency' ) + ); + $services_revenue_gauge->set( self::get_services_revenue_this_month(), array( $currency ) ); + } + + /** + * Register Grafana dashboards. + * + * @param object $provider The wp-prometheus dashboard provider instance. + * @return void + */ + public static function register_dashboards( $provider ): void { + $dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json'; + + if ( file_exists( $dashboard_file ) ) { + $provider->register_dashboard( + 'wp-bnb', + array( + 'title' => __( 'WP BnB Dashboard', 'wp-bnb' ), + 'description' => __( 'Monitor occupancy, bookings, revenue, and guest statistics for your B&B.', 'wp-bnb' ), + 'icon' => 'dashicons-building', + 'file' => $dashboard_file, + 'plugin' => 'WP BnB Manager', + ) + ); + } + } + + // ========================================================================= + // Helper Methods - Inventory + // ========================================================================= + + /** + * Count rooms by status. + * + * @param string $status Room status. + * @return int + */ + private static function count_rooms_by_status( string $status ): int { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_bnb_room_status' + AND pm.meta_value = %s", + Room::POST_TYPE, + $status + ) + ); + } + + /** + * Count services by status. + * + * @param string $status Service status. + * @return int + */ + private static function count_services_by_status( string $status ): int { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_bnb_service_status' + AND pm.meta_value = %s", + Service::POST_TYPE, + $status + ) + ); + } + + // ========================================================================= + // Helper Methods - Bookings + // ========================================================================= + + /** + * Count bookings by status. + * + * @param string $status Booking status. + * @return int + */ + private static function count_bookings_by_status( string $status ): int { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_bnb_booking_status' + AND pm.meta_value = %s", + Booking::POST_TYPE, + $status + ) + ); + } + + /** + * Count today's check-ins. + * + * @return int + */ + private static function count_todays_checkins(): int { + global $wpdb; + $today = gmdate( 'Y-m-d' ); + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_date.meta_key = '_bnb_booking_check_in' + AND pm_date.meta_value = %s + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'pending')", + Booking::POST_TYPE, + $today + ) + ); + } + + /** + * Count today's check-outs. + * + * @return int + */ + private static function count_todays_checkouts(): int { + global $wpdb; + $today = gmdate( 'Y-m-d' ); + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_date.meta_key = '_bnb_booking_check_out' + AND pm_date.meta_value = %s + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value = 'checked_in'", + Booking::POST_TYPE, + $today + ) + ); + } + + /** + * Count upcoming bookings within given days. + * + * @param int $days Number of days to look ahead. + * @return int + */ + private static function count_upcoming_bookings( int $days ): int { + global $wpdb; + $today = gmdate( 'Y-m-d' ); + $end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) ); + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_date.meta_key = '_bnb_booking_check_in' + AND pm_date.meta_value >= %s + AND pm_date.meta_value <= %s + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'pending')", + Booking::POST_TYPE, + $today, + $end_date + ) + ); + } + + /** + * Get average booking duration in nights. + * + * @return float + */ + private static function get_average_booking_duration(): float { + global $wpdb; + + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT AVG(DATEDIFF(pm_out.meta_value, pm_in.meta_value)) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id + INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_in.meta_key = '_bnb_booking_check_in' + AND pm_out.meta_key = '_bnb_booking_check_out' + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value NOT IN ('cancelled')", + Booking::POST_TYPE + ) + ); + + return round( (float) $result, 1 ); + } + + // ========================================================================= + // Helper Methods - Guests + // ========================================================================= + + /** + * Count guests by status. + * + * @param string $status Guest status. + * @return int + */ + private static function count_guests_by_status( string $status ): int { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_bnb_guest_status' + AND pm.meta_value = %s", + Guest::POST_TYPE, + $status + ) + ); + } + + /** + * Count repeat guests (more than one booking). + * + * @return int + */ + private static function count_repeat_guests(): int { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT pm.meta_value) + FROM {$wpdb->postmeta} pm + INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_bnb_booking_guest_id' + AND pm.meta_value != '' + GROUP BY pm.meta_value + HAVING COUNT(*) > 1", + Booking::POST_TYPE + ) + ); + } + + /** + * Count new guests registered this month. + * + * @return int + */ + private static function count_new_guests_this_month(): int { + global $wpdb; + $first_of_month = gmdate( 'Y-m-01 00:00:00' ); + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) + FROM {$wpdb->posts} + WHERE post_type = %s + AND post_status = 'publish' + AND post_date >= %s", + Guest::POST_TYPE, + $first_of_month + ) + ); + } + + // ========================================================================= + // Helper Methods - Occupancy + // ========================================================================= + + /** + * Get current occupancy rate (percentage of rooms occupied today). + * + * @return float + */ + private static function get_current_occupancy_rate(): float { + $total_rooms = self::count_available_rooms(); + if ( $total_rooms <= 0 ) { + return 0.0; + } + + $occupied = self::count_currently_occupied_rooms(); + return round( ( $occupied / $total_rooms ) * 100, 1 ); + } + + /** + * Get monthly occupancy rate. + * + * @return float + */ + private static function get_monthly_occupancy_rate(): float { + global $wpdb; + + $total_rooms = self::count_available_rooms(); + if ( $total_rooms <= 0 ) { + return 0.0; + } + + $first_of_month = gmdate( 'Y-m-01' ); + $today = gmdate( 'Y-m-d' ); + $days_so_far = (int) gmdate( 'd' ); + $total_room_nights = $total_rooms * $days_so_far; + + // Count booked nights this month (simplified: count bookings that overlap with this month). + $booked_nights = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( + DATEDIFF( + LEAST(pm_out.meta_value, %s), + GREATEST(pm_in.meta_value, %s) + ) + ) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id + INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_in.meta_key = '_bnb_booking_check_in' + AND pm_out.meta_key = '_bnb_booking_check_out' + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out') + AND pm_in.meta_value <= %s + AND pm_out.meta_value >= %s", + $today, + $first_of_month, + Booking::POST_TYPE, + $today, + $first_of_month + ) + ); + + if ( $total_room_nights <= 0 ) { + return 0.0; + } + + return round( ( $booked_nights / $total_room_nights ) * 100, 1 ); + } + + /** + * Count currently occupied rooms. + * + * @return int + */ + private static function count_currently_occupied_rooms(): int { + global $wpdb; + $today = gmdate( 'Y-m-d' ); + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT pm_room.meta_value) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_room ON p.ID = pm_room.post_id + INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id + INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_room.meta_key = '_bnb_booking_room_id' + AND pm_in.meta_key = '_bnb_booking_check_in' + AND pm_out.meta_key = '_bnb_booking_check_out' + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'checked_in') + AND pm_in.meta_value <= %s + AND pm_out.meta_value > %s", + Booking::POST_TYPE, + $today, + $today + ) + ); + } + + /** + * Count available rooms (not in maintenance/inactive). + * + * @return int + */ + private static function count_available_rooms(): int { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT p.ID) + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_bnb_room_status' + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND (pm.meta_value IS NULL OR pm.meta_value IN ('available', 'occupied'))", + Room::POST_TYPE + ) + ); + } + + /** + * Get total bed capacity. + * + * @return int + */ + private static function get_total_bed_capacity(): int { + global $wpdb; + + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM(CAST(pm.meta_value AS UNSIGNED)) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_bnb_room_beds'", + Room::POST_TYPE + ) + ); + + return (int) $result; + } + + // ========================================================================= + // Helper Methods - Revenue + // ========================================================================= + + /** + * Get revenue for the current month. + * + * @return float + */ + private static function get_revenue_this_month(): float { + global $wpdb; + $first_of_month = gmdate( 'Y-m-01' ); + + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2))) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id + INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_price.meta_key = '_bnb_booking_total_price' + AND pm_in.meta_key = '_bnb_booking_check_in' + AND pm_in.meta_value >= %s + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')", + Booking::POST_TYPE, + $first_of_month + ) + ); + + return round( (float) $result, 2 ); + } + + /** + * Get revenue year to date. + * + * @return float + */ + private static function get_revenue_ytd(): float { + global $wpdb; + $first_of_year = gmdate( 'Y-01-01' ); + + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2))) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id + INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_price.meta_key = '_bnb_booking_total_price' + AND pm_in.meta_key = '_bnb_booking_check_in' + AND pm_in.meta_value >= %s + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')", + Booking::POST_TYPE, + $first_of_year + ) + ); + + return round( (float) $result, 2 ); + } + + /** + * Get average booking value. + * + * @return float + */ + private static function get_average_booking_value(): float { + global $wpdb; + + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT AVG(CAST(pm_price.meta_value AS DECIMAL(10,2))) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_price.meta_key = '_bnb_booking_total_price' + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')", + Booking::POST_TYPE + ) + ); + + return round( (float) $result, 2 ); + } + + /** + * Get revenue from services this month. + * + * @return float + */ + private static function get_services_revenue_this_month(): float { + global $wpdb; + $first_of_month = gmdate( 'Y-m-01' ); + + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM(CAST(pm_services.meta_value AS DECIMAL(10,2))) + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm_services ON p.ID = pm_services.post_id + INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id + INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm_services.meta_key = '_bnb_booking_services_total' + AND pm_in.meta_key = '_bnb_booking_check_in' + AND pm_in.meta_value >= %s + AND pm_status.meta_key = '_bnb_booking_status' + AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')", + Booking::POST_TYPE, + $first_of_month + ) + ); + + return round( (float) $result, 2 ); + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 90cc575..0fdb401 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -19,6 +19,7 @@ use Magdev\WpBnb\Booking\EmailNotifier; use Magdev\WpBnb\Frontend\Search; use Magdev\WpBnb\Frontend\Shortcodes; use Magdev\WpBnb\Integration\CF7; +use Magdev\WpBnb\Integration\Prometheus; use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar; use Magdev\WpBnb\Frontend\Widgets\BuildingRooms; use Magdev\WpBnb\Frontend\Widgets\SimilarRooms; @@ -142,6 +143,9 @@ final class Plugin { CF7::init(); } + // Initialize Prometheus metrics integration. + Prometheus::init(); + // Initialize admin components. if ( is_admin() ) { $this->init_admin(); @@ -610,6 +614,10 @@ final class Plugin { class="nav-tab "> + + +
@@ -624,6 +632,9 @@ final class Plugin { case 'updates': $this->render_updates_settings(); break; + case 'metrics': + $this->render_metrics_settings(); + break; default: $this->render_general_settings(); break; @@ -1265,6 +1276,163 @@ final class Plugin { +
+ + +

+ + +
+

+ +
+ wp-prometheus' + ); + ?> +

+
+ +
+

+ + + +

+
+ + + + + + + + + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
wp_bnb_buildings_total
wp_bnb_rooms_total
wp_bnb_bookings_total
wp_bnb_occupancy_rate_current
wp_bnb_occupancy_rate_this_month
wp_bnb_checkins_today
wp_bnb_checkouts_today
wp_bnb_revenue_this_month
wp_bnb_revenue_ytd
wp_bnb_booking_avg_value
wp_bnb_guests_total
wp_bnb_guests_repeat
+ +

+ + +

+ +

+

+ + assets/grafana/wp-bnb-dashboard.json +

+

+ +

+ +
+

+
+ + +

+ +

+
+ save_updates_settings(); break; + case 'metrics': + $this->save_metrics_settings(); + break; default: $this->save_general_settings(); break; @@ -1465,6 +1636,19 @@ final class Plugin { settings_errors( 'wp_bnb_settings' ); } + /** + * Save metrics settings. + * + * @return void + */ + private function save_metrics_settings(): void { + $metrics_enabled = isset( $_POST['wp_bnb_metrics_enabled'] ) ? 'yes' : 'no'; + update_option( Prometheus::OPTION_ENABLED, $metrics_enabled ); + + add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Metrics settings saved.', 'wp-bnb' ), 'success' ); + settings_errors( 'wp_bnb_settings' ); + } + /** * AJAX handler for checking room availability. * diff --git a/wp-bnb.php b/wp-bnb.php index 38418bb..b1b6caf 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.8.0 + * Version: 0.9.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.8.0' ); +define( 'WP_BNB_VERSION', '0.9.0' ); // Plugin path constants. define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );