Teren-app/REPORTS_BACKEND_REWORK_PLAN.md
2026-01-02 12:32:20 +01:00

399 lines
13 KiB
Markdown

# 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