399 lines
13 KiB
Markdown
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
|