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

13 KiB

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

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.

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.

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.

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.

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.

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

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:

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:

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:
    $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