New report system and views
This commit is contained in:
@@ -827,6 +827,6 @@ public function destroy(Request $request, Import $import)
|
||||
|
||||
$import->delete();
|
||||
|
||||
return back()->with(['ok' => true]);
|
||||
return back()->with('success', 'Import deleted successfully');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Reports\ReportRegistry;
|
||||
use App\Models\Report;
|
||||
use App\Services\ReportQueryBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -10,15 +11,19 @@
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function __construct(protected ReportRegistry $registry) {}
|
||||
public function __construct(protected ReportQueryBuilder $queryBuilder) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$reports = collect($this->registry->all())
|
||||
$reports = Report::where('enabled', true)
|
||||
->orderBy('order')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn ($r) => [
|
||||
'slug' => $r->slug(),
|
||||
'name' => $r->name(),
|
||||
'description' => $r->description(),
|
||||
'slug' => $r->slug,
|
||||
'name' => $r->name,
|
||||
'description' => $r->description,
|
||||
'category' => $r->category,
|
||||
])
|
||||
->values();
|
||||
|
||||
@@ -29,26 +34,30 @@ public function index(Request $request)
|
||||
|
||||
public function show(string $slug, Request $request)
|
||||
{
|
||||
$report = $this->registry->findBySlug($slug);
|
||||
abort_if(! $report, 404);
|
||||
$report->authorize($request);
|
||||
$report = Report::with(['filters', 'columns'])
|
||||
->where('slug', $slug)
|
||||
->where('enabled', true)
|
||||
->firstOrFail();
|
||||
|
||||
// Accept filters & pagination from query and return initial data for server-driven table
|
||||
$filters = $this->validateFilters($report->inputs(), $request);
|
||||
$inputs = $this->buildInputsArray($report);
|
||||
$filters = $this->validateFilters($inputs, $request);
|
||||
\Log::info('Report filters', ['filters' => $filters, 'request' => $request->all()]);
|
||||
|
||||
$perPage = (int) ($request->integer('per_page') ?: 25);
|
||||
$paginator = $report->paginate($filters, $perPage);
|
||||
$query = $this->queryBuilder->build($report, $filters);
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
$rows = collect($paginator->items())
|
||||
->map(fn ($row) => $this->normalizeRow($row))
|
||||
->values();
|
||||
|
||||
return Inertia::render('Reports/Show', [
|
||||
'slug' => $report->slug(),
|
||||
'name' => $report->name(),
|
||||
'description' => $report->description(),
|
||||
'inputs' => $report->inputs(),
|
||||
'columns' => $report->columns(),
|
||||
'slug' => $report->slug,
|
||||
'name' => $report->name,
|
||||
'description' => $report->description,
|
||||
'inputs' => $inputs,
|
||||
'columns' => $this->buildColumnsArray($report),
|
||||
'rows' => $rows,
|
||||
'meta' => [
|
||||
'total' => $paginator->total(),
|
||||
@@ -62,14 +71,17 @@ public function show(string $slug, Request $request)
|
||||
|
||||
public function data(string $slug, Request $request)
|
||||
{
|
||||
$report = $this->registry->findBySlug($slug);
|
||||
abort_if(! $report, 404);
|
||||
$report->authorize($request);
|
||||
$report = Report::with(['filters', 'columns'])
|
||||
->where('slug', $slug)
|
||||
->where('enabled', true)
|
||||
->firstOrFail();
|
||||
|
||||
$filters = $this->validateFilters($report->inputs(), $request);
|
||||
$inputs = $this->buildInputsArray($report);
|
||||
$filters = $this->validateFilters($inputs, $request);
|
||||
$perPage = (int) ($request->integer('per_page') ?: 25);
|
||||
|
||||
$paginator = $report->paginate($filters, $perPage);
|
||||
$query = $this->queryBuilder->build($report, $filters);
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
$rows = collect($paginator->items())
|
||||
->map(fn ($row) => $this->normalizeRow($row))
|
||||
@@ -85,20 +97,23 @@ public function data(string $slug, Request $request)
|
||||
|
||||
public function export(string $slug, Request $request)
|
||||
{
|
||||
$report = $this->registry->findBySlug($slug);
|
||||
abort_if(! $report, 404);
|
||||
$report->authorize($request);
|
||||
$report = Report::with(['filters', 'columns'])
|
||||
->where('slug', $slug)
|
||||
->where('enabled', true)
|
||||
->firstOrFail();
|
||||
|
||||
$filters = $this->validateFilters($report->inputs(), $request);
|
||||
$inputs = $this->buildInputsArray($report);
|
||||
$filters = $this->validateFilters($inputs, $request);
|
||||
$format = strtolower((string) $request->get('format', 'csv'));
|
||||
|
||||
$rows = $report->query($filters)->get()->map(fn ($row) => $this->normalizeRow($row));
|
||||
$columns = $report->columns();
|
||||
$filename = $report->slug().'-'.now()->format('Ymd_His');
|
||||
$query = $this->queryBuilder->build($report, $filters);
|
||||
$rows = $query->get()->map(fn ($row) => $this->normalizeRow($row));
|
||||
$columns = $this->buildColumnsArray($report);
|
||||
$filename = $report->slug.'-'.now()->format('Ymd_His');
|
||||
|
||||
if ($format === 'pdf') {
|
||||
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('reports.pdf.table', [
|
||||
'name' => $report->name(),
|
||||
'name' => $report->name,
|
||||
'columns' => $columns,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
@@ -299,6 +314,35 @@ protected function validateFilters(array $inputs, Request $request): array
|
||||
return $request->validate($rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build inputs array from report filters.
|
||||
*/
|
||||
protected function buildInputsArray(Report $report): array
|
||||
{
|
||||
return $report->filters->map(fn($filter) => [
|
||||
'key' => $filter->key,
|
||||
'type' => $filter->type,
|
||||
'label' => $filter->label,
|
||||
'nullable' => $filter->nullable,
|
||||
'default' => $filter->default_value,
|
||||
'options' => $filter->options,
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build columns array from report columns.
|
||||
*/
|
||||
protected function buildColumnsArray(Report $report): array
|
||||
{
|
||||
return $report->columns
|
||||
->where('visible', true)
|
||||
->map(fn($col) => [
|
||||
'key' => $col->key,
|
||||
'label' => $col->label,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure derived export/display fields exist on row objects.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportEntity;
|
||||
use App\Models\ReportColumn;
|
||||
use App\Models\ReportFilter;
|
||||
use App\Models\ReportCondition;
|
||||
use App\Models\ReportOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ReportSettingsController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$reports = Report::orderBy('order')->orderBy('name')->get();
|
||||
|
||||
return Inertia::render('Settings/Reports/Index', [
|
||||
'reports' => $reports,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Report $report)
|
||||
{
|
||||
$report->load(['entities', 'columns', 'filters', 'conditions', 'orders']);
|
||||
|
||||
return Inertia::render('Settings/Reports/Edit', [
|
||||
'report' => $report,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'slug' => 'required|string|unique:reports,slug|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'category' => 'nullable|string|max:100',
|
||||
'enabled' => 'boolean',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$report = Report::create($validated);
|
||||
|
||||
return redirect()->route('settings.reports.index')
|
||||
->with('success', 'Report created successfully.');
|
||||
}
|
||||
|
||||
public function update(Request $request, Report $report)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'slug' => 'required|string|unique:reports,slug,' . $report->id . '|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'category' => 'nullable|string|max:100',
|
||||
'enabled' => 'boolean',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$report->update($validated);
|
||||
|
||||
return redirect()->route('settings.reports.index')
|
||||
->with('success', 'Report updated successfully.');
|
||||
}
|
||||
|
||||
public function destroy(Report $report)
|
||||
{
|
||||
$report->delete();
|
||||
|
||||
return redirect()->route('settings.reports.index')
|
||||
->with('success', 'Report deleted successfully.');
|
||||
}
|
||||
|
||||
public function toggleEnabled(Report $report)
|
||||
{
|
||||
$report->update(['enabled' => !$report->enabled]);
|
||||
|
||||
return back()->with('success', 'Report status updated.');
|
||||
}
|
||||
|
||||
// Entity CRUD
|
||||
public function storeEntity(Request $request, Report $report)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_class' => 'required|string|max:255',
|
||||
'alias' => 'nullable|string|max:50',
|
||||
'join_type' => 'required|in:base,join,leftJoin,rightJoin',
|
||||
'join_first' => 'nullable|string|max:100',
|
||||
'join_operator' => 'nullable|string|max:10',
|
||||
'join_second' => 'nullable|string|max:100',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$report->entities()->create($validated);
|
||||
|
||||
return back()->with('success', 'Entity added successfully.');
|
||||
}
|
||||
|
||||
public function updateEntity(Request $request, ReportEntity $entity)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_class' => 'required|string|max:255',
|
||||
'alias' => 'nullable|string|max:50',
|
||||
'join_type' => 'required|in:base,join,leftJoin,rightJoin',
|
||||
'join_first' => 'nullable|string|max:100',
|
||||
'join_operator' => 'nullable|string|max:10',
|
||||
'join_second' => 'nullable|string|max:100',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$entity->update($validated);
|
||||
|
||||
return back()->with('success', 'Entity updated successfully.');
|
||||
}
|
||||
|
||||
public function destroyEntity(ReportEntity $entity)
|
||||
{
|
||||
$entity->delete();
|
||||
|
||||
return back()->with('success', 'Entity deleted successfully.');
|
||||
}
|
||||
|
||||
// Column CRUD
|
||||
public function storeColumn(Request $request, Report $report)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key' => 'required|string|max:100',
|
||||
'label' => 'required|string|max:255',
|
||||
'type' => 'required|string|max:50',
|
||||
'expression' => 'required|string',
|
||||
'sortable' => 'boolean',
|
||||
'visible' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'format_options' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$report->columns()->create($validated);
|
||||
|
||||
return back()->with('success', 'Column added successfully.');
|
||||
}
|
||||
|
||||
public function updateColumn(Request $request, ReportColumn $column)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key' => 'required|string|max:100',
|
||||
'label' => 'required|string|max:255',
|
||||
'type' => 'required|string|max:50',
|
||||
'expression' => 'required|string',
|
||||
'sortable' => 'boolean',
|
||||
'visible' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'format_options' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$column->update($validated);
|
||||
|
||||
return back()->with('success', 'Column updated successfully.');
|
||||
}
|
||||
|
||||
public function destroyColumn(ReportColumn $column)
|
||||
{
|
||||
$column->delete();
|
||||
|
||||
return back()->with('success', 'Column deleted successfully.');
|
||||
}
|
||||
|
||||
// Filter CRUD
|
||||
public function storeFilter(Request $request, Report $report)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key' => 'required|string|max:100',
|
||||
'label' => 'required|string|max:255',
|
||||
'type' => 'required|string|max:50',
|
||||
'nullable' => 'boolean',
|
||||
'default_value' => 'nullable|string',
|
||||
'options' => 'nullable|array',
|
||||
'data_source' => 'nullable|string|max:255',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$report->filters()->create($validated);
|
||||
|
||||
return back()->with('success', 'Filter added successfully.');
|
||||
}
|
||||
|
||||
public function updateFilter(Request $request, ReportFilter $filter)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key' => 'required|string|max:100',
|
||||
'label' => 'required|string|max:255',
|
||||
'type' => 'required|string|max:50',
|
||||
'nullable' => 'boolean',
|
||||
'default_value' => 'nullable|string',
|
||||
'options' => 'nullable|array',
|
||||
'data_source' => 'nullable|string|max:255',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$filter->update($validated);
|
||||
|
||||
return back()->with('success', 'Filter updated successfully.');
|
||||
}
|
||||
|
||||
public function destroyFilter(ReportFilter $filter)
|
||||
{
|
||||
$filter->delete();
|
||||
|
||||
return back()->with('success', 'Filter deleted successfully.');
|
||||
}
|
||||
|
||||
// Condition CRUD
|
||||
public function storeCondition(Request $request, Report $report)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'column' => 'required|string|max:255',
|
||||
'operator' => 'required|string|max:50',
|
||||
'value_type' => 'required|in:static,filter,expression',
|
||||
'value' => 'nullable|string',
|
||||
'filter_key' => 'nullable|string|max:100',
|
||||
'logical_operator' => 'required|in:AND,OR',
|
||||
'group_id' => 'nullable|integer',
|
||||
'order' => 'integer',
|
||||
'enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$report->conditions()->create($validated);
|
||||
|
||||
return back()->with('success', 'Condition added successfully.');
|
||||
}
|
||||
|
||||
public function updateCondition(Request $request, ReportCondition $condition)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'column' => 'required|string|max:255',
|
||||
'operator' => 'required|string|max:50',
|
||||
'value_type' => 'required|in:static,filter,expression',
|
||||
'value' => 'nullable|string',
|
||||
'filter_key' => 'nullable|string|max:100',
|
||||
'logical_operator' => 'required|in:AND,OR',
|
||||
'group_id' => 'nullable|integer',
|
||||
'order' => 'integer',
|
||||
'enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$condition->update($validated);
|
||||
|
||||
return back()->with('success', 'Condition updated successfully.');
|
||||
}
|
||||
|
||||
public function destroyCondition(ReportCondition $condition)
|
||||
{
|
||||
$condition->delete();
|
||||
|
||||
return back()->with('success', 'Condition deleted successfully.');
|
||||
}
|
||||
|
||||
// Order CRUD
|
||||
public function storeOrder(Request $request, Report $report)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'column' => 'required|string|max:255',
|
||||
'direction' => 'required|in:ASC,DESC',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$report->orders()->create($validated);
|
||||
|
||||
return back()->with('success', 'Order clause added successfully.');
|
||||
}
|
||||
|
||||
public function updateOrder(Request $request, ReportOrder $order)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'column' => 'required|string|max:255',
|
||||
'direction' => 'required|in:ASC,DESC',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$order->update($validated);
|
||||
|
||||
return back()->with('success', 'Order clause updated successfully.');
|
||||
}
|
||||
|
||||
public function destroyOrder(ReportOrder $order)
|
||||
{
|
||||
$order->delete();
|
||||
|
||||
return back()->with('success', 'Order clause deleted successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Report extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
'description',
|
||||
'category',
|
||||
'enabled',
|
||||
'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'boolean',
|
||||
'order' => 'integer',
|
||||
];
|
||||
|
||||
public function entities(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportEntity::class)->orderBy('order');
|
||||
}
|
||||
|
||||
public function columns(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportColumn::class)->orderBy('order');
|
||||
}
|
||||
|
||||
public function filters(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportFilter::class)->orderBy('order');
|
||||
}
|
||||
|
||||
public function conditions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportCondition::class)->orderBy('order');
|
||||
}
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportOrder::class)->orderBy('order');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportColumn extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'key',
|
||||
'label',
|
||||
'type',
|
||||
'expression',
|
||||
'sortable',
|
||||
'visible',
|
||||
'order',
|
||||
'format_options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sortable' => 'boolean',
|
||||
'visible' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'format_options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportCondition extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'column',
|
||||
'operator',
|
||||
'value_type',
|
||||
'value',
|
||||
'filter_key',
|
||||
'logical_operator',
|
||||
'group_id',
|
||||
'order',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'group_id' => 'integer',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportEntity extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'model_class',
|
||||
'alias',
|
||||
'join_type',
|
||||
'join_first',
|
||||
'join_operator',
|
||||
'join_second',
|
||||
'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'order' => 'integer',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportFilter extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'key',
|
||||
'label',
|
||||
'type',
|
||||
'nullable',
|
||||
'default_value',
|
||||
'options',
|
||||
'data_source',
|
||||
'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'nullable' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportOrder extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'column',
|
||||
'direction',
|
||||
'order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'order' => 'integer',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Reports\ActionsDecisionsCountReport;
|
||||
use App\Reports\ActivitiesPerPeriodReport;
|
||||
use App\Reports\ActiveContractsReport;
|
||||
use App\Reports\FieldJobsCompletedReport;
|
||||
use App\Reports\DecisionsCountReport;
|
||||
use App\Reports\ReportRegistry;
|
||||
use App\Reports\SegmentActivityCountsReport;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ReportServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(ReportRegistry::class, function () {
|
||||
$registry = new ReportRegistry;
|
||||
// Register built-in reports here
|
||||
$registry->register(new FieldJobsCompletedReport);
|
||||
$registry->register(new SegmentActivityCountsReport);
|
||||
$registry->register(new ActionsDecisionsCountReport);
|
||||
$registry->register(new ActivitiesPerPeriodReport);
|
||||
$registry->register(new DecisionsCountReport);
|
||||
$registry->register(new ActiveContractsReport);
|
||||
|
||||
return $registry;
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Reports\Contracts\Report;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ActionsDecisionsCountReport extends BaseEloquentReport implements Report
|
||||
{
|
||||
public function slug(): string
|
||||
{
|
||||
return 'actions-decisions-counts';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'Dejanja / Odločitve – štetje';
|
||||
}
|
||||
|
||||
public function description(): ?string
|
||||
{
|
||||
return 'Število aktivnosti po dejanjih in odločitvah v obdobju.';
|
||||
}
|
||||
|
||||
public function inputs(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
|
||||
['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
|
||||
];
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'action_name', 'label' => 'Dejanje'],
|
||||
['key' => 'decision_name', 'label' => 'Odločitev'],
|
||||
['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $filters): Builder
|
||||
{
|
||||
return Activity::query()
|
||||
->leftJoin('actions', 'activities.action_id', '=', 'actions.id')
|
||||
->leftJoin('decisions', 'activities.decision_id', '=', 'decisions.id')
|
||||
->when(! empty($filters['from']), fn ($q) => $q->whereDate('activities.created_at', '>=', $filters['from']))
|
||||
->when(! empty($filters['to']), fn ($q) => $q->whereDate('activities.created_at', '<=', $filters['to']))
|
||||
->groupBy('actions.name', 'decisions.name')
|
||||
->selectRaw("COALESCE(actions.name, '—') as action_name, COALESCE(decisions.name, '—') as decision_name, COUNT(*) as activities_count");
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Reports\Contracts\Report;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ActiveContractsReport extends BaseEloquentReport implements Report
|
||||
{
|
||||
public function slug(): string
|
||||
{
|
||||
return 'active-contracts';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'Aktivne pogodbe';
|
||||
}
|
||||
|
||||
public function description(): ?string
|
||||
{
|
||||
return 'Pogodbe, ki so aktivne na izbrani dan, z možnostjo filtriranja po stranki.';
|
||||
}
|
||||
|
||||
public function inputs(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'client_uuid', 'type' => 'select:client', 'label' => 'Stranka', 'nullable' => true],
|
||||
];
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'contract_reference', 'label' => 'Pogodba'],
|
||||
['key' => 'client_name', 'label' => 'Stranka'],
|
||||
['key' => 'person_name', 'label' => 'Zadeva (oseba)'],
|
||||
['key' => 'start_date', 'label' => 'Začetek'],
|
||||
['key' => 'end_date', 'label' => 'Konec'],
|
||||
['key' => 'balance_amount', 'label' => 'Saldo'],
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $filters): Builder
|
||||
{
|
||||
$asOf = now()->toDateString();
|
||||
|
||||
return Contract::query()
|
||||
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
|
||||
->leftJoin('clients', 'client_cases.client_id', '=', 'clients.id')
|
||||
->leftJoin('person as client_people', 'clients.person_id', '=', 'client_people.id')
|
||||
->leftJoin('person as subject_people', 'client_cases.person_id', '=', 'subject_people.id')
|
||||
->leftJoin('accounts', 'contracts.id', '=', 'accounts.contract_id')
|
||||
->when(! empty($filters['client_uuid']), fn ($q) => $q->where('clients.uuid', $filters['client_uuid']))
|
||||
// Active as of date: start_date <= as_of (or null) AND (end_date is null OR end_date >= as_of)
|
||||
->where(function ($q) use ($asOf) {
|
||||
$q->whereNull('contracts.start_date')
|
||||
->orWhereDate('contracts.start_date', '<=', $asOf);
|
||||
})
|
||||
->where(function ($q) use ($asOf) {
|
||||
$q->whereNull('contracts.end_date')
|
||||
->orWhereDate('contracts.end_date', '>=', $asOf);
|
||||
})
|
||||
->select([
|
||||
'contracts.id',
|
||||
'contracts.start_date',
|
||||
'contracts.end_date',
|
||||
])
|
||||
->addSelect([
|
||||
\DB::raw('contracts.reference as contract_reference'),
|
||||
\DB::raw('client_people.full_name as client_name'),
|
||||
\DB::raw('subject_people.full_name as person_name'),
|
||||
\DB::raw('CAST(accounts.balance_amount AS FLOAT) as balance_amount'),
|
||||
])
|
||||
->orderBy('contracts.start_date', 'asc');
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Reports\Contracts\Report;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ActivitiesPerPeriodReport extends BaseEloquentReport implements Report
|
||||
{
|
||||
public function slug(): string
|
||||
{
|
||||
return 'activities-per-period';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'Aktivnosti po obdobjih';
|
||||
}
|
||||
|
||||
public function description(): ?string
|
||||
{
|
||||
return 'Seštevek aktivnosti po dneh/tednih/mesecih v obdobju.';
|
||||
}
|
||||
|
||||
public function inputs(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
|
||||
['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
|
||||
['key' => 'period', 'type' => 'string', 'label' => 'Obdobje (day|week|month)', 'default' => 'day'],
|
||||
];
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'period', 'label' => 'Obdobje'],
|
||||
['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $filters): Builder
|
||||
{
|
||||
$periodRaw = $filters['period'] ?? 'day';
|
||||
$period = in_array($periodRaw, ['day', 'week', 'month'], true) ? $periodRaw : 'day';
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
// Build database-compatible period expressions
|
||||
if ($driver === 'sqlite') {
|
||||
if ($period === 'day') {
|
||||
// Use string slice to avoid timezone conversion differences in SQLite
|
||||
$selectExpr = DB::raw('SUBSTR(activities.created_at, 1, 10) as period');
|
||||
$groupExpr = DB::raw('SUBSTR(activities.created_at, 1, 10)');
|
||||
$orderExpr = DB::raw('SUBSTR(activities.created_at, 1, 10)');
|
||||
} elseif ($period === 'month') {
|
||||
$selectExpr = DB::raw("strftime('%Y-%m-01', activities.created_at) as period");
|
||||
$groupExpr = DB::raw("strftime('%Y-%m-01', activities.created_at)");
|
||||
$orderExpr = DB::raw("strftime('%Y-%m-01', activities.created_at)");
|
||||
} else { // week
|
||||
$selectExpr = DB::raw("strftime('%Y-%W', activities.created_at) as period");
|
||||
$groupExpr = DB::raw("strftime('%Y-%W', activities.created_at)");
|
||||
$orderExpr = DB::raw("strftime('%Y-%W', activities.created_at)");
|
||||
}
|
||||
} elseif ($driver === 'mysql') {
|
||||
if ($period === 'day') {
|
||||
$selectExpr = DB::raw('DATE(activities.created_at) as period');
|
||||
$groupExpr = DB::raw('DATE(activities.created_at)');
|
||||
$orderExpr = DB::raw('DATE(activities.created_at)');
|
||||
} elseif ($period === 'month') {
|
||||
$selectExpr = DB::raw("DATE_FORMAT(activities.created_at, '%Y-%m-01') as period");
|
||||
$groupExpr = DB::raw("DATE_FORMAT(activities.created_at, '%Y-%m-01')");
|
||||
$orderExpr = DB::raw("DATE_FORMAT(activities.created_at, '%Y-%m-01')");
|
||||
} else { // week
|
||||
// ISO week-year-week number for grouping; adequate for summary grouping
|
||||
$selectExpr = DB::raw("DATE_FORMAT(activities.created_at, '%x-%v') as period");
|
||||
$groupExpr = DB::raw("DATE_FORMAT(activities.created_at, '%x-%v')");
|
||||
$orderExpr = DB::raw("DATE_FORMAT(activities.created_at, '%x-%v')");
|
||||
}
|
||||
} else { // postgres and others supporting date_trunc
|
||||
$selectExpr = DB::raw("date_trunc('".$period."', activities.created_at) as period");
|
||||
$groupExpr = DB::raw("date_trunc('".$period."', activities.created_at)");
|
||||
$orderExpr = DB::raw("date_trunc('".$period."', activities.created_at)");
|
||||
}
|
||||
|
||||
return Activity::query()
|
||||
->when(! empty($filters['from']), fn ($q) => $q->whereDate('activities.created_at', '>=', $filters['from']))
|
||||
->when(! empty($filters['to']), fn ($q) => $q->whereDate('activities.created_at', '<=', $filters['to']))
|
||||
->groupBy($groupExpr)
|
||||
->orderBy($orderExpr)
|
||||
->select($selectExpr)
|
||||
->selectRaw('COUNT(*) as activities_count');
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Reports\Contracts\Report;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
abstract class BaseEloquentReport implements Report
|
||||
{
|
||||
public function description(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function authorize(Request $request): void
|
||||
{
|
||||
// Default: no extra checks. Controllers can gate via middleware.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
public function paginate(array $filters, int $perPage = 25): LengthAwarePaginator
|
||||
{
|
||||
/** @var EloquentBuilder|QueryBuilder $query */
|
||||
$query = $this->query($filters);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
interface Report
|
||||
{
|
||||
public function slug(): string;
|
||||
|
||||
public function name(): string;
|
||||
|
||||
public function description(): ?string;
|
||||
|
||||
/**
|
||||
* Return an array describing input filters (type, label, default, options) for UI.
|
||||
* Example item: ['key' => 'from', 'type' => 'date', 'label' => 'Od', 'default' => today()]
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function inputs(): array;
|
||||
|
||||
/**
|
||||
* Return column definitions for the table and exports.
|
||||
* Example: [ ['key' => 'id', 'label' => '#'], ['key' => 'user', 'label' => 'Uporabnik'] ]
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function columns(): array;
|
||||
|
||||
/**
|
||||
* Build the data source query for the report based on validated filters.
|
||||
* Should return an Eloquent or Query builder.
|
||||
*
|
||||
* @param array<string, mixed> $filters
|
||||
* @return EloquentBuilder|QueryBuilder
|
||||
*/
|
||||
public function query(array $filters);
|
||||
|
||||
/**
|
||||
* Optional per-report authorization logic.
|
||||
*/
|
||||
public function authorize(Request $request): void;
|
||||
|
||||
/**
|
||||
* Execute the report and return a paginator for UI.
|
||||
*
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
public function paginate(array $filters, int $perPage = 25): LengthAwarePaginator;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Reports\Contracts\Report;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DecisionsCountReport extends BaseEloquentReport implements Report
|
||||
{
|
||||
public function slug(): string
|
||||
{
|
||||
return 'decisions-counts';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'Odločitve – štetje';
|
||||
}
|
||||
|
||||
public function description(): ?string
|
||||
{
|
||||
return 'Število aktivnosti po odločitvah v izbranem obdobju.';
|
||||
}
|
||||
|
||||
public function inputs(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
|
||||
['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
|
||||
];
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'decision_name', 'label' => 'Odločitev'],
|
||||
['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $filters): Builder
|
||||
{
|
||||
return Activity::query()
|
||||
->leftJoin('decisions', 'activities.decision_id', '=', 'decisions.id')
|
||||
->when(!empty($filters['from']), fn ($q) => $q->whereDate('activities.created_at', '>=', $filters['from']))
|
||||
->when(!empty($filters['to']), fn ($q) => $q->whereDate('activities.created_at', '<=', $filters['to']))
|
||||
->groupBy('decisions.name')
|
||||
->selectRaw("COALESCE(decisions.name, '—') as decision_name, COUNT(*) as activities_count");
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Models\FieldJob;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
|
||||
class FieldJobsCompletedReport extends BaseEloquentReport
|
||||
{
|
||||
public function slug(): string
|
||||
{
|
||||
return 'field-jobs-completed';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'Zaključeni tereni';
|
||||
}
|
||||
|
||||
public function description(): ?string
|
||||
{
|
||||
return 'Pregled zaključenih terenov po datumu in uporabniku.';
|
||||
}
|
||||
|
||||
public function inputs(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'from', 'type' => 'date', 'label' => 'Od', 'default' => now()->startOfMonth()->toDateString()],
|
||||
['key' => 'to', 'type' => 'date', 'label' => 'Do', 'default' => now()->toDateString()],
|
||||
['key' => 'user_id', 'type' => 'select:user', 'label' => 'Uporabnik', 'default' => null],
|
||||
];
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'id', 'label' => '#'],
|
||||
['key' => 'contract_reference', 'label' => 'Pogodba'],
|
||||
['key' => 'assigned_user_name', 'label' => 'Terenski'],
|
||||
['key' => 'completed_at', 'label' => 'Zaključeno'],
|
||||
['key' => 'notes', 'label' => 'Opombe'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
public function query(array $filters): EloquentBuilder
|
||||
{
|
||||
$from = isset($filters['from']) ? now()->parse($filters['from'])->startOfDay() : now()->startOfMonth();
|
||||
$to = isset($filters['to']) ? now()->parse($filters['to'])->endOfDay() : now()->endOfDay();
|
||||
|
||||
return FieldJob::query()
|
||||
->whereNull('cancelled_at')
|
||||
->whereBetween('completed_at', [$from, $to])
|
||||
->when(! empty($filters['user_id']), fn ($q) => $q->where('assigned_user_id', $filters['user_id']))
|
||||
->with(['assignedUser:id,name', 'contract:id,reference'])
|
||||
->select(['id', 'assigned_user_id', 'contract_id', 'completed_at', 'notes']);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Reports\Contracts\Report;
|
||||
|
||||
class ReportRegistry
|
||||
{
|
||||
/** @var array<string, Report> */
|
||||
protected array $reports = [];
|
||||
|
||||
public function register(Report $report): void
|
||||
{
|
||||
$this->reports[$report->slug()] = $report;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Report>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->reports;
|
||||
}
|
||||
|
||||
public function findBySlug(string $slug): ?Report
|
||||
{
|
||||
return $this->reports[$slug] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Reports;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Reports\Contracts\Report;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SegmentActivityCountsReport extends BaseEloquentReport implements Report
|
||||
{
|
||||
public function slug(): string
|
||||
{
|
||||
return 'segment-activity-counts';
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'Aktivnosti po segmentih';
|
||||
}
|
||||
|
||||
public function description(): ?string
|
||||
{
|
||||
return 'Število aktivnosti po segmentih v izbranem obdobju (glede na segment dejanja).';
|
||||
}
|
||||
|
||||
public function inputs(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
|
||||
['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
|
||||
];
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'segment_name', 'label' => 'Segment'],
|
||||
['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
|
||||
];
|
||||
}
|
||||
|
||||
public function query(array $filters): Builder
|
||||
{
|
||||
$q = Activity::query()
|
||||
->join('actions', 'activities.action_id', '=', 'actions.id')
|
||||
->leftJoin('segments', 'actions.segment_id', '=', 'segments.id')
|
||||
->when(! empty($filters['from']), fn ($qq) => $qq->whereDate('activities.created_at', '>=', $filters['from']))
|
||||
->when(! empty($filters['to']), fn ($qq) => $qq->whereDate('activities.created_at', '<=', $filters['to']))
|
||||
->groupBy('segments.name')
|
||||
->selectRaw("COALESCE(segments.name, 'Brez segmenta') as segment_name, COUNT(*) as activities_count");
|
||||
|
||||
return $q;
|
||||
}
|
||||
}
|
||||
@@ -73,30 +73,21 @@ public function process(Import $import, array $mapped, array $raw, array $contex
|
||||
];
|
||||
}
|
||||
|
||||
$existing = $this->resolve($mapped, $context);
|
||||
|
||||
// Check for duplicates if configured
|
||||
if ($this->getOption('deduplicate', true) && $existing) {
|
||||
// Update person_id if different
|
||||
if ($existing->person_id !== $personId) {
|
||||
$existing->person_id = $personId;
|
||||
$existing->save();
|
||||
|
||||
return [
|
||||
'action' => 'updated',
|
||||
'entity' => $existing,
|
||||
'applied_fields' => ['person_id'],
|
||||
];
|
||||
}
|
||||
// Check if this email already exists for THIS person
|
||||
$existing = Email::where('person_id', $personId)
|
||||
->where('value', strtolower(trim($email)))
|
||||
->first();
|
||||
|
||||
// If email already exists for this person, skip
|
||||
if ($existing) {
|
||||
return [
|
||||
'action' => 'skipped',
|
||||
'entity' => $existing,
|
||||
'message' => 'Email already exists',
|
||||
'message' => 'Email already exists for this person',
|
||||
];
|
||||
}
|
||||
|
||||
// Create new email
|
||||
// Create new email for this person
|
||||
$payload = $this->buildPayload($mapped, new Email);
|
||||
$payload['person_id'] = $personId;
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Report;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReportQueryBuilder
|
||||
{
|
||||
/**
|
||||
* Build a query from a database-driven report configuration.
|
||||
*/
|
||||
public function build(Report $report, array $filters = []): Builder
|
||||
{
|
||||
// Load all required relationships
|
||||
$report->load(['entities', 'columns', 'conditions', 'orders']);
|
||||
|
||||
// 1. Start with base model query
|
||||
$query = $this->buildBaseQuery($report);
|
||||
|
||||
// 2. Apply joins from report_entities
|
||||
$this->applyJoins($query, $report);
|
||||
|
||||
// 3. Select columns from report_columns
|
||||
$this->applySelects($query, $report);
|
||||
|
||||
// 4. Apply GROUP BY if aggregate functions are used
|
||||
$this->applyGroupBy($query, $report);
|
||||
|
||||
// 5. Apply conditions from report_conditions
|
||||
$this->applyConditions($query, $report, $filters);
|
||||
|
||||
// 6. Apply ORDER BY from report_orders
|
||||
$this->applyOrders($query, $report);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the base query from the first entity.
|
||||
*/
|
||||
protected function buildBaseQuery(Report $report): Builder
|
||||
{
|
||||
$baseEntity = $report->entities->firstWhere('join_type', 'base');
|
||||
|
||||
if (!$baseEntity) {
|
||||
throw new \RuntimeException("Report {$report->slug} has no base entity defined.");
|
||||
}
|
||||
|
||||
$modelClass = $baseEntity->model_class;
|
||||
|
||||
if (!class_exists($modelClass)) {
|
||||
throw new \RuntimeException("Model class {$modelClass} does not exist.");
|
||||
}
|
||||
|
||||
return $modelClass::query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply joins from report entities.
|
||||
*/
|
||||
protected function applyJoins(Builder $query, Report $report): void
|
||||
{
|
||||
$entities = $report->entities->where('join_type', '!=', 'base');
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$table = $this->getTableFromModel($entity->model_class);
|
||||
|
||||
// Use alias if provided
|
||||
if ($entity->alias) {
|
||||
$table = "{$table} as {$entity->alias}";
|
||||
}
|
||||
|
||||
$joinMethod = $entity->join_type;
|
||||
|
||||
$query->{$joinMethod}(
|
||||
$table,
|
||||
$entity->join_first,
|
||||
$entity->join_operator ?? '=',
|
||||
$entity->join_second
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply column selections.
|
||||
*/
|
||||
protected function applySelects(Builder $query, Report $report): void
|
||||
{
|
||||
$columns = $report->columns
|
||||
->where('visible', true)
|
||||
->map(fn($col) => DB::raw("{$col->expression} as {$col->key}"))
|
||||
->toArray();
|
||||
|
||||
if (!empty($columns)) {
|
||||
$query->select($columns);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply GROUP BY clause if aggregate functions are detected.
|
||||
*/
|
||||
protected function applyGroupBy(Builder $query, Report $report): void
|
||||
{
|
||||
$visibleColumns = $report->columns->where('visible', true);
|
||||
|
||||
// Check if any column uses aggregate functions
|
||||
$hasAggregates = $visibleColumns->contains(function ($col) {
|
||||
return preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression);
|
||||
});
|
||||
|
||||
if (!$hasAggregates) {
|
||||
return; // No aggregates, no grouping needed
|
||||
}
|
||||
|
||||
// Find non-aggregate columns that need to be in GROUP BY
|
||||
$groupByColumns = $visibleColumns
|
||||
->filter(function ($col) {
|
||||
// Check if this column does NOT use an aggregate function
|
||||
return !preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression);
|
||||
})
|
||||
->map(function ($col) {
|
||||
// Extract the actual column expression (before any COALESCE, CAST, etc.)
|
||||
// For COALESCE(segments.name, 'default'), we need segments.name
|
||||
if (preg_match('/COALESCE\s*\(\s*([^,]+)\s*,/i', $col->expression, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
// For simple columns, use as-is
|
||||
return $col->expression;
|
||||
})
|
||||
->filter() // Remove empty values
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
if (!empty($groupByColumns)) {
|
||||
foreach ($groupByColumns as $column) {
|
||||
$query->groupBy(DB::raw($column));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply conditions (WHERE clauses).
|
||||
*/
|
||||
protected function applyConditions(Builder $query, Report $report, array $filters): void
|
||||
{
|
||||
$conditions = $report->conditions->where('enabled', true);
|
||||
|
||||
// Group conditions by group_id
|
||||
$groups = $conditions->groupBy('group_id');
|
||||
|
||||
foreach ($groups as $groupId => $groupConditions) {
|
||||
$query->where(function ($subQuery) use ($groupConditions, $filters) {
|
||||
foreach ($groupConditions as $condition) {
|
||||
$value = $this->resolveConditionValue($condition, $filters);
|
||||
|
||||
// Skip if filter-based and no value provided
|
||||
if ($condition->value_type === 'filter' && $value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = $condition->logical_operator === 'OR' ? 'orWhere' : 'where';
|
||||
|
||||
$this->applyCondition($subQuery, $condition, $value, $method);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a single condition to the query.
|
||||
*/
|
||||
protected function applyCondition(Builder $query, $condition, $value, string $method): void
|
||||
{
|
||||
$column = $condition->column;
|
||||
$operator = strtoupper($condition->operator);
|
||||
|
||||
switch ($operator) {
|
||||
case 'IS NULL':
|
||||
$query->{$method . 'Null'}($column);
|
||||
break;
|
||||
|
||||
case 'IS NOT NULL':
|
||||
$query->{$method . 'NotNull'}($column);
|
||||
break;
|
||||
|
||||
case 'IN':
|
||||
$values = is_array($value) ? $value : explode(',', $value);
|
||||
$query->{$method . 'In'}($column, $values);
|
||||
break;
|
||||
|
||||
case 'NOT IN':
|
||||
$values = is_array($value) ? $value : explode(',', $value);
|
||||
$query->{$method . 'NotIn'}($column, $values);
|
||||
break;
|
||||
|
||||
case 'BETWEEN':
|
||||
if (is_array($value) && count($value) === 2) {
|
||||
$query->{$method . 'Between'}($column, $value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LIKE':
|
||||
$query->{$method}($column, 'LIKE', $value);
|
||||
break;
|
||||
|
||||
default:
|
||||
$query->{$method}($column, $operator, $value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve condition value based on value_type.
|
||||
*/
|
||||
protected function resolveConditionValue($condition, array $filters)
|
||||
{
|
||||
return match ($condition->value_type) {
|
||||
'static' => $condition->value,
|
||||
'filter' => $filters[$condition->filter_key] ?? null,
|
||||
'expression' => DB::raw($condition->value),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply ORDER BY clauses.
|
||||
*/
|
||||
protected function applyOrders(Builder $query, Report $report): void
|
||||
{
|
||||
foreach ($report->orders as $order) {
|
||||
$query->orderBy($order->column, $order->direction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table name from model class.
|
||||
*/
|
||||
protected function getTableFromModel(string $modelClass): string
|
||||
{
|
||||
if (!class_exists($modelClass)) {
|
||||
throw new \RuntimeException("Model class {$modelClass} does not exist.");
|
||||
}
|
||||
|
||||
return (new $modelClass)->getTable();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user