Implement Phase 8: Dashboard & Reports (v0.8.0)
Some checks failed
Create Release Package / build-release (push) Has been cancelled
Some checks failed
Create Release Package / build-release (push) Has been cancelled
- Add comprehensive admin dashboard with stat cards and widgets - Add Chart.js for occupancy/revenue trend charts - Add Reports page with Occupancy, Revenue, Guest tabs - Add CSV and PDF export functionality (using mPDF) - Add date range filters for reports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
33
CHANGELOG.md
33
CHANGELOG.md
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.8.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Admin Dashboard with comprehensive statistics:
|
||||||
|
- Occupancy overview card with current rate and comparison to last month
|
||||||
|
- Revenue summary card with this month, YTD, and comparison
|
||||||
|
- Bookings stat card with pending/confirmed counts
|
||||||
|
- Guests stat card with total, new, and repeat counts
|
||||||
|
- Today's Activity widget showing check-ins and check-outs
|
||||||
|
- Upcoming Bookings widget (next 7 days)
|
||||||
|
- Quick Actions widget for common tasks
|
||||||
|
- Chart.js integration for visual trend charts:
|
||||||
|
- Occupancy trend line chart (30 days)
|
||||||
|
- Revenue trend bar chart (6 months)
|
||||||
|
- Reports page with three report types:
|
||||||
|
- Occupancy Report: by room, by building, with progress bars
|
||||||
|
- Revenue Report: by room, by pricing tier, with averages
|
||||||
|
- Guest Statistics: top guests, nationality breakdown
|
||||||
|
- Date range filters (this month, last month, this year, custom)
|
||||||
|
- Export functionality:
|
||||||
|
- CSV export for all report types (native PHP)
|
||||||
|
- PDF export using mPDF library with professional styling
|
||||||
|
- New Composer dependency: mpdf/mpdf ^8.2 for PDF generation
|
||||||
|
- Dashboard and Reports CSS styles in admin.css (~350 lines)
|
||||||
|
- JavaScript chart initialization and report page handlers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Dashboard now uses dedicated `src/Admin/Dashboard.php` class
|
||||||
|
- Admin menu now includes Reports submenu item
|
||||||
|
- Asset enqueuing conditionally loads Chart.js on dashboard page
|
||||||
|
|
||||||
## [0.7.2] - 2026-02-03
|
## [0.7.2] - 2026-02-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
86
CLAUDE.md
86
CLAUDE.md
@@ -849,3 +849,89 @@ Admin features always work; frontend requires valid license.
|
|||||||
- Merged to main (fast-forward)
|
- Merged to main (fast-forward)
|
||||||
- Tagged: `v0.7.1`
|
- Tagged: `v0.7.1`
|
||||||
- Pushed to origin: dev, main, v0.7.1
|
- Pushed to origin: dev, main, v0.7.1
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.8.0 (Dashboard & Reports)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Admin/Dashboard.php` class (~700 lines)
|
||||||
|
- `render()` method for full dashboard page
|
||||||
|
- Occupancy stat card with current rate, room count, comparison to last month
|
||||||
|
- Revenue stat card with this month, YTD, comparison
|
||||||
|
- Bookings stat card with pending/confirmed counts
|
||||||
|
- Guests stat card with total, new this month, repeat guests
|
||||||
|
- Today's Activity widget showing check-ins and check-outs
|
||||||
|
- Upcoming Bookings widget with next 7 days' bookings
|
||||||
|
- Quick Actions widget (New Booking, New Guest, Calendar, Reports)
|
||||||
|
- Occupancy trend chart (30-day line chart)
|
||||||
|
- Revenue trend chart (6-month bar chart)
|
||||||
|
- Data methods: `get_occupancy_stats()`, `get_revenue_stats()`, `get_booking_stats()`, `get_guest_stats()`
|
||||||
|
- Chart data methods: `get_occupancy_trend_data()`, `get_revenue_trend_data()`
|
||||||
|
- Transient caching for expensive calculations (1-hour expiry)
|
||||||
|
- Created `src/Admin/Reports.php` class (~1100 lines)
|
||||||
|
- Tabbed interface: Occupancy, Revenue, Guests
|
||||||
|
- Date range filters with presets (this month, last month, this year, custom)
|
||||||
|
- Occupancy Report: by room, by building with progress bars and status labels
|
||||||
|
- Revenue Report: by room, by pricing tier, with averages
|
||||||
|
- Guest Statistics: top guests by revenue, nationality breakdown
|
||||||
|
- CSV export using native PHP `fputcsv()`
|
||||||
|
- PDF export using mPDF with professional HTML styling
|
||||||
|
- Summary cards with key metrics
|
||||||
|
- Progress bar visualizations for occupancy rates
|
||||||
|
- Added mPDF dependency to `composer.json` (`mpdf/mpdf ^8.2`)
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added Dashboard and Reports class imports
|
||||||
|
- `render_dashboard_page()` delegates to `Dashboard::render()`
|
||||||
|
- Added `render_reports_page()` method
|
||||||
|
- Reports submenu registration
|
||||||
|
- Updated menu ordering to include Reports
|
||||||
|
- Chart.js CDN enqueuing on dashboard page
|
||||||
|
- Chart data passed via `wp_localize_script()`
|
||||||
|
- Dashboard CSS styles (~350 lines in admin.css)
|
||||||
|
- Responsive grid layout (4-col stats, 2-col charts, 3-col activity)
|
||||||
|
- Stat cards with icons and gradients
|
||||||
|
- Widget components with headers
|
||||||
|
- Activity list styling
|
||||||
|
- Upcoming bookings table
|
||||||
|
- Quick action buttons grid
|
||||||
|
- Reports CSS styles (~200 lines in admin.css)
|
||||||
|
- Filter form layout
|
||||||
|
- Summary cards with primary variant
|
||||||
|
- Progress bars for occupancy
|
||||||
|
- Status labels (high/medium/low)
|
||||||
|
- Export buttons styling
|
||||||
|
- JavaScript additions in admin.js
|
||||||
|
- `initDashboardCharts()` for Chart.js initialization
|
||||||
|
- Occupancy line chart with tooltips and styling
|
||||||
|
- Revenue bar chart with currency formatting
|
||||||
|
- `initReportsPage()` for custom date toggle
|
||||||
|
- Updated version to 0.8.0
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
|
||||||
|
- `src/Admin/Dashboard.php` - Dashboard page with widgets and charts
|
||||||
|
- `src/Admin/Reports.php` - Reports page with tabs and export
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `composer.json` - Added mpdf/mpdf dependency
|
||||||
|
- `composer.lock` - Updated with mPDF and dependencies
|
||||||
|
- `src/Plugin.php` - Dashboard/Reports integration, Chart.js enqueuing
|
||||||
|
- `assets/css/admin.css` - Dashboard and Reports styles (~550 lines added)
|
||||||
|
- `assets/js/admin.js` - Chart initialization, reports page handlers
|
||||||
|
- `wp-bnb.php` - Version bump to 0.8.0
|
||||||
|
- `CHANGELOG.md` - Added v0.8.0 release notes
|
||||||
|
- `PLAN.md` - Marked Phase 8 as complete
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Chart.js CDN loading requires conditional enqueuing to avoid loading on all admin pages
|
||||||
|
- Dashboard data methods should use transient caching for expensive queries
|
||||||
|
- PDF export with mPDF requires HTML string generation with inline CSS
|
||||||
|
- Reports use `get_posts()` with meta queries for date range filtering
|
||||||
|
- Progress bar visualization done with CSS positioning and `min(100, value)` clamping
|
||||||
|
- Chart.js 4.x uses `new Chart()` constructor with configuration object
|
||||||
|
- PDF generation needs `try/catch` for mPDF exceptions
|
||||||
|
- CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility
|
||||||
|
- Guest data aggregation from bookings uses unique key pattern for anonymous guests
|
||||||
|
- Occupancy calculation: (booked nights / total room nights) * 100
|
||||||
|
|||||||
20
PLAN.md
20
PLAN.md
@@ -164,21 +164,21 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Room-specific inquiries
|
- [x] Room-specific inquiries
|
||||||
- [x] Auto-response templates (uses default CF7 mail templates)
|
- [x] Auto-response templates (uses default CF7 mail templates)
|
||||||
|
|
||||||
## Phase 8: Dashboard & Reports (v0.8.0)
|
## Phase 8: Dashboard & Reports (v0.8.0) - Complete
|
||||||
|
|
||||||
### Admin Dashboard
|
### Admin Dashboard
|
||||||
|
|
||||||
- [ ] Occupancy overview
|
- [x] Occupancy overview
|
||||||
- [ ] Upcoming check-ins/check-outs
|
- [x] Upcoming check-ins/check-outs
|
||||||
- [ ] Revenue summary
|
- [x] Revenue summary
|
||||||
- [ ] Quick actions
|
- [x] Quick actions
|
||||||
|
|
||||||
### Reports
|
### Reports
|
||||||
|
|
||||||
- [ ] Occupancy report
|
- [x] Occupancy report
|
||||||
- [ ] Revenue report
|
- [x] Revenue report
|
||||||
- [ ] Guest statistics
|
- [x] Guest statistics
|
||||||
- [ ] Export functionality (CSV, PDF)
|
- [x] Export functionality (CSV, PDF)
|
||||||
|
|
||||||
## Phase 9: Prometheus Metrics (v0.9.0)
|
## Phase 9: Prometheus Metrics (v0.9.0)
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| 0.5.0 | Services | Complete |
|
| 0.5.0 | Services | Complete |
|
||||||
| 0.6.0 | Frontend | Complete |
|
| 0.6.0 | Frontend | Complete |
|
||||||
| 0.7.0 | CF7 Integration | Complete |
|
| 0.7.0 | CF7 Integration | Complete |
|
||||||
| 0.8.0 | Dashboard | TBD |
|
| 0.8.0 | Dashboard | Complete |
|
||||||
| 0.9.0 | Prometheus Metrics | TBD |
|
| 0.9.0 | Prometheus Metrics | TBD |
|
||||||
| 0.10.0 | Security Audit | TBD |
|
| 0.10.0 | Security Audit | TBD |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
@@ -4,7 +4,382 @@
|
|||||||
* @package Magdev\WpBnb
|
* @package Magdev\WpBnb
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Dashboard */
|
/* ============================================
|
||||||
|
Dashboard
|
||||||
|
============================================ */
|
||||||
|
.wp-bnb-dashboard-grid {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-dashboard-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Row - 4 columns */
|
||||||
|
.wp-bnb-stats-row {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Charts Row - 2 columns */
|
||||||
|
.wp-bnb-charts-row {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Row - 3 columns */
|
||||||
|
.wp-bnb-activity-row {
|
||||||
|
grid-template-columns: 1fr 1.5fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
.wp-bnb-stats-row {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.wp-bnb-activity-row {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.wp-bnb-activity-row .wp-bnb-quick-actions {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.wp-bnb-stats-row,
|
||||||
|
.wp-bnb-charts-row,
|
||||||
|
.wp-bnb-activity-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.wp-bnb-activity-row .wp-bnb-quick-actions {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat Cards */
|
||||||
|
.wp-bnb-stat-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #2271b1, #135e96);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon.revenue {
|
||||||
|
background: linear-gradient(135deg, #00a32a, #007017);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon.bookings {
|
||||||
|
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon.guests {
|
||||||
|
background: linear-gradient(135deg, #e67e22, #d35400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-icon .dashicons {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #50575e;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change.positive {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change.negative {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-stat-change .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widgets */
|
||||||
|
.wp-bnb-widget {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header .wp-bnb-widget-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-header .wp-bnb-view-all {
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-widget-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Widgets */
|
||||||
|
.wp-bnb-chart-widget .wp-bnb-widget-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.wp-bnb-empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-empty-state .dashicons {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
color: #c3c4c7;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-empty-state p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Section */
|
||||||
|
.wp-bnb-activity-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section h4 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section h4 .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-section h4 .count {
|
||||||
|
background: #2271b1;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list a:hover {
|
||||||
|
background: #dcdcde;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list strong {
|
||||||
|
color: #1d2327;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-activity-list .room {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upcoming Bookings Table */
|
||||||
|
.wp-bnb-upcoming-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #50575e;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table a {
|
||||||
|
color: #2271b1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table a:hover {
|
||||||
|
color: #135e96;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-upcoming-table .wp-bnb-status-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Actions */
|
||||||
|
.wp-bnb-quick-actions .wp-bnb-widget-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-actions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 20px 10px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #1d2327;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn:hover {
|
||||||
|
background: #2271b1;
|
||||||
|
border-color: #2271b1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn .dashicons {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-action-btn span:last-child {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy Dashboard (backward compatibility) */
|
||||||
.wp-bnb-dashboard {
|
.wp-bnb-dashboard {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #c3c4c7;
|
border: 1px solid #c3c4c7;
|
||||||
@@ -1345,3 +1720,305 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #135e96;
|
color: #135e96;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Reports Page
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Reports Tabs */
|
||||||
|
.wp-bnb-reports-tabs .nav-tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-reports-tabs .nav-tab .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reports Content Container */
|
||||||
|
.wp-bnb-reports-content {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-top: none;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reports Filters */
|
||||||
|
.wp-bnb-reports-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-filter-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-filter-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-filter-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-period-select {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-custom-dates {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-custom-dates input[type="date"] {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-period {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-period strong {
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report Body */
|
||||||
|
.wp-bnb-report-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-section h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
margin: 25px 0 15px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-section h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary Cards */
|
||||||
|
.wp-bnb-summary-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-cards.secondary {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.wp-bnb-summary-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.wp-bnb-summary-cards.secondary {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 782px) {
|
||||||
|
.wp-bnb-summary-cards,
|
||||||
|
.wp-bnb-summary-cards.secondary {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card {
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.primary {
|
||||||
|
background: linear-gradient(135deg, #d4edda, #c3e6cb);
|
||||||
|
border-color: #a3d4aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.small {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2271b1;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.primary .wp-bnb-summary-value {
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-card.small .wp-bnb-summary-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-summary-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #50575e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report Tables */
|
||||||
|
.wp-bnb-report-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table th,
|
||||||
|
.wp-bnb-report-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table th.num,
|
||||||
|
.wp-bnb-report-table td.num {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table tfoot th {
|
||||||
|
background: #f0f6fc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table a {
|
||||||
|
color: #2271b1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-report-table a:hover {
|
||||||
|
color: #135e96;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.wp-bnb-progress-bar {
|
||||||
|
position: relative;
|
||||||
|
background: #dcdcde;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #2271b1, #135e96);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-progress-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
text-shadow: 0 0 3px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Labels */
|
||||||
|
.wp-bnb-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-status.high {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-status.medium {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-status.low {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Data Message */
|
||||||
|
.wp-bnb-no-data {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: #787c82;
|
||||||
|
font-style: italic;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export Buttons */
|
||||||
|
.wp-bnb-export-buttons {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-buttons h4 {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-actions .button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-export-actions .button .dashicons {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1024,6 +1024,181 @@
|
|||||||
updateServicesTotal();
|
updateServicesTotal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize dashboard charts.
|
||||||
|
*/
|
||||||
|
function initDashboardCharts() {
|
||||||
|
// Only run on dashboard page.
|
||||||
|
if (!wpBnbAdmin.isDashboard || typeof Chart === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chartData = wpBnbAdmin.chartData;
|
||||||
|
if (!chartData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart.js default configuration.
|
||||||
|
Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
|
||||||
|
Chart.defaults.font.size = 12;
|
||||||
|
Chart.defaults.color = '#50575e';
|
||||||
|
|
||||||
|
// Initialize Occupancy Chart.
|
||||||
|
var occupancyCtx = document.getElementById('wp-bnb-occupancy-chart');
|
||||||
|
if (occupancyCtx && chartData.occupancy) {
|
||||||
|
new Chart(occupancyCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: chartData.occupancy.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: wpBnbAdmin.i18n.occupancy,
|
||||||
|
data: chartData.occupancy.data,
|
||||||
|
borderColor: '#2271b1',
|
||||||
|
backgroundColor: 'rgba(34, 113, 177, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 5,
|
||||||
|
pointBackgroundColor: '#2271b1',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointBorderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1d2327',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.parsed.y.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Revenue Chart.
|
||||||
|
var revenueCtx = document.getElementById('wp-bnb-revenue-chart');
|
||||||
|
if (revenueCtx && chartData.revenue) {
|
||||||
|
new Chart(revenueCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: chartData.revenue.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: wpBnbAdmin.i18n.revenue,
|
||||||
|
data: chartData.revenue.data,
|
||||||
|
backgroundColor: 'rgba(0, 163, 42, 0.8)',
|
||||||
|
borderColor: '#00a32a',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
barPercentage: 0.6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1d2327',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return new Intl.NumberFormat('de-CH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CHF'
|
||||||
|
}).format(context.parsed.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return new Intl.NumberFormat('de-CH', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CHF',
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize reports page functionality.
|
||||||
|
*/
|
||||||
|
function initReportsPage() {
|
||||||
|
var $periodSelect = $('.wp-bnb-period-select');
|
||||||
|
var $customDates = $('.wp-bnb-custom-dates');
|
||||||
|
|
||||||
|
if (!$periodSelect.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle custom date fields based on period selection.
|
||||||
|
$periodSelect.on('change', function() {
|
||||||
|
if ($(this).val() === 'custom') {
|
||||||
|
$customDates.show();
|
||||||
|
} else {
|
||||||
|
$customDates.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on document ready.
|
// Initialize on document ready.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initLicenseManagement();
|
initLicenseManagement();
|
||||||
@@ -1037,6 +1212,8 @@
|
|||||||
initGuestSearch();
|
initGuestSearch();
|
||||||
initServicePricing();
|
initServicePricing();
|
||||||
initBookingServices();
|
initBookingServices();
|
||||||
|
initDashboardCharts();
|
||||||
|
initReportsPage();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3.0",
|
"php": ">=8.3.0",
|
||||||
"twig/twig": "^3.0",
|
"twig/twig": "^3.0",
|
||||||
"magdev/wc-licensed-product-client": "^0.2"
|
"magdev/wc-licensed-product-client": "^0.2",
|
||||||
|
"mpdf/mpdf": "^8.2"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
357
composer.lock
generated
357
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "aed1e4dd36ea76994768a8379100314b",
|
"content-hash": "ae9fdb5fb51bbef492ad4f2a40406fd3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "magdev/wc-licensed-product-client",
|
"name": "magdev/wc-licensed-product-client",
|
||||||
@@ -56,6 +56,289 @@
|
|||||||
"relative": true
|
"relative": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/mpdf",
|
||||||
|
"version": "v8.2.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/mpdf.git",
|
||||||
|
"reference": "b59670a09498689c33ce639bac8f5ba26721dab3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3",
|
||||||
|
"reference": "b59670a09498689c33ce639bac8f5ba26721dab3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
|
||||||
|
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
|
||||||
|
"myclabs/deep-copy": "^1.7",
|
||||||
|
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||||
|
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"setasign/fpdi": "^2.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.3.0",
|
||||||
|
"mpdf/qrcode": "^1.1.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5.0",
|
||||||
|
"tracy/tracy": "~2.5",
|
||||||
|
"yoast/phpunit-polyfills": "^1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Needed for generation of some types of barcodes",
|
||||||
|
"ext-xml": "Needed mainly for SVG manipulation",
|
||||||
|
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-2.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matěj Humpál",
|
||||||
|
"role": "Developer, maintainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ian Back",
|
||||||
|
"role": "Developer (retired)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
|
||||||
|
"homepage": "https://mpdf.github.io",
|
||||||
|
"keywords": [
|
||||||
|
"pdf",
|
||||||
|
"php",
|
||||||
|
"utf-8"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://mpdf.github.io",
|
||||||
|
"issues": "https://github.com/mpdf/mpdf/issues",
|
||||||
|
"source": "https://github.com/mpdf/mpdf"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.me/mpdf",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-12-01T10:18:02+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-http-message-shim",
|
||||||
|
"version": "v2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-http-message-shim.git",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrHttpMessageShim\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nigel Cunningham",
|
||||||
|
"email": "nigel.cunningham@technocrat.com.au"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Shim to allow support of different psr/message versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
|
||||||
|
},
|
||||||
|
"time": "2023-10-02T14:34:03+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-log-aware-trait",
|
||||||
|
"version": "v3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/log": "^3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrLogAwareTrait\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Trait to allow support of different psr/log versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2023-05-03T06:19:36+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "myclabs/deep-copy",
|
||||||
|
"version": "1.13.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/collections": "<1.6.8",
|
||||||
|
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/collections": "^1.6.8",
|
||||||
|
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||||
|
"phpspec/prophecy": "^1.10",
|
||||||
|
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/DeepCopy/deep_copy.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopy\\": "src/DeepCopy/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Create deep copies (clones) of your objects",
|
||||||
|
"keywords": [
|
||||||
|
"clone",
|
||||||
|
"copy",
|
||||||
|
"duplicate",
|
||||||
|
"object",
|
||||||
|
"object graph"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||||
|
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-01T08:46:24+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paragonie/random_compat",
|
||||||
|
"version": "v9.99.100",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/paragonie/random_compat.git",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">= 7"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "4.*|5.*",
|
||||||
|
"vimeo/psalm": "^1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paragon Initiative Enterprises",
|
||||||
|
"email": "security@paragonie.com",
|
||||||
|
"homepage": "https://paragonie.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||||
|
"keywords": [
|
||||||
|
"csprng",
|
||||||
|
"polyfill",
|
||||||
|
"pseudorandom",
|
||||||
|
"random"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"email": "info@paragonie.com",
|
||||||
|
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||||
|
"source": "https://github.com/paragonie/random_compat"
|
||||||
|
},
|
||||||
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -313,6 +596,78 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-09-11T13:17:53+00:00"
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "setasign/fpdi",
|
||||||
|
"version": "v2.6.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Setasign/FPDI.git",
|
||||||
|
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||||
|
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"setasign/tfpdf": "<1.31"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7",
|
||||||
|
"setasign/fpdf": "~1.8.6",
|
||||||
|
"setasign/tfpdf": "~1.33",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"tecnickcom/tcpdf": "^6.8"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"setasign\\Fpdi\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jan Slabon",
|
||||||
|
"email": "jan.slabon@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maximilian Kresse",
|
||||||
|
"email": "maximilian.kresse@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||||
|
"homepage": "https://www.setasign.com/fpdi",
|
||||||
|
"keywords": [
|
||||||
|
"fpdf",
|
||||||
|
"fpdi",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||||
|
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-05T09:57:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/deprecation-contracts",
|
"name": "symfony/deprecation-contracts",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
|
|||||||
942
src/Admin/Dashboard.php
Normal file
942
src/Admin/Dashboard.php
Normal file
@@ -0,0 +1,942 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin Dashboard page.
|
||||||
|
*
|
||||||
|
* Displays comprehensive dashboard with statistics, charts, and quick actions.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Admin;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard class.
|
||||||
|
*/
|
||||||
|
final class Dashboard {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for dashboard stats.
|
||||||
|
*/
|
||||||
|
private const CACHE_KEY = 'wp_bnb_dashboard_stats';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache expiry in seconds (1 hour).
|
||||||
|
*/
|
||||||
|
private const CACHE_EXPIRY = HOUR_IN_SECONDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the dashboard page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render(): void {
|
||||||
|
$license_valid = LicenseManager::is_license_valid();
|
||||||
|
$is_localhost = LicenseManager::is_localhost();
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
||||||
|
|
||||||
|
<?php self::render_notices( $license_valid, $is_localhost ); ?>
|
||||||
|
|
||||||
|
<div class="wp-bnb-dashboard-grid">
|
||||||
|
<!-- Row 1: Stats Cards -->
|
||||||
|
<div class="wp-bnb-dashboard-row wp-bnb-stats-row">
|
||||||
|
<?php self::render_occupancy_card(); ?>
|
||||||
|
<?php self::render_revenue_card(); ?>
|
||||||
|
<?php self::render_bookings_card(); ?>
|
||||||
|
<?php self::render_guests_card(); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Charts -->
|
||||||
|
<div class="wp-bnb-dashboard-row wp-bnb-charts-row">
|
||||||
|
<?php self::render_occupancy_chart(); ?>
|
||||||
|
<?php self::render_revenue_chart(); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Activity and Quick Actions -->
|
||||||
|
<div class="wp-bnb-dashboard-row wp-bnb-activity-row">
|
||||||
|
<?php self::render_today_activity(); ?>
|
||||||
|
<?php self::render_upcoming_bookings(); ?>
|
||||||
|
<?php self::render_quick_actions(); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render admin notices.
|
||||||
|
*
|
||||||
|
* @param bool $license_valid Whether license is valid.
|
||||||
|
* @param bool $is_localhost Whether running on localhost.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_notices( bool $license_valid, bool $is_localhost ): void {
|
||||||
|
if ( $is_localhost ) :
|
||||||
|
?>
|
||||||
|
<div class="notice notice-info">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
||||||
|
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'You are running on a local development environment. All features are enabled.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
elseif ( ! $license_valid ) :
|
||||||
|
?>
|
||||||
|
<div class="notice notice-warning">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Link to settings page */
|
||||||
|
esc_html__( 'Your license is not active. Please %s to unlock all features.', 'wp-bnb' ),
|
||||||
|
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ) . '">' . esc_html__( 'activate your license', 'wp-bnb' ) . '</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render occupancy stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_occupancy_card(): void {
|
||||||
|
$stats = self::get_occupancy_stats();
|
||||||
|
$rate = $stats['rate'];
|
||||||
|
$occupied = $stats['occupied'];
|
||||||
|
$total = $stats['total'];
|
||||||
|
$previous_rate = $stats['previous_rate'];
|
||||||
|
$change = $rate - $previous_rate;
|
||||||
|
$change_class = $change >= 0 ? 'positive' : 'negative';
|
||||||
|
$change_icon = $change >= 0 ? 'arrow-up-alt' : 'arrow-down-alt';
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon">
|
||||||
|
<span class="dashicons dashicons-admin-home"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Current Occupancy', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( number_format( $rate, 1 ) ); ?>%</div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: Number of occupied rooms, 2: Total rooms */
|
||||||
|
esc_html__( '%1$d of %2$d rooms', 'wp-bnb' ),
|
||||||
|
$occupied,
|
||||||
|
$total
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php if ( $previous_rate > 0 ) : ?>
|
||||||
|
<div class="wp-bnb-stat-change <?php echo esc_attr( $change_class ); ?>">
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $change_icon ); ?>"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Percentage change */
|
||||||
|
esc_html__( '%s%% vs last month', 'wp-bnb' ),
|
||||||
|
number_format( abs( $change ), 1 )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render revenue stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_revenue_card(): void {
|
||||||
|
$stats = self::get_revenue_stats();
|
||||||
|
$this_month = $stats['this_month'];
|
||||||
|
$last_month = $stats['last_month'];
|
||||||
|
$change = $last_month > 0 ? ( ( $this_month - $last_month ) / $last_month ) * 100 : 0;
|
||||||
|
$change_class = $change >= 0 ? 'positive' : 'negative';
|
||||||
|
$change_icon = $change >= 0 ? 'arrow-up-alt' : 'arrow-down-alt';
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon revenue">
|
||||||
|
<span class="dashicons dashicons-chart-area"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Revenue This Month', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( Calculator::formatPrice( $this_month ) ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Year-to-date revenue */
|
||||||
|
esc_html__( 'YTD: %s', 'wp-bnb' ),
|
||||||
|
Calculator::formatPrice( $stats['ytd'] )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php if ( $last_month > 0 ) : ?>
|
||||||
|
<div class="wp-bnb-stat-change <?php echo esc_attr( $change_class ); ?>">
|
||||||
|
<span class="dashicons dashicons-<?php echo esc_attr( $change_icon ); ?>"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Percentage change */
|
||||||
|
esc_html__( '%s%% vs last month', 'wp-bnb' ),
|
||||||
|
number_format( abs( $change ), 1 )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render bookings stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_bookings_card(): void {
|
||||||
|
$stats = self::get_booking_stats();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon bookings">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Bookings This Month', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( $stats['this_month'] ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: Pending count, 2: Confirmed count */
|
||||||
|
esc_html__( '%1$d pending, %2$d confirmed', 'wp-bnb' ),
|
||||||
|
$stats['pending'],
|
||||||
|
$stats['confirmed']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render guests stat card.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_guests_card(): void {
|
||||||
|
$stats = self::get_guest_stats();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-stat-card">
|
||||||
|
<div class="wp-bnb-stat-icon guests">
|
||||||
|
<span class="dashicons dashicons-groups"></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-stat-content">
|
||||||
|
<div class="wp-bnb-stat-label"><?php esc_html_e( 'Total Guests', 'wp-bnb' ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-value"><?php echo esc_html( $stats['total'] ); ?></div>
|
||||||
|
<div class="wp-bnb-stat-meta">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: New guests this month, 2: Repeat guests count */
|
||||||
|
esc_html__( '%1$d new this month, %2$d repeat', 'wp-bnb' ),
|
||||||
|
$stats['new_this_month'],
|
||||||
|
$stats['repeat']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render occupancy trend chart.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_occupancy_chart(): void {
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget wp-bnb-chart-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Occupancy Trend (Last 30 Days)', 'wp-bnb' ); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<canvas id="wp-bnb-occupancy-chart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render revenue trend chart.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_revenue_chart(): void {
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget wp-bnb-chart-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Revenue Trend (Last 6 Months)', 'wp-bnb' ); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<canvas id="wp-bnb-revenue-chart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render today's activity widget.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_today_activity(): void {
|
||||||
|
$checkins = Availability::get_todays_checkins();
|
||||||
|
$checkouts = Availability::get_todays_checkouts();
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( "Today's Activity", 'wp-bnb' ); ?></h3>
|
||||||
|
<span class="wp-bnb-widget-date"><?php echo esc_html( wp_date( get_option( 'date_format' ) ) ); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<?php if ( empty( $checkins ) && empty( $checkouts ) ) : ?>
|
||||||
|
<div class="wp-bnb-empty-state">
|
||||||
|
<span class="dashicons dashicons-calendar"></span>
|
||||||
|
<p><?php esc_html_e( 'No check-ins or check-outs scheduled for today.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php if ( ! empty( $checkins ) ) : ?>
|
||||||
|
<div class="wp-bnb-activity-section">
|
||||||
|
<h4>
|
||||||
|
<span class="dashicons dashicons-migrate"></span>
|
||||||
|
<?php esc_html_e( 'Check-ins', 'wp-bnb' ); ?>
|
||||||
|
<span class="count"><?php echo count( $checkins ); ?></span>
|
||||||
|
</h4>
|
||||||
|
<ul class="wp-bnb-activity-list">
|
||||||
|
<?php foreach ( $checkins as $booking ) : ?>
|
||||||
|
<?php
|
||||||
|
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
|
||||||
|
<strong><?php echo esc_html( $guest_name ); ?></strong>
|
||||||
|
<?php if ( $room ) : ?>
|
||||||
|
<span class="room"><?php echo esc_html( $room->post_title ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $checkouts ) ) : ?>
|
||||||
|
<div class="wp-bnb-activity-section">
|
||||||
|
<h4>
|
||||||
|
<span class="dashicons dashicons-external"></span>
|
||||||
|
<?php esc_html_e( 'Check-outs', 'wp-bnb' ); ?>
|
||||||
|
<span class="count"><?php echo count( $checkouts ); ?></span>
|
||||||
|
</h4>
|
||||||
|
<ul class="wp-bnb-activity-list">
|
||||||
|
<?php foreach ( $checkouts as $booking ) : ?>
|
||||||
|
<?php
|
||||||
|
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
|
||||||
|
<strong><?php echo esc_html( $guest_name ); ?></strong>
|
||||||
|
<?php if ( $room ) : ?>
|
||||||
|
<span class="room"><?php echo esc_html( $room->post_title ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render upcoming bookings widget.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_upcoming_bookings(): void {
|
||||||
|
$bookings = self::get_upcoming_bookings( 7 );
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Upcoming Bookings', 'wp-bnb' ); ?></h3>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=' . Booking::POST_TYPE ) ); ?>" class="wp-bnb-view-all">
|
||||||
|
<?php esc_html_e( 'View All', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<?php if ( empty( $bookings ) ) : ?>
|
||||||
|
<div class="wp-bnb-empty-state">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<p><?php esc_html_e( 'No upcoming bookings in the next 7 days.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<table class="wp-bnb-upcoming-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Guest', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Room', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $bookings as $booking ) : ?>
|
||||||
|
<?php
|
||||||
|
$guest_name = get_post_meta( $booking->ID, '_bnb_booking_guest_name', true );
|
||||||
|
$room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true );
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$status = get_post_meta( $booking->ID, '_bnb_booking_status', true );
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
$statuses = Booking::get_booking_statuses();
|
||||||
|
$colors = Booking::get_status_colors();
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking->ID ) ); ?>">
|
||||||
|
<?php echo esc_html( $guest_name ); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><?php echo $room ? esc_html( $room->post_title ) : '—'; ?></td>
|
||||||
|
<td><?php echo esc_html( wp_date( get_option( 'date_format' ), strtotime( $check_in ) ) ); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="wp-bnb-status-badge" style="background-color: <?php echo esc_attr( $colors[ $status ] ?? '#666' ); ?>">
|
||||||
|
<?php echo esc_html( $statuses[ $status ] ?? $status ); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render quick actions widget.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_quick_actions(): void {
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-widget wp-bnb-quick-actions">
|
||||||
|
<div class="wp-bnb-widget-header">
|
||||||
|
<h3><?php esc_html_e( 'Quick Actions', 'wp-bnb' ); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="wp-bnb-widget-content">
|
||||||
|
<div class="wp-bnb-actions-grid">
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Booking::POST_TYPE ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-plus-alt"></span>
|
||||||
|
<span><?php esc_html_e( 'New Booking', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=' . Guest::POST_TYPE ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-admin-users"></span>
|
||||||
|
<span><?php esc_html_e( 'New Guest', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-calendar' ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<span><?php esc_html_e( 'View Calendar', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-reports' ) ); ?>" class="wp-bnb-action-btn">
|
||||||
|
<span class="dashicons dashicons-analytics"></span>
|
||||||
|
<span><?php esc_html_e( 'View Reports', 'wp-bnb' ); ?></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get occupancy statistics.
|
||||||
|
*
|
||||||
|
* @return array{rate: float, occupied: int, total: int, previous_rate: float}
|
||||||
|
*/
|
||||||
|
public static function get_occupancy_stats(): array {
|
||||||
|
// Get total rooms.
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_rooms = count( $rooms );
|
||||||
|
|
||||||
|
// Get currently occupied rooms.
|
||||||
|
$current_bookings = Availability::get_current_bookings();
|
||||||
|
$occupied_rooms = count( $current_bookings );
|
||||||
|
|
||||||
|
$rate = $total_rooms > 0 ? ( $occupied_rooms / $total_rooms ) * 100 : 0;
|
||||||
|
|
||||||
|
// Calculate last month's average occupancy.
|
||||||
|
$previous_rate = self::get_average_occupancy_for_month(
|
||||||
|
(int) gmdate( 'Y', strtotime( '-1 month' ) ),
|
||||||
|
(int) gmdate( 'n', strtotime( '-1 month' ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'rate' => $rate,
|
||||||
|
'occupied' => $occupied_rooms,
|
||||||
|
'total' => $total_rooms,
|
||||||
|
'previous_rate' => $previous_rate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average occupancy rate for a specific month.
|
||||||
|
*
|
||||||
|
* @param int $year Year.
|
||||||
|
* @param int $month Month.
|
||||||
|
* @return float Average occupancy percentage.
|
||||||
|
*/
|
||||||
|
private static function get_average_occupancy_for_month( int $year, int $month ): float {
|
||||||
|
$cache_key = self::CACHE_KEY . "_occupancy_{$year}_{$month}";
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return (float) $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_rooms = count( $rooms );
|
||||||
|
|
||||||
|
if ( $total_rooms === 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$days_in_month = (int) gmdate( 't', mktime( 0, 0, 0, $month, 1, $year ) );
|
||||||
|
$total_room_nights = $total_rooms * $days_in_month;
|
||||||
|
$booked_nights = 0;
|
||||||
|
|
||||||
|
$month_start = sprintf( '%04d-%02d-01', $year, $month );
|
||||||
|
$month_end = sprintf( '%04d-%02d-%02d', $year, $month, $days_in_month );
|
||||||
|
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $month_end,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $month_start,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true );
|
||||||
|
|
||||||
|
// Clamp to month boundaries.
|
||||||
|
$start = max( $check_in, $month_start );
|
||||||
|
$end = min( $check_out, gmdate( 'Y-m-d', strtotime( $month_end . ' +1 day' ) ) );
|
||||||
|
|
||||||
|
$nights = Booking::calculate_nights( $start, $end );
|
||||||
|
$booked_nights += $nights;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rate = ( $booked_nights / $total_room_nights ) * 100;
|
||||||
|
|
||||||
|
set_transient( $cache_key, $rate, self::CACHE_EXPIRY );
|
||||||
|
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue statistics.
|
||||||
|
*
|
||||||
|
* @return array{this_month: float, last_month: float, ytd: float}
|
||||||
|
*/
|
||||||
|
public static function get_revenue_stats(): array {
|
||||||
|
$this_month = self::get_revenue_for_period(
|
||||||
|
gmdate( 'Y-m-01' ),
|
||||||
|
gmdate( 'Y-m-t' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$last_month = self::get_revenue_for_period(
|
||||||
|
gmdate( 'Y-m-01', strtotime( '-1 month' ) ),
|
||||||
|
gmdate( 'Y-m-t', strtotime( '-1 month' ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
$ytd = self::get_revenue_for_period(
|
||||||
|
gmdate( 'Y-01-01' ),
|
||||||
|
gmdate( 'Y-m-d' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'this_month' => $this_month,
|
||||||
|
'last_month' => $last_month,
|
||||||
|
'ytd' => $ytd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue for a specific period.
|
||||||
|
*
|
||||||
|
* @param string $start_date Start date (Y-m-d).
|
||||||
|
* @param string $end_date End date (Y-m-d).
|
||||||
|
* @return float Total revenue.
|
||||||
|
*/
|
||||||
|
public static function get_revenue_for_period( string $start_date, string $end_date ): float {
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $start_date,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $end_date,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$total = 0.0;
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true );
|
||||||
|
$total += (float) $price;
|
||||||
|
|
||||||
|
// Add services total.
|
||||||
|
$services_total = Booking::calculate_booking_services_total( $booking->ID );
|
||||||
|
$total += $services_total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get booking statistics.
|
||||||
|
*
|
||||||
|
* @return array{this_month: int, pending: int, confirmed: int}
|
||||||
|
*/
|
||||||
|
public static function get_booking_stats(): array {
|
||||||
|
$month_start = gmdate( 'Y-m-01' );
|
||||||
|
$month_end = gmdate( 'Y-m-t' );
|
||||||
|
|
||||||
|
// Bookings created this month.
|
||||||
|
$this_month = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'date_query' => array(
|
||||||
|
array(
|
||||||
|
'after' => $month_start,
|
||||||
|
'before' => $month_end,
|
||||||
|
'inclusive' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pending bookings.
|
||||||
|
$pending = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'pending',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Confirmed bookings.
|
||||||
|
$confirmed = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'confirmed',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'this_month' => count( $this_month ),
|
||||||
|
'pending' => count( $pending ),
|
||||||
|
'confirmed' => count( $confirmed ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guest statistics.
|
||||||
|
*
|
||||||
|
* @return array{total: int, new_this_month: int, repeat: int}
|
||||||
|
*/
|
||||||
|
public static function get_guest_stats(): array {
|
||||||
|
$month_start = gmdate( 'Y-m-01' );
|
||||||
|
$month_end = gmdate( 'Y-m-t' );
|
||||||
|
|
||||||
|
// Total guests.
|
||||||
|
$total = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// New guests this month.
|
||||||
|
$new_this_month = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'date_query' => array(
|
||||||
|
array(
|
||||||
|
'after' => $month_start,
|
||||||
|
'before' => $month_end,
|
||||||
|
'inclusive' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Repeat guests (2+ bookings).
|
||||||
|
$all_guests = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$repeat = 0;
|
||||||
|
foreach ( $all_guests as $guest ) {
|
||||||
|
$booking_count = Guest::get_booking_count( $guest->ID );
|
||||||
|
if ( $booking_count >= 2 ) {
|
||||||
|
++$repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'total' => count( $total ),
|
||||||
|
'new_this_month' => count( $new_this_month ),
|
||||||
|
'repeat' => $repeat,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upcoming bookings.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to look ahead.
|
||||||
|
* @return array<\WP_Post> Array of booking posts.
|
||||||
|
*/
|
||||||
|
private static function get_upcoming_bookings( int $days = 7 ): array {
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
|
||||||
|
|
||||||
|
return get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => 10,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'pending', 'confirmed' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $today,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $end_date,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_booking_check_in',
|
||||||
|
'order' => 'ASC',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get occupancy trend data for charts.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to include.
|
||||||
|
* @return array{labels: array<string>, data: array<float>}
|
||||||
|
*/
|
||||||
|
public static function get_occupancy_trend_data( int $days = 30 ): array {
|
||||||
|
$labels = array();
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_rooms = count( $rooms );
|
||||||
|
|
||||||
|
if ( $total_rooms === 0 ) {
|
||||||
|
return array(
|
||||||
|
'labels' => array(),
|
||||||
|
'data' => array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( $i = $days - 1; $i >= 0; $i-- ) {
|
||||||
|
$date = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
|
||||||
|
$labels[] = wp_date( 'M j', strtotime( $date ) );
|
||||||
|
|
||||||
|
// Count bookings active on this date.
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => array( 'confirmed', 'checked_in', 'checked_out' ),
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $date,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $date,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rate = ( count( $bookings ) / $total_rooms ) * 100;
|
||||||
|
$data[] = round( $rate, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue trend data for charts.
|
||||||
|
*
|
||||||
|
* @param int $months Number of months to include.
|
||||||
|
* @return array{labels: array<string>, data: array<float>}
|
||||||
|
*/
|
||||||
|
public static function get_revenue_trend_data( int $months = 6 ): array {
|
||||||
|
$labels = array();
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
for ( $i = $months - 1; $i >= 0; $i-- ) {
|
||||||
|
$month_start = gmdate( 'Y-m-01', strtotime( "-{$i} months" ) );
|
||||||
|
$month_end = gmdate( 'Y-m-t', strtotime( "-{$i} months" ) );
|
||||||
|
$month_name = gmdate( 'M Y', strtotime( $month_start ) );
|
||||||
|
|
||||||
|
$labels[] = $month_name;
|
||||||
|
$data[] = self::get_revenue_for_period( $month_start, $month_end );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'labels' => $labels,
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1368
src/Admin/Reports.php
Normal file
1368
src/Admin/Reports.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ declare( strict_types=1 );
|
|||||||
namespace Magdev\WpBnb;
|
namespace Magdev\WpBnb;
|
||||||
|
|
||||||
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||||
|
use Magdev\WpBnb\Admin\Dashboard as DashboardAdmin;
|
||||||
|
use Magdev\WpBnb\Admin\Reports as ReportsAdmin;
|
||||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||||
use Magdev\WpBnb\Blocks\BlockRegistrar;
|
use Magdev\WpBnb\Blocks\BlockRegistrar;
|
||||||
use Magdev\WpBnb\Booking\Availability;
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
@@ -248,6 +250,7 @@ final class Plugin {
|
|||||||
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
$is_plugin_page = strpos( $hook_suffix, 'wp-bnb' ) !== false;
|
||||||
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
$is_our_post_type = in_array( $post_type, array( Building::POST_TYPE, Room::POST_TYPE, Booking::POST_TYPE, Guest::POST_TYPE, Service::POST_TYPE ), true );
|
||||||
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
$is_edit_screen = in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true );
|
||||||
|
$is_dashboard = 'toplevel_page_wp-bnb' === $hook_suffix;
|
||||||
|
|
||||||
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
if ( ! $is_plugin_page && ! ( $is_our_post_type && $is_edit_screen ) ) {
|
||||||
return;
|
return;
|
||||||
@@ -268,6 +271,18 @@ final class Plugin {
|
|||||||
$script_deps[] = 'jquery-ui-sortable';
|
$script_deps[] = 'jquery-ui-sortable';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Chart.js for dashboard.
|
||||||
|
if ( $is_dashboard ) {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'chartjs',
|
||||||
|
'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js',
|
||||||
|
array(),
|
||||||
|
'4.4.1',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$script_deps[] = 'chartjs';
|
||||||
|
}
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wp-bnb-admin',
|
'wp-bnb-admin',
|
||||||
WP_BNB_URL . 'assets/js/admin.js',
|
WP_BNB_URL . 'assets/js/admin.js',
|
||||||
@@ -276,13 +291,12 @@ final class Plugin {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_localize_script(
|
// Build localize data.
|
||||||
'wp-bnb-admin',
|
$localize_data = array(
|
||||||
'wpBnbAdmin',
|
|
||||||
array(
|
|
||||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
'nonce' => wp_create_nonce( 'wp_bnb_admin_nonce' ),
|
||||||
'postType' => $post_type,
|
'postType' => $post_type,
|
||||||
|
'isDashboard' => $is_dashboard,
|
||||||
'i18n' => array(
|
'i18n' => array(
|
||||||
'validating' => __( 'Validating...', 'wp-bnb' ),
|
'validating' => __( 'Validating...', 'wp-bnb' ),
|
||||||
'activating' => __( 'Activating...', 'wp-bnb' ),
|
'activating' => __( 'Activating...', 'wp-bnb' ),
|
||||||
@@ -310,9 +324,20 @@ final class Plugin {
|
|||||||
'updateAvailable' => __( 'Update available!', 'wp-bnb' ),
|
'updateAvailable' => __( 'Update available!', 'wp-bnb' ),
|
||||||
'upToDate' => __( '(You are up to date)', 'wp-bnb' ),
|
'upToDate' => __( '(You are up to date)', 'wp-bnb' ),
|
||||||
'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ),
|
'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ),
|
||||||
|
'occupancy' => __( 'Occupancy %', 'wp-bnb' ),
|
||||||
|
'revenue' => __( 'Revenue', 'wp-bnb' ),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add chart data for dashboard.
|
||||||
|
if ( $is_dashboard ) {
|
||||||
|
$localize_data['chartData'] = array(
|
||||||
|
'occupancy' => DashboardAdmin::get_occupancy_trend_data( 30 ),
|
||||||
|
'revenue' => DashboardAdmin::get_revenue_trend_data( 6 ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_localize_script( 'wp-bnb-admin', 'wpBnbAdmin', $localize_data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -448,6 +473,16 @@ final class Plugin {
|
|||||||
array( $this, 'render_dashboard_page' )
|
array( $this, 'render_dashboard_page' )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reports submenu.
|
||||||
|
add_submenu_page(
|
||||||
|
'wp-bnb',
|
||||||
|
__( 'Reports', 'wp-bnb' ),
|
||||||
|
__( 'Reports', 'wp-bnb' ),
|
||||||
|
'manage_options',
|
||||||
|
'wp-bnb-reports',
|
||||||
|
array( $this, 'render_reports_page' )
|
||||||
|
);
|
||||||
|
|
||||||
// Settings submenu.
|
// Settings submenu.
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'wp-bnb',
|
'wp-bnb',
|
||||||
@@ -483,6 +518,7 @@ final class Plugin {
|
|||||||
'edit.php?post_type=bnb_guest', // Guests.
|
'edit.php?post_type=bnb_guest', // Guests.
|
||||||
'edit.php?post_type=bnb_service', // Services.
|
'edit.php?post_type=bnb_service', // Services.
|
||||||
'wp-bnb-calendar', // Calendar.
|
'wp-bnb-calendar', // Calendar.
|
||||||
|
'wp-bnb-reports', // Reports.
|
||||||
'wp-bnb-seasons', // Seasons.
|
'wp-bnb-seasons', // Seasons.
|
||||||
'wp-bnb-settings', // Settings (always last).
|
'wp-bnb-settings', // Settings (always last).
|
||||||
);
|
);
|
||||||
@@ -528,39 +564,16 @@ final class Plugin {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function render_dashboard_page(): void {
|
public function render_dashboard_page(): void {
|
||||||
$license_valid = LicenseManager::is_license_valid();
|
DashboardAdmin::render();
|
||||||
$is_localhost = LicenseManager::is_localhost();
|
}
|
||||||
?>
|
|
||||||
<div class="wrap">
|
|
||||||
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
|
||||||
|
|
||||||
<?php if ( $is_localhost ) : ?>
|
/**
|
||||||
<div class="notice notice-info">
|
* Render reports page.
|
||||||
<p>
|
*
|
||||||
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
* @return void
|
||||||
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
*/
|
||||||
<?php esc_html_e( 'You are running on a local development environment. All features are enabled.', 'wp-bnb' ); ?>
|
public function render_reports_page(): void {
|
||||||
</p>
|
ReportsAdmin::render();
|
||||||
</div>
|
|
||||||
<?php elseif ( ! $license_valid ) : ?>
|
|
||||||
<div class="notice notice-warning">
|
|
||||||
<p>
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %s: Link to settings page */
|
|
||||||
esc_html__( 'Your license is not active. Please %s to unlock all features.', 'wp-bnb' ),
|
|
||||||
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=license' ) ) . '">' . esc_html__( 'activate your license', 'wp-bnb' ) . '</a>'
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="wp-bnb-dashboard">
|
|
||||||
<p><?php esc_html_e( 'Welcome to WP BnB Management. Use the menu on the left to manage your buildings, rooms, bookings, and guests.', 'wp-bnb' ); ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP BnB Management
|
* Plugin Name: WP BnB Management
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||||
* Version: 0.7.2
|
* Version: 0.8.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 8.3
|
* Requires PHP: 8.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin version constant - MUST match Version in header above.
|
// Plugin version constant - MUST match Version in header above.
|
||||||
define( 'WP_BNB_VERSION', '0.7.2' );
|
define( 'WP_BNB_VERSION', '0.8.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user