stats-module-development
QloApps Stats Module Development
Create statistics modules that appear under Stats > Stats in the QloApps back office.
Quick Commands
# Stats module structure (single-file module)
modules/stats{modulename}/
├── stats{modulename}.php # Main module class
├── config.xml # Module metadata
├── index.php # Security file (every folder)
└── logo.png # Module icon (optional)
Common operations:
- Create tabular report (ModuleGrid) -- See module-grid-pattern.md
- Create chart/graph (ModuleGraph) -- See module-graph-pattern.md
- Hotel restriction and SQL patterns -- See query-patterns.md
- UI layout and performance rules -- See ui-performance.md
When to Use This Skill
Applies to:
- Creating statistics/analytics modules for the Stats dashboard
- Building reports (occupancy, revenue, bookings, customers)
- Creating charts and trend visualizations
- Building complex dashboard widgets under AdminStats
When NOT to Use
- Feature modules →
module-developmentskill - Payment modules →
payment-module-developmentskill
Critical Rules
1. Data Consistency (Non-Negotiable)
When a stats module calculates a metric that already exists in QloApps (in a KPI, dashboard widget, or another stats module), the calculation logic MUST be consistent.
Canonical source: controllers/admin/AdminStatsController.php contains 80+ static methods that serve KPIs and dashboard modules. Before writing any calculation, check if AdminStatsController already computes it.
Consistency means:
- Same tables used
- Same columns used
- Same JOIN conditions
- Same WHERE filters (e.g.,
is_refunded = 0,o.valid = 1) - Same arithmetic operations
Ideal: Reuse the exact AdminStatsController method. Acceptable: When a report needs multiple columns from a single query for performance, replicate the same logic inline. Not acceptable: Different calculation logic that produces different numbers for the same metric.
Key canonical calculations in AdminStatsController:
getOccupancyData()-- total rooms, booked rooms, unavailable rooms, available roomsgetAverageDailyRate()-- total room revenue / total booked roomsgetAverageOccupancyRate()-- (totalBooked / totalRooms) * 100getRevenuePerAvailableRoom()-- RevPARgetRoomsRevenueForDiscreteDates()-- per-night normalization:SUM(total_price_tax_excl / conversion_rate) / DATEDIFF(date_to, date_from)getCancellationRate()-- cancelled bookings / total bookingsgetAverageLengthOfStay()-- ALOS
2. No Queries Inside Loops (Non-Negotiable)
Never execute database queries inside foreach, for, or while loops. Use JOINs, subqueries, or batch queries instead.
3. Do Not Recreate Built-in Features
The stats dashboard already provides:
| Feature | How to Access |
|---|---|
| Date range picker | $this->getDate() or ModuleGraph::getDateBetween() |
| Pagination | $this->_start, $this->_limit (ModuleGrid) |
| Sorting | $this->_sort, $this->_direction (ModuleGrid) |
| CSV export | $this->csvExport($engine_params) |
| Hotel filter | HotelBranchInformation::addHotelRestriction() |
Never create custom date pickers, pagination controls, sorting logic, or CSV generation code.
4. hookAdminStatsModules() Must Return a String
Never echo. The controller expects a return value.
5. SQL Security
- Strings:
pSQL() - Integers:
(int)cast - Column/table names in ORDER BY:
bqSQL()withValidate::IsName()check - Never use
$_GET/$_POSTdirectly
Module Types
| Type | Base Class | Use When | Rendering | Reference |
|---|---|---|---|---|
| ModuleGrid | ModuleGrid |
Tabular data (lists, rankings, reports) | Automatic via grid engine | module-grid-pattern.md |
| ModuleGraph | ModuleGraph |
Charts (line, column, pie) | Automatic via graph engine | module-graph-pattern.md |
| Module | Module |
Complex dashboards, multiple views, custom forms | Manual HTML | modules/statsforecast/statsforecast.php |
Decision: If the output is rows and columns, use ModuleGrid. If it is a chart, use ModuleGraph. Use plain Module only when neither fits.
Skill Components
Reference guides for each area:
- module-grid-pattern.md -- ModuleGrid class, columns, getData(), hookAdminStatsModules(), CSV export, rendering pipeline
- module-graph-pattern.md -- ModuleGraph class, graph types, setDateGraph() callback pattern, setOption(), multi-graph modules
- query-patterns.md -- Hotel restriction, revenue normalization, occupancy calculation, date filtering, SQL security, data consistency examples
- ui-performance.md -- UI layout order, hotel selector pattern, terminology, performance optimization, no-queries-in-loops patterns
Rendering Pipeline
ModuleGrid
hookAdminStatsModules() --> engine() --> iframe HTML
--> grider.php (AJAX) --> create() --> getData() --> render() --> JSON
--> JavaScript populates table
ModuleGraph
hookAdminStatsModules() --> engine() --> AJAX HTML
--> drawer.php --> create() --> getData() --> draw() --> JSON
--> NVD3 renders chart
Plain Module
hookAdminStatsModules() --> return custom HTML string directly
The grid/graph engines handle all rendering. Your module only implements getData() and hookAdminStatsModules().
Quick Reference
Module Creation Checklist
- Determine module type (ModuleGrid / ModuleGraph / Module)
- Check AdminStatsController for existing calculation logic
- Create module file extending correct base class
- Set
$this->tab = 'analytics_stats'and$this->need_instance = 0 - Register hook:
$this->registerHook('AdminStatsModules') - Implement
getData()with date filtering and hotel restriction - Set
$this->_valuesand$this->_totalCount(ModuleGrid) - Return HTML string from
hookAdminStatsModules() - Create config.xml
- Add index.php security file
- Verify no queries inside loops
- Verify data consistency with AdminStatsController
Constructor Pattern (All Types)
$this->name = 'stats{modulename}'; // lowercase, alphanumeric
$this->tab = 'analytics_stats'; // always this value
$this->version = '1.0.0';
$this->author = '{author}';
$this->need_instance = 0;
parent::__construct();
$this->displayName = $this->l('Module Name');
$this->description = $this->l('Description.');
config.xml Template
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>stats{modulename}</name>
<displayName><![CDATA[Module Name]]></displayName>
<version><![CDATA[1.0.0]]></version>
<description><![CDATA[Description.]]></description>
<author><![CDATA[{author}]]></author>
<tab><![CDATA[analytics_stats]]></tab>
<is_configurable>0</is_configurable>
<need_instance>0</need_instance>
</module>
Required Include for Hotel Data
// At top of module file (NOT inside getData)
require_once _PS_MODULE_DIR_.'hotelreservationsystem/define.php';
Common Pitfalls
- Creating custom date pickers -- Use
$this->getDate(). The system already provides a global date range picker. - Queries inside loops -- Use JOINs and subqueries. See ui-performance.md.
- Inconsistent calculations -- Check AdminStatsController before writing metric logic. See query-patterns.md.
- Echo instead of return --
hookAdminStatsModules()must return a string. - Missing hotel restriction -- Hotel-specific data must use
HotelBranchInformation::addHotelRestriction(). - Missing
_DB_PREFIX_-- All table names must use_DB_PREFIX_constant. - Forgetting
$this->_values/$this->_totalCount-- ModuleGrid requires both ingetData(). - Wrong hook name -- Must be
AdminStatsModules(or aliasdisplayAdminStatsModules).
Reference Modules
| Module | Type | Key Concepts |
|---|---|---|
modules/statsbestproducts/ |
ModuleGrid | Complex subqueries, revenue normalization, available rooms |
modules/statsbestcustomers/ |
ModuleGrid | Customer rankings, hotel restriction EXISTS pattern |
modules/statssales/ |
ModuleGraph | setOption(), multi-graph, setDateGraph() callbacks |
modules/statsvisits/ |
ModuleGraph | Simple line chart, layers |
modules/statsforecast/ |
Module | Complex dashboard, time granularity, conversion funnel |
modules/statscheckup/ |
Module | Configuration thresholds, color scoring, multiple tabs |
Core files:
classes/module/ModuleGrid.php-- Base grid class withgetData(),engine(),create()classes/module/ModuleGraph.php-- Base graph class withsetDateGraph(),setOption()controllers/admin/AdminStatsController.php-- Canonical calculation engine (80+ methods)controllers/admin/AdminStatsTabController.php-- Stats page controller, date picker, module loadingadminhtl/grider.php-- AJAX entry point for grid renderingadminhtl/drawer.php-- AJAX entry point for graph rendering
Development Workflow
- Plan -- Determine module type, identify data sources, check AdminStatsController for existing calculations
- Setup -- Create module directory, main file, config.xml, index.php
- Implement -- Write
getData()with proper date filtering, hotel restriction, and SQL security - Validate -- Verify data matches AdminStatsController calculations, no queries in loops, proper escaping
- Test -- Install module, verify in Stats dashboard, test date range changes, CSV export, sorting/pagination
Troubleshooting
- Module not showing in sidebar -- Verify
registerHook('AdminStatsModules')ininstall(), reinstall module, clearcache/class_index.php. - Grid shows empty -- Check
$this->_valuesis set ingetData(), verify SQL returns data, check browser console for AJAX errors. - Graph not rendering -- Verify
graphnvd3module is installed and active, check$this->_valuesarray structure. - CSV export broken -- Ensure
$engine_paramsincsvExport()matches the ones passed toengine().
Additional Resources
- QloApps DevDocs: https://devdocs.qloapps.com/
More from qloapps/agent-skills
module-development
Create, build, or modify QloApps modules. Covers hooks-first architecture, ObjectModel classes, admin and front controllers, Smarty templates, database operations, security validation, and deployment. Use for any module development task including creating new modules, adding hooks, building admin pages, writing database models, or preparing modules for distribution.
12payment-module-development
Use whenever creating, modifying, or debugging payment modules in QloApps — offline methods and online payment gateways. Covers PaymentModule class, payment hooks, checkout integration, validateOrder flow, API credentials, webhook handling, transaction logging, and refunds.
11