Dev branch

This commit is contained in:
Simon Pocrnjič
2025-11-02 12:31:01 +01:00
parent 5f879c9436
commit 63e0958b66
241 changed files with 17686 additions and 7327 deletions
@@ -0,0 +1,53 @@
<?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");
}
}
+78
View File
@@ -0,0 +1,78 @@
<?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');
}
}
+95
View File
@@ -0,0 +1,95 @@
<?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');
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?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;
}
+51
View File
@@ -0,0 +1,51 @@
<?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");
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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']);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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;
}
}
@@ -0,0 +1,54 @@
<?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;
}
}