# Reports Backend Rework Plan ## Overview Transform the current hardcoded report system into a flexible, database-driven architecture that allows dynamic report configuration without code changes. ## Current Architecture Analysis ### Existing Structure - **Report Classes**: Individual PHP classes (`ActiveContractsReport`, `ActivitiesPerPeriodReport`, etc.) - **Registry Pattern**: `ReportRegistry` stores report instances in memory - **Service Provider**: `ReportServiceProvider` registers reports at boot time - **Base Class**: `BaseEloquentReport` provides common pagination logic - **Contract Interface**: `Report` interface defines required methods (`slug`, `name`, `description`, `inputs`, `columns`, `query`) - **Controller**: `ReportController` handles index, show, data, export routes ### Current Features 1. **Report Definition**: Each report defines: - Slug (unique identifier) - Name & Description - Input parameters (filters) - Column definitions - Eloquent query builder 2. **Filter Types**: `date`, `string`, `select:client`, etc. 3. **Export**: PDF and CSV export functionality 4. **Pagination**: Server-side pagination support ## Proposed New Architecture ### 1. Database Schema #### `reports` Table ```sql CREATE TABLE reports ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, slug VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, description TEXT NULL, category VARCHAR(100) NULL, -- e.g., 'contracts', 'activities', 'financial' enabled BOOLEAN DEFAULT TRUE, order INT DEFAULT 0, created_at TIMESTAMP, updated_at TIMESTAMP, INDEX idx_slug (slug), INDEX idx_enabled_order (enabled, order) ); ``` #### `report_entities` Table Defines which database entities (models) the report queries. ```sql CREATE TABLE report_entities ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, report_id BIGINT UNSIGNED NOT NULL, model_class VARCHAR(255) NOT NULL, -- e.g., 'App\Models\Contract' alias VARCHAR(50) NULL, -- table alias for joins join_type ENUM('base', 'join', 'leftJoin', 'rightJoin') DEFAULT 'base', join_first VARCHAR(100) NULL, -- first column for join join_operator VARCHAR(10) NULL, -- =, !=, etc. join_second VARCHAR(100) NULL, -- second column for join order INT DEFAULT 0, created_at TIMESTAMP, updated_at TIMESTAMP, FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, INDEX idx_report_order (report_id, order) ); ``` #### `report_columns` Table Defines selectable columns and their presentation. ```sql CREATE TABLE report_columns ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, report_id BIGINT UNSIGNED NOT NULL, key VARCHAR(100) NOT NULL, -- column identifier label VARCHAR(255) NOT NULL, -- display label type VARCHAR(50) DEFAULT 'string', -- string, number, date, boolean, currency expression TEXT NOT NULL, -- SQL expression or column path sortable BOOLEAN DEFAULT TRUE, visible BOOLEAN DEFAULT TRUE, order INT DEFAULT 0, format_options JSON NULL, -- { "decimals": 2, "prefix": "$" } created_at TIMESTAMP, updated_at TIMESTAMP, FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, INDEX idx_report_order (report_id, order) ); ``` #### `report_filters` Table Defines available filter parameters. ```sql CREATE TABLE report_filters ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, report_id BIGINT UNSIGNED NOT NULL, key VARCHAR(100) NOT NULL, -- filter identifier label VARCHAR(255) NOT NULL, type VARCHAR(50) NOT NULL, -- date, string, select, multiselect, number, boolean nullable BOOLEAN DEFAULT TRUE, default_value TEXT NULL, options JSON NULL, -- For select/multiselect: [{"label":"...", "value":"..."}] data_source VARCHAR(255) NULL, -- e.g., 'clients', 'segments' for dynamic selects order INT DEFAULT 0, created_at TIMESTAMP, updated_at TIMESTAMP, FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, INDEX idx_report_order (report_id, order) ); ``` #### `report_conditions` Table Defines WHERE clause conditions (rules) for filtering data. ```sql CREATE TABLE report_conditions ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, report_id BIGINT UNSIGNED NOT NULL, column VARCHAR(255) NOT NULL, -- e.g., 'contracts.start_date' operator VARCHAR(50) NOT NULL, -- =, !=, >, <, >=, <=, LIKE, IN, BETWEEN, IS NULL, etc. value_type VARCHAR(50) NOT NULL, -- static, filter, expression value TEXT NULL, -- static value or expression filter_key VARCHAR(100) NULL, -- references report_filters.key logical_operator ENUM('AND', 'OR') DEFAULT 'AND', group_id INT NULL, -- for grouping conditions (AND within group, OR between groups) order INT DEFAULT 0, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP, updated_at TIMESTAMP, FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, INDEX idx_report_group (report_id, group_id, order) ); ``` #### `report_orders` Table Defines default ORDER BY clauses. ```sql CREATE TABLE report_orders ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, report_id BIGINT UNSIGNED NOT NULL, column VARCHAR(255) NOT NULL, direction ENUM('ASC', 'DESC') DEFAULT 'ASC', order INT DEFAULT 0, created_at TIMESTAMP, updated_at TIMESTAMP, FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, INDEX idx_report_order (report_id, order) ); ``` ### 2. Model Structure #### Report Model ```php class Report extends Model { protected $fillable = ['slug', 'name', 'description', 'category', 'enabled', 'order']; protected $casts = ['enabled' => 'boolean']; public function entities(): HasMany { return $this->hasMany(ReportEntity::class); } public function columns(): HasMany { return $this->hasMany(ReportColumn::class); } public function filters(): HasMany { return $this->hasMany(ReportFilter::class); } public function conditions(): HasMany { return $this->hasMany(ReportCondition::class); } public function orders(): HasMany { return $this->hasMany(ReportOrder::class); } } ``` ### 3. Query Builder Service Create `ReportQueryBuilder` service to dynamically construct queries: ```php class ReportQueryBuilder { public function build(Report $report, array $filters = []): Builder { // 1. Start with base model query // 2. Apply joins from report_entities // 3. Select columns from report_columns // 4. Apply conditions from report_conditions // 5. Apply filter values to parameterized conditions // 6. Apply ORDER BY from report_orders // 7. Return Builder instance } } ``` ### 4. Backward Compatibility Layer Keep existing Report classes but load from database: ```php class DatabaseReport extends BaseEloquentReport implements Report { public function __construct(protected Report $dbReport) {} public function slug(): string { return $this->dbReport->slug; } public function name(): string { return $this->dbReport->name; } public function description(): ?string { return $this->dbReport->description; } public function inputs(): array { return $this->dbReport->filters() ->orderBy('order') ->get() ->map(fn($f) => [ 'key' => $f->key, 'type' => $f->type, 'label' => $f->label, 'nullable' => $f->nullable, 'default' => $f->default_value, 'options' => $f->options, ]) ->toArray(); } public function columns(): array { return $this->dbReport->columns() ->where('visible', true) ->orderBy('order') ->get() ->map(fn($c) => ['key' => $c->key, 'label' => $c->label]) ->toArray(); } public function query(array $filters): Builder { return app(ReportQueryBuilder::class)->build($this->dbReport, $filters); } } ``` ### 5. Migration Strategy #### Phase 1: Database Setup 1. Create migrations for all new tables 2. Create models with relationships 3. Create seeders to migrate existing hardcoded reports #### Phase 2: Service Layer 1. Build `ReportQueryBuilder` service 2. Build `DatabaseReport` adapter class 3. Update `ReportRegistry` to load from database 4. Create report management CRUD (admin UI) #### Phase 3: Testing & Validation 1. Unit tests for query builder 2. Integration tests comparing old vs new results 3. Performance benchmarks 4. Export functionality validation #### Phase 4: Migration Seeder 1. Create seeder that converts each hardcoded report into database records 2. Example for `ActiveContractsReport`: ```php $report = Report::create([ 'slug' => 'active-contracts', 'name' => 'Aktivne pogodbe', 'description' => 'Pogodbe, ki so aktivne...', 'enabled' => true, ]); // Add entities (joins) $report->entities()->create([ 'model_class' => 'App\Models\Contract', 'join_type' => 'base', 'order' => 0, ]); $report->entities()->create([ 'model_class' => 'App\Models\ClientCase', 'join_type' => 'join', 'join_first' => 'contracts.client_case_id', 'join_operator' => '=', 'join_second' => 'client_cases.id', 'order' => 1, ]); // Add columns $report->columns()->create([ 'key' => 'contract_reference', 'label' => 'Pogodba', 'expression' => 'contracts.reference', 'order' => 0, ]); // Add filters $report->filters()->create([ 'key' => 'client_uuid', 'label' => 'Stranka', 'type' => 'select', 'data_source' => 'clients', 'nullable' => true, 'order' => 0, ]); // Add conditions $report->conditions()->create([ 'column' => 'contracts.start_date', 'operator' => '<=', 'value_type' => 'expression', 'value' => 'CURRENT_DATE', 'group_id' => 1, 'order' => 0, ]); ``` #### Phase 5: Remove Old Report System Once the new database-driven system is validated and working: 1. **Delete Hardcoded Report Classes**: - Remove `app/Reports/ActiveContractsReport.php` - Remove `app/Reports/ActivitiesPerPeriodReport.php` - Remove `app/Reports/ActionsDecisionsCountReport.php` - Remove `app/Reports/DecisionsCountReport.php` - Remove `app/Reports/FieldJobsCompletedReport.php` - Remove `app/Reports/SegmentActivityCountsReport.php` 2. **Remove Base Classes/Interfaces** (if no longer needed): - Remove `app/Reports/BaseEloquentReport.php` - Remove `app/Reports/Contracts/Report.php` interface 3. **Remove/Update Service Provider**: - Remove `app/Providers/ReportServiceProvider.php` - Or update it to only load reports from database 4. **Update ReportRegistry**: - Modify to load from database instead of manual registration - Remove all hardcoded `register()` calls 5. **Clean Up Config**: - Remove any report-specific configuration files if they exist - Update `bootstrap/providers.php` to remove ReportServiceProvider 6. **Documentation Cleanup**: - Update any documentation referencing old report classes - Add migration guide for future report creation ### 6. Admin UI for Report Management Create CRUD interface at `Settings/Reports/*`: - **Index**: List all reports with enable/disable toggle - **Create**: Wizard-style form for building new reports - **Edit**: Visual query builder interface - **Test**: Preview report results - **Clone**: Duplicate existing report as starting point ### 7. Advanced Features (Future) 1. **Calculated Fields**: Allow expressions like `(column_a + column_b) / 2` 2. **Aggregations**: Support SUM, AVG, COUNT, MIN, MAX 3. **Subqueries**: Define subquery relationships 4. **Report Templates**: Predefined report structures 5. **Scheduled Reports**: Email reports on schedule 6. **Report Sharing**: Share reports with specific users/roles 7. **Version History**: Track report definition changes 8. **Report Permissions**: Control who can view/edit reports ## Benefits 1. **No Code Changes**: Add/modify reports through UI 2. **Flexibility**: Non-developers can create reports 3. **Consistency**: All reports follow same structure 4. **Maintainability**: Centralized report logic 5. **Reusability**: Share entities, filters, conditions 6. **Version Control**: Track changes to report definitions 7. **Performance**: Optimize query builder once 8. **Export**: Works with any report automatically ## Risks & Considerations 1. **Complexity**: Query builder must handle diverse SQL patterns 2. **Performance**: Dynamic query building overhead 3. **Security**: SQL injection risks with user input 4. **Learning Curve**: Team needs to understand new system 5. **Testing**: Comprehensive test suite required 6. **Migration**: Convert all existing reports correctly 7. **Edge Cases**: Complex queries may be difficult to represent ## Timeline Estimate - **Phase 1 (Database)**: 2-3 days - **Phase 2 (Services)**: 4-5 days - **Phase 3 (Testing)**: 2-3 days - **Phase 4 (Migration)**: 1-2 days - **Phase 5 (Cleanup)**: 1 day - **Admin UI**: 3-4 days - **Total**: 13-18 days ## Success Criteria 1. ✅ All existing reports work identically 2. ✅ New reports can be created via UI 3. ✅ Export functionality preserved 4. ✅ Performance within 10% of current 5. ✅ Zero SQL injection vulnerabilities 6. ✅ Comprehensive test coverage (>80%) 7. ✅ Documentation complete