feat(analytics): add site analytics with grouped navigation sections
- Add Analytics container: models, tasks, jobs, controllers, routes, services - Add analytics config with section groups (structure, institute, content, services) - Add database tables: analytics_sessions, analytics_hits, analytics_daily_stats - Add frontend: Analytics page, section detail, chart, overview, navigation components - Add consent banner and JS tracking service (sendBeacon + Inertia listener) - Add permission sync for view_analytics, view_any_analytic - Fix trailing slash URL matching in GetNavigationSectionsTask - Fix event propagation on expand arrows in navigation sections - Fix RecordVisitJob to not depend on dropped analytics_geo_cache table - Replace plain text views/visitors with badge-style icons - Group navigation sections by category with separate blocks - Add config-driven non-CMS sections (faculties, divisions, DPO, journals) - Remove Events, TV, Sveden from analytics sections
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Commands;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsDailyStat;
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AggregateAnalyticsCommand extends \Illuminate\Console\Command
|
||||
{
|
||||
protected $signature = 'analytics:aggregate {--days=1 : Days back to aggregate}';
|
||||
|
||||
protected $description = 'Aggregate analytics hits into daily stats';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
$date = now()->subDays($days)->toDateString();
|
||||
|
||||
$stats = AnalyticsHit::selectRaw('
|
||||
DATE(created_at) as hit_date,
|
||||
url,
|
||||
COUNT(*) as views_count,
|
||||
COUNT(DISTINCT session_id) as unique_visitors
|
||||
')
|
||||
->whereDate('created_at', $date)
|
||||
->groupBy(DB::raw('DATE(created_at)'), 'url')
|
||||
->get();
|
||||
|
||||
$inserted = 0;
|
||||
|
||||
foreach ($stats as $row) {
|
||||
AnalyticsDailyStat::updateOrCreate(
|
||||
['date' => $row->hit_date, 'url' => $row->url],
|
||||
[
|
||||
'views_count' => $row->views_count,
|
||||
'unique_visitors' => $row->unique_visitors,
|
||||
]
|
||||
);
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
$this->info("Aggregated {$inserted} daily stat rows for {$date}");
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Jobs;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\Analytics\Models\AnalyticsSession;
|
||||
use App\Containers\Analytics\Services\DeviceDetectorService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RecordVisitJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public int $timeout = 15;
|
||||
|
||||
public function __construct(
|
||||
public array $data,
|
||||
) {}
|
||||
|
||||
public function handle(
|
||||
DeviceDetectorService $detector,
|
||||
): void {
|
||||
$userAgent = $this->data['user_agent'] ?? '';
|
||||
|
||||
if ($detector->isBot($userAgent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$device = $detector->parse($userAgent);
|
||||
|
||||
$utmParams = array_filter([
|
||||
'utm_source' => $this->data['utm_source'] ?? null,
|
||||
'utm_medium' => $this->data['utm_medium'] ?? null,
|
||||
'utm_campaign' => $this->data['utm_campaign'] ?? null,
|
||||
], fn($v) => $v !== null);
|
||||
|
||||
$session = (new AnalyticsSession())->firstOrCreateSession(
|
||||
visitorId: $this->data['visitor_id'],
|
||||
ip: $this->data['ip'] ?? '0.0.0.0',
|
||||
userId: $this->data['user_id'] ?? null,
|
||||
entryPage: $this->data['url'],
|
||||
utmParams: $utmParams,
|
||||
);
|
||||
|
||||
if (empty($session->browser)) {
|
||||
$session->update([
|
||||
'browser' => $device['browser'],
|
||||
'os' => $device['os'],
|
||||
'device_type' => $device['type'],
|
||||
]);
|
||||
}
|
||||
|
||||
AnalyticsHit::create([
|
||||
'session_id' => $session->id,
|
||||
'user_id' => $this->data['user_id'] ?? null,
|
||||
'url' => $this->data['url'],
|
||||
'referrer' => $this->data['referrer'] ?? null,
|
||||
'title' => $this->data['title'] ?? null,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AnalyticsDailyStat extends Model
|
||||
{
|
||||
protected $table = 'analytics_daily_stats';
|
||||
|
||||
protected $fillable = [
|
||||
'date',
|
||||
'url',
|
||||
'views_count',
|
||||
'unique_visitors',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'views_count' => 'integer',
|
||||
'unique_visitors' => 'integer',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AnalyticsHit extends Model
|
||||
{
|
||||
protected $table = 'analytics_hits';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'session_id',
|
||||
'user_id',
|
||||
'url',
|
||||
'referrer',
|
||||
'title',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function session(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AnalyticsSession::class, 'session_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Containers\User\Models\User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AnalyticsSession extends Model
|
||||
{
|
||||
protected $table = 'analytics_sessions';
|
||||
|
||||
protected $fillable = [
|
||||
'visitor_id',
|
||||
'user_id',
|
||||
'entry_page',
|
||||
'ip',
|
||||
'utm_source',
|
||||
'utm_medium',
|
||||
'utm_campaign',
|
||||
'browser',
|
||||
'os',
|
||||
'device_type',
|
||||
'started_at',
|
||||
'last_activity_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'started_at' => 'datetime',
|
||||
'last_activity_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function hits(): HasMany
|
||||
{
|
||||
return $this->hasMany(AnalyticsHit::class, 'session_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Containers\User\Models\User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function firstOrCreateSession(
|
||||
string $visitorId,
|
||||
string $ip,
|
||||
int $userId = null,
|
||||
string $entryPage = null,
|
||||
array $utmParams = [],
|
||||
): self {
|
||||
$lastSession = static::where('visitor_id', $visitorId)
|
||||
->where('last_activity_at', '>', now()->subMinutes(30))
|
||||
->latest('last_activity_at')
|
||||
->first();
|
||||
|
||||
if ($lastSession) {
|
||||
$lastSession->update(['last_activity_at' => now()]);
|
||||
return $lastSession;
|
||||
}
|
||||
|
||||
return static::create([
|
||||
'visitor_id' => $visitorId,
|
||||
'user_id' => $userId,
|
||||
'entry_page' => $entryPage,
|
||||
'ip' => $ip,
|
||||
'utm_source' => $utmParams['utm_source'] ?? null,
|
||||
'utm_medium' => $utmParams['utm_medium'] ?? null,
|
||||
'utm_campaign' => $utmParams['utm_campaign'] ?? null,
|
||||
'started_at' => now(),
|
||||
'last_activity_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
|
||||
class AnalyticsServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
\App\Containers\Analytics\Commands\AggregateAnalyticsCommand::class,
|
||||
]);
|
||||
|
||||
$this->app->afterResolving(Schedule::class, function (Schedule $schedule) {
|
||||
$schedule->command('analytics:aggregate', ['--days' => 1])->hourly();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Services;
|
||||
|
||||
use WhichBrowser\Parser;
|
||||
|
||||
class DeviceDetectorService
|
||||
{
|
||||
public function parse(string $userAgent): array
|
||||
{
|
||||
$parser = new Parser($userAgent);
|
||||
|
||||
$browser = $parser->browser->name ?? null;
|
||||
$os = $parser->os->name ?? null;
|
||||
|
||||
$deviceType = 'desktop';
|
||||
if ($parser->isType('tablet')) {
|
||||
$deviceType = 'tablet';
|
||||
} elseif ($parser->isType('mobile')) {
|
||||
$deviceType = 'mobile';
|
||||
}
|
||||
|
||||
return [
|
||||
'browser' => $browser,
|
||||
'os' => $os,
|
||||
'type' => $deviceType,
|
||||
];
|
||||
}
|
||||
|
||||
public function isBot(string $userAgent): bool
|
||||
{
|
||||
$parser = new Parser($userAgent);
|
||||
return $parser->isType('bot');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Services;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsGeoCache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GeoIpService
|
||||
{
|
||||
private const CACHE_TTL_DAYS = 7;
|
||||
|
||||
public function locate(string $ip): ?array
|
||||
{
|
||||
$ipHash = sha1($ip);
|
||||
|
||||
$cached = AnalyticsGeoCache::where('ip_hash', $ipHash)
|
||||
->where('cached_at', '>', now()->subDays(self::CACHE_TTL_DAYS))
|
||||
->first();
|
||||
|
||||
if ($cached) {
|
||||
return [
|
||||
'country' => $cached->country,
|
||||
'city' => $cached->city,
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(3)->get("http://ip-api.com/json/{$ip}", [
|
||||
'fields' => 'country,city',
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
AnalyticsGeoCache::updateOrCreate(
|
||||
['ip_hash' => $ipHash],
|
||||
[
|
||||
'country' => $data['country'] ?? null,
|
||||
'city' => $data['city'] ?? null,
|
||||
'cached_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
return [
|
||||
'country' => $data['country'] ?? null,
|
||||
'city' => $data['city'] ?? null,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('GeoIP lookup failed', ['ip' => $ip, 'error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\Analytics\Models\AnalyticsSession;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetAnalyticsOverviewTask
|
||||
{
|
||||
public function run(int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
$to = now()->endOfDay();
|
||||
|
||||
if ($days === 0) {
|
||||
$prevFrom = Carbon::yesterday()->startOfDay();
|
||||
$prevTo = Carbon::yesterday()->endOfDay();
|
||||
} else {
|
||||
$prevFrom = Carbon::now()->subDays($days * 2)->startOfDay();
|
||||
$prevTo = Carbon::now()->subDays($days)->startOfDay();
|
||||
}
|
||||
|
||||
$current = $this->getPeriodStats($from, $to);
|
||||
$previous = $this->getPeriodStats($prevFrom, $prevTo);
|
||||
|
||||
return [
|
||||
'unique_visitors' => $this->compare($current['visitors'], $previous['visitors']),
|
||||
'page_views' => $this->compare($current['views'], $previous['views']),
|
||||
'avg_time' => $this->compare($current['avg_time'], $previous['avg_time'], true),
|
||||
'bounce_rate' => $this->compare($current['bounce_rate'], $previous['bounce_rate'], true, true),
|
||||
];
|
||||
}
|
||||
|
||||
private function getPeriodStats(Carbon $from, Carbon $to): array
|
||||
{
|
||||
$hits = AnalyticsHit::whereBetween('created_at', [$from, $to])->count();
|
||||
|
||||
$sessions = AnalyticsSession::whereBetween('started_at', [$from, $to])
|
||||
->get(['id', 'started_at', 'last_activity_at']);
|
||||
|
||||
$uniqueVisitors = AnalyticsSession::whereBetween('started_at', [$from, $to])
|
||||
->distinct('visitor_id')
|
||||
->count('visitor_id');
|
||||
|
||||
$totalSessions = $sessions->count();
|
||||
|
||||
$avgTime = 0;
|
||||
if ($totalSessions > 0) {
|
||||
$totalSeconds = $sessions->sum(function ($s) {
|
||||
return $s->started_at->diffInSeconds($s->last_activity_at);
|
||||
});
|
||||
$avgTime = (int) round($totalSeconds / $totalSessions);
|
||||
}
|
||||
|
||||
$bounceRate = 0;
|
||||
if ($totalSessions > 0) {
|
||||
$sessionIds = $sessions->pluck('id');
|
||||
$singleHitSessions = AnalyticsHit::whereIn('session_id', $sessionIds)
|
||||
->selectRaw('session_id, COUNT(*) as hits')
|
||||
->groupBy('session_id')
|
||||
->havingRaw('COUNT(*) = 1')
|
||||
->count();
|
||||
$bounceRate = (int) round(($singleHitSessions / $totalSessions) * 100);
|
||||
}
|
||||
|
||||
return [
|
||||
'views' => $hits,
|
||||
'visitors' => $uniqueVisitors,
|
||||
'avg_time' => $avgTime,
|
||||
'bounce_rate' => $bounceRate,
|
||||
];
|
||||
}
|
||||
|
||||
private function compare(int $current, int $previous, bool $isRatio = false, bool $invertDirection = false): array
|
||||
{
|
||||
$change = 0;
|
||||
if ($previous > 0) {
|
||||
$change = $isRatio
|
||||
? $current - $previous
|
||||
: (int) round((($current - $previous) / $previous) * 100);
|
||||
} elseif ($current > 0) {
|
||||
$change = $isRatio ? 0 : 100;
|
||||
}
|
||||
|
||||
$direction = $invertDirection
|
||||
? ($change < 0 ? 'up' : ($change > 0 ? 'down' : 'neutral'))
|
||||
: ($change > 0 ? 'up' : ($change < 0 ? 'down' : 'neutral'));
|
||||
|
||||
return [
|
||||
'value' => $current,
|
||||
'change' => $change,
|
||||
'direction' => $direction,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsDailyStat;
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetDailyStatsTask
|
||||
{
|
||||
public function run(int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
|
||||
$stats = AnalyticsDailyStat::where('date', '>=', $from)
|
||||
->selectRaw('
|
||||
date,
|
||||
SUM(views_count) as total_views,
|
||||
SUM(unique_visitors) as total_unique
|
||||
')
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
if ($stats->isEmpty()) {
|
||||
return AnalyticsHit::where('created_at', '>=', $from)
|
||||
->selectRaw('
|
||||
DATE(created_at) as hit_date,
|
||||
COUNT(*) as total_views,
|
||||
COUNT(DISTINCT session_id) as total_unique
|
||||
')
|
||||
->groupBy(DB::raw('DATE(created_at)'))
|
||||
->orderBy('hit_date')
|
||||
->get()
|
||||
->map(fn($row) => [
|
||||
'date' => (string) $row->hit_date,
|
||||
'views' => (int) $row->total_views,
|
||||
'visitors' => (int) $row->total_unique,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return $stats->map(fn($row) => [
|
||||
'date' => $row->date instanceof Carbon ? $row->date->format('Y-m-d') : (string) $row->date,
|
||||
'views' => (int) $row->total_views,
|
||||
'visitors' => (int) $row->total_unique,
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsSession;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class GetDevicesTask
|
||||
{
|
||||
public function run(int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
|
||||
$devices = AnalyticsSession::where('started_at', '>=', $from)
|
||||
->selectRaw('
|
||||
device_type,
|
||||
COUNT(*) as count
|
||||
')
|
||||
->groupBy('device_type')
|
||||
->get()
|
||||
->pluck('count', 'device_type')
|
||||
->toArray();
|
||||
|
||||
$browsers = AnalyticsSession::where('started_at', '>=', $from)
|
||||
->whereNotNull('browser')
|
||||
->selectRaw('
|
||||
browser,
|
||||
COUNT(*) as count
|
||||
')
|
||||
->groupBy('browser')
|
||||
->orderByDesc('count')
|
||||
->limit(10)
|
||||
->get()
|
||||
->pluck('count', 'browser')
|
||||
->toArray();
|
||||
|
||||
$oses = AnalyticsSession::where('started_at', '>=', $from)
|
||||
->whereNotNull('os')
|
||||
->selectRaw('
|
||||
os,
|
||||
COUNT(*) as count
|
||||
')
|
||||
->groupBy('os')
|
||||
->orderByDesc('count')
|
||||
->limit(10)
|
||||
->get()
|
||||
->pluck('count', 'os')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'devices' => $devices,
|
||||
'browsers' => $browsers,
|
||||
'oses' => $oses,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\AppStructure\Models\MainSection;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class GetNavigationSectionsTask
|
||||
{
|
||||
public function run(int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
|
||||
$hits = AnalyticsHit::where('created_at', '>=', $from)
|
||||
->selectRaw('url, session_id')
|
||||
->get();
|
||||
|
||||
$mainSections = MainSection::with('subSections.pages')
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
|
||||
$sections = [];
|
||||
$cmsSlugs = [];
|
||||
|
||||
// 1. CMS sections (MainSection → SubSection → Page)
|
||||
foreach ($mainSections as $ms) {
|
||||
$msPrefix = '/' . $ms->slug;
|
||||
$cmsSlugs[] = $msPrefix;
|
||||
$msHits = $hits->filter(fn($h) => str_starts_with(rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/'), $msPrefix));
|
||||
$subSections = [];
|
||||
|
||||
foreach ($ms->subSections as $ss) {
|
||||
$ssPrefix = $msPrefix . '/' . $ss->slug;
|
||||
$ssHits = $msHits->filter(fn($h) => str_starts_with(rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/'), $ssPrefix));
|
||||
$pages = [];
|
||||
|
||||
foreach ($ss->pages as $page) {
|
||||
$pagePath = '/' . $page->path;
|
||||
$pHits = $ssHits->filter(fn($h) => rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/') === $pagePath);
|
||||
|
||||
$pages[] = [
|
||||
'id' => $page->id,
|
||||
'title' => $page->title ?: $page->slug,
|
||||
'path' => $page->path,
|
||||
'url' => $pagePath,
|
||||
'views' => $pHits->count(),
|
||||
'visitors' => $pHits->pluck('session_id')->unique()->count(),
|
||||
];
|
||||
}
|
||||
|
||||
$subSections[] = [
|
||||
'id' => $ss->id,
|
||||
'title' => $ss->title,
|
||||
'slug' => $ss->slug,
|
||||
'views' => $ssHits->count(),
|
||||
'visitors' => $ssHits->pluck('session_id')->unique()->count(),
|
||||
'pages' => $pages,
|
||||
];
|
||||
}
|
||||
|
||||
$sections[] = [
|
||||
'id' => $ms->id,
|
||||
'title' => $ms->title,
|
||||
'slug' => $ms->slug,
|
||||
'group' => 'structure',
|
||||
'views' => $msHits->count(),
|
||||
'visitors' => $msHits->pluck('session_id')->unique()->count(),
|
||||
'sub_sections' => $subSections,
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Config-defined sections (non-CMS)
|
||||
$configSections = config('analytics.sections', []);
|
||||
foreach ($configSections as $prefix => $config) {
|
||||
if ($prefix === '/') continue;
|
||||
if (in_array($prefix, $cmsSlugs)) continue;
|
||||
|
||||
$sectionHits = $hits->filter(fn($h) => str_starts_with(rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/'), $prefix));
|
||||
$subSections = [];
|
||||
|
||||
if (isset($config['children'])) {
|
||||
$model = $config['children']['model'];
|
||||
$nameKey = $config['children']['name_key'];
|
||||
$slugKey = $config['children']['slug_key'];
|
||||
$children = $model::all();
|
||||
|
||||
foreach ($children as $child) {
|
||||
$childSlug = $child->{$slugKey};
|
||||
$childPrefix = $prefix . '/' . $childSlug;
|
||||
$childHits = $sectionHits->filter(fn($h) => str_starts_with(rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/'), $childPrefix));
|
||||
|
||||
$subSections[] = [
|
||||
'id' => $child->id,
|
||||
'title' => $child->{$nameKey},
|
||||
'slug' => $childSlug,
|
||||
'views' => $childHits->count(),
|
||||
'visitors' => $childHits->pluck('session_id')->unique()->count(),
|
||||
'pages' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$sections[] = [
|
||||
'id' => 'config:' . $prefix,
|
||||
'title' => $config['label'],
|
||||
'slug' => ltrim($prefix, '/'),
|
||||
'group' => $config['group'] ?? 'other',
|
||||
'views' => $sectionHits->count(),
|
||||
'visitors' => $sectionHits->pluck('session_id')->unique()->count(),
|
||||
'sub_sections' => $subSections,
|
||||
];
|
||||
}
|
||||
|
||||
// Homepage — as first item in structure group
|
||||
$homeHits = $hits->filter(fn($h) => (rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/') === '/'));
|
||||
|
||||
array_unshift($sections, [
|
||||
'id' => 'home',
|
||||
'title' => 'Главная страница',
|
||||
'slug' => '',
|
||||
'group' => 'structure',
|
||||
'type' => 'home',
|
||||
'views' => $homeHits->count(),
|
||||
'visitors' => $homeHits->pluck('session_id')->unique()->count(),
|
||||
'sub_sections' => [],
|
||||
]);
|
||||
|
||||
return [
|
||||
'sections' => $sections,
|
||||
'groupLabels' => config('analytics.group_labels', []),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetReferrersTask
|
||||
{
|
||||
public function run(int $days = 30, int $limit = 10): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
|
||||
$referrers = AnalyticsHit::where('created_at', '>=', $from)
|
||||
->whereNotNull('referrer')
|
||||
->where('referrer', '!=', '')
|
||||
->selectRaw('
|
||||
CASE
|
||||
WHEN referrer LIKE "%google.%" THEN "Google"
|
||||
WHEN referrer LIKE "%yandex.%" THEN "Yandex"
|
||||
WHEN referrer LIKE "%vk.com%" THEN "VK"
|
||||
WHEN referrer LIKE "%t.me%" THEN "Telegram"
|
||||
WHEN referrer LIKE "%ok.ru%" THEN "Одноклассники"
|
||||
WHEN referrer LIKE "%dzen.ru%" THEN "Дзен"
|
||||
WHEN referrer LIKE "%facebook.%" THEN "Facebook"
|
||||
WHEN referrer LIKE "%instagram.%" THEN "Instagram"
|
||||
WHEN referrer LIKE "%twitter.%" THEN "Twitter"
|
||||
ELSE SUBSTRING_INDEX(SUBSTRING_INDEX(referrer, "/", 3), "/", -1)
|
||||
END as source,
|
||||
COUNT(*) as hits,
|
||||
COUNT(DISTINCT session_id) as visitors
|
||||
')
|
||||
->groupBy('source')
|
||||
->orderByDesc('hits')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$directCount = AnalyticsHit::where('created_at', '>=', $from)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('referrer')->orWhere('referrer', '');
|
||||
})
|
||||
->count();
|
||||
|
||||
$results = array_map(fn($r) => [
|
||||
'source' => $r['source'],
|
||||
'hits' => (int) $r['hits'],
|
||||
'visitors' => (int) $r['visitors'],
|
||||
], $referrers);
|
||||
|
||||
if ($directCount > 0) {
|
||||
array_unshift($results, [
|
||||
'source' => 'Прямой заход',
|
||||
'hits' => $directCount,
|
||||
'visitors' => $directCount,
|
||||
]);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\Analytics\Models\AnalyticsSession;
|
||||
use App\Containers\AppStructure\Models\MainSection;
|
||||
use App\Containers\AppStructure\Models\Page;
|
||||
use App\Containers\AppStructure\Models\SubSection;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class GetSectionDetailTask
|
||||
{
|
||||
public function run(string $prefix, int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
|
||||
$hits = AnalyticsHit::where('created_at', '>=', $from)
|
||||
->where('url', 'like', $prefix . '%')
|
||||
->selectRaw('url, session_id, created_at, referrer')
|
||||
->get();
|
||||
|
||||
$totalViews = $hits->count();
|
||||
$uniqueVisitors = $hits->pluck('session_id')->unique()->count();
|
||||
|
||||
$mainSection = null;
|
||||
$subSection = null;
|
||||
$page = null;
|
||||
$children = [];
|
||||
$breadcrumbs = [];
|
||||
$type = 'unknown';
|
||||
$exitPages = [];
|
||||
|
||||
$cleanPrefix = ltrim($prefix, '/');
|
||||
$parts = array_filter(explode('/', $cleanPrefix));
|
||||
|
||||
// 1. Check if MainSection
|
||||
$mainSection = MainSection::where('slug', $cleanPrefix)->first();
|
||||
if ($mainSection) {
|
||||
$type = 'main_section';
|
||||
$breadcrumbs = [['title' => 'Аналитика', 'url' => route('dashboard.analytics.index')]];
|
||||
$subSections = SubSection::where('main_section_id', $mainSection->id)
|
||||
->with('pages')
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
|
||||
foreach ($subSections as $ss) {
|
||||
$ssPrefix = '/' . $mainSection->slug . '/' . $ss->slug;
|
||||
$ssHits = $hits->filter(fn($h) => str_starts_with(parse_url($h->url, PHP_URL_PATH) ?: '', $ssPrefix));
|
||||
$children[] = [
|
||||
'id' => $ss->id,
|
||||
'title' => $ss->title,
|
||||
'prefix' => $ssPrefix,
|
||||
'views' => $ssHits->count(),
|
||||
'visitors' => $ssHits->pluck('session_id')->unique()->count(),
|
||||
'pages_count' => $ss->pages->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
// 2. Check if SubSection (2 parts)
|
||||
elseif (count($parts) === 2) {
|
||||
$mainSection = MainSection::where('slug', $parts[0])->first();
|
||||
$subSection = SubSection::where('slug', $parts[1])
|
||||
->where('main_section_id', $mainSection?->id)
|
||||
->first();
|
||||
|
||||
if ($subSection && $mainSection) {
|
||||
$type = 'sub_section';
|
||||
$breadcrumbs = [
|
||||
['title' => 'Аналитика', 'url' => route('dashboard.analytics.index')],
|
||||
['title' => $mainSection->title, 'url' => route('dashboard.analytics.section', ['prefix' => '/' . $mainSection->slug])],
|
||||
];
|
||||
|
||||
$pages = Page::where('sub_section_id', $subSection->id)
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
|
||||
foreach ($pages as $p) {
|
||||
$pUrl = '/' . $p->path;
|
||||
$pHits = $hits->filter(fn($h) => rtrim(parse_url($h->url, PHP_URL_PATH) ?: '', '/') === $pUrl);
|
||||
$children[] = [
|
||||
'id' => $p->id,
|
||||
'title' => $p->title ?: $p->slug,
|
||||
'url' => $pUrl,
|
||||
'views' => $pHits->count(),
|
||||
'visitors' => $pHits->pluck('session_id')->unique()->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check if Page (by path or 3+ parts)
|
||||
if ($type === 'unknown') {
|
||||
$page = Page::where('path', $cleanPrefix)->first();
|
||||
if ($page && $page->section) {
|
||||
$subSection = $page->section;
|
||||
$mainSection = $subSection->mainSection;
|
||||
}
|
||||
|
||||
if ($mainSection && $subSection) {
|
||||
$type = 'page';
|
||||
$breadcrumbs = [
|
||||
['title' => 'Аналитика', 'url' => route('dashboard.analytics.index')],
|
||||
['title' => $mainSection->title, 'url' => route('dashboard.analytics.section', ['prefix' => '/' . $mainSection->slug])],
|
||||
['title' => $subSection->title, 'url' => route('dashboard.analytics.section', ['prefix' => '/' . $mainSection->slug . '/' . $subSection->slug])],
|
||||
];
|
||||
|
||||
// Exit pages: URLs visited after this page in the same session
|
||||
$exitPages = $this->getExitPages($hits, $prefix, $days);
|
||||
}
|
||||
}
|
||||
|
||||
$label = $cleanPrefix;
|
||||
if ($mainSection) $label = $mainSection->title;
|
||||
if ($subSection) $label = $subSection->title;
|
||||
if ($page) $label = $page->title ?: $page->slug;
|
||||
|
||||
$topPages = $hits->groupBy('url')
|
||||
->map(fn($group) => [
|
||||
'url' => $group->first()->url,
|
||||
'views' => $group->count(),
|
||||
'unique_visitors' => $group->pluck('session_id')->unique()->count(),
|
||||
])
|
||||
->sortByDesc('views')
|
||||
->values()
|
||||
->take(15)
|
||||
->toArray();
|
||||
|
||||
$dailyStats = $hits->groupBy(fn($h) => $h->created_at->format('Y-m-d'))
|
||||
->map(fn($group, $date) => [
|
||||
'date' => $date,
|
||||
'views' => $group->count(),
|
||||
'visitors' => $group->pluck('session_id')->unique()->count(),
|
||||
])
|
||||
->values()
|
||||
->sortBy('date')
|
||||
->toArray();
|
||||
|
||||
$referrers = $hits->filter(fn($h) => !empty($h->referrer))
|
||||
->groupBy(function ($h) {
|
||||
$url = $h->referrer;
|
||||
if (str_contains($url, 'google.')) return 'Google';
|
||||
if (str_contains($url, 'yandex.')) return 'Yandex';
|
||||
if (str_contains($url, 'vk.com')) return 'VK';
|
||||
if (str_contains($url, 't.me')) return 'Telegram';
|
||||
if (str_contains($url, 'ok.ru')) return 'Одноклассники';
|
||||
if (str_contains($url, 'dzen.ru')) return 'Дзен';
|
||||
return parse_url($url, PHP_URL_HOST) ?? 'Другое';
|
||||
})
|
||||
->map(fn($group) => [
|
||||
'source' => $group->first()->referrer,
|
||||
'hits' => $group->count(),
|
||||
'visitors' => $group->pluck('session_id')->unique()->count(),
|
||||
])
|
||||
->sortByDesc('hits')
|
||||
->values()
|
||||
->take(10)
|
||||
->toArray();
|
||||
|
||||
$devices = AnalyticsSession::where('started_at', '>=', $from)
|
||||
->whereIn('id', $hits->pluck('session_id')->unique())
|
||||
->selectRaw('device_type, COUNT(*) as count')
|
||||
->groupBy('device_type')
|
||||
->pluck('count', 'device_type')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'prefix' => $prefix,
|
||||
'type' => $type,
|
||||
'label' => $label,
|
||||
'breadcrumbs' => $breadcrumbs,
|
||||
'children' => $children,
|
||||
'exit_pages' => $exitPages,
|
||||
'total_views' => $totalViews,
|
||||
'unique_visitors' => $uniqueVisitors,
|
||||
'top_pages' => $topPages,
|
||||
'daily_stats' => $dailyStats,
|
||||
'referrers' => $referrers,
|
||||
'devices' => $devices,
|
||||
];
|
||||
}
|
||||
|
||||
private function getExitPages($hits, string $prefix, int $days): array
|
||||
{
|
||||
$sessionIds = $hits->pluck('session_id')->unique();
|
||||
|
||||
$allHits = AnalyticsHit::where('created_at', '>=', now()->subDays($days)->startOfDay())
|
||||
->whereIn('session_id', $sessionIds)
|
||||
->orderBy('session_id')
|
||||
->orderBy('created_at')
|
||||
->selectRaw('session_id, url, created_at')
|
||||
->get()
|
||||
->groupBy('session_id');
|
||||
|
||||
$exitCounts = [];
|
||||
foreach ($allHits as $sessionId => $sessionHits) {
|
||||
$sorted = $sessionHits->values();
|
||||
for ($i = 0; $i < $sorted->count() - 1; $i++) {
|
||||
$currentUrl = parse_url($sorted[$i]->url, PHP_URL_PATH) ?: '/';
|
||||
if (str_starts_with($currentUrl, $prefix)) {
|
||||
$nextUrl = parse_url($sorted[$i + 1]->url, PHP_URL_PATH) ?: '/';
|
||||
if (!isset($exitCounts[$nextUrl])) {
|
||||
$exitCounts[$nextUrl] = 0;
|
||||
}
|
||||
$exitCounts[$nextUrl]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arsort($exitCounts);
|
||||
return array_slice(array_map(fn($url, $count) => [
|
||||
'url' => $url,
|
||||
'count' => $count,
|
||||
], array_keys($exitCounts), $exitCounts), 0, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\AppStructure\Models\MainSection;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetSectionsStatsTask
|
||||
{
|
||||
public function run(int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
$sections = config('analytics.sections', []);
|
||||
|
||||
$hits = AnalyticsHit::where('created_at', '>=', $from)
|
||||
->selectRaw('url, session_id')
|
||||
->get();
|
||||
|
||||
$grouped = [];
|
||||
foreach ($hits as $hit) {
|
||||
$prefix = $this->resolvePrefix($hit->url, $sections);
|
||||
if (!isset($grouped[$prefix])) {
|
||||
$grouped[$prefix] = ['views' => 0, 'visitors' => []];
|
||||
}
|
||||
$grouped[$prefix]['views']++;
|
||||
$grouped[$prefix]['visitors'][$hit->session_id] = true;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($grouped as $prefix => $data) {
|
||||
$config = $sections[$prefix] ?? null;
|
||||
$result[] = [
|
||||
'prefix' => $prefix,
|
||||
'label' => $config['label'] ?? $prefix,
|
||||
'icon' => $config['icon'] ?? 'folder',
|
||||
'views' => $data['views'],
|
||||
'visitors' => count($data['visitors']),
|
||||
'order' => $config['order'] ?? 99,
|
||||
];
|
||||
}
|
||||
|
||||
usort($result, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function resolvePrefix(string $url, array $sections): string
|
||||
{
|
||||
$path = parse_url($url, PHP_URL_PATH) ?: '/';
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
$firstSegment = '/' . ($segments[0] ?? '');
|
||||
|
||||
if (isset($sections[$firstSegment])) {
|
||||
return $firstSegment;
|
||||
}
|
||||
|
||||
if ($firstSegment === '/' && $path === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// Check main sections for CMS pages
|
||||
$mainSection = MainSection::where('slug', $segments[0] ?? '')->first();
|
||||
if ($mainSection) {
|
||||
return '/page';
|
||||
}
|
||||
|
||||
return '/other';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetTopPagesTask
|
||||
{
|
||||
public function run(int $days = 7, int $limit = 10): array
|
||||
{
|
||||
return AnalyticsHit::select('url')
|
||||
->selectRaw('COUNT(*) as views')
|
||||
->selectRaw('COUNT(DISTINCT session_id) as unique_visitors')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->groupBy('url')
|
||||
->orderByDesc('views')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\UI\WEB\Controllers;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsDailyStat;
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\Analytics\Models\AnalyticsSession;
|
||||
use App\Containers\Analytics\Tasks\GetAnalyticsOverviewTask;
|
||||
use App\Containers\Analytics\Tasks\GetDailyStatsTask;
|
||||
use App\Containers\Analytics\Tasks\GetDevicesTask;
|
||||
use App\Containers\Analytics\Tasks\GetNavigationSectionsTask;
|
||||
use App\Containers\Analytics\Tasks\GetReferrersTask;
|
||||
use App\Containers\Analytics\Tasks\GetSectionDetailTask;
|
||||
use App\Containers\Analytics\Tasks\GetTopPagesTask;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Response;
|
||||
|
||||
class AnalyticsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetAnalyticsOverviewTask $overviewTask,
|
||||
private readonly GetTopPagesTask $topPagesTask,
|
||||
private readonly GetDailyStatsTask $dailyStatsTask,
|
||||
private readonly GetReferrersTask $referrersTask,
|
||||
private readonly GetDevicesTask $devicesTask,
|
||||
private readonly GetNavigationSectionsTask $navigationSectionsTask,
|
||||
private readonly GetSectionDetailTask $sectionDetailTask,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$days = (int) $request->query('days', 30);
|
||||
|
||||
return inertia()->render('Dashboard/Analytics', [
|
||||
'overview' => $this->overviewTask->run(days: $days),
|
||||
'topPages' => $this->topPagesTask->run(days: $days),
|
||||
'dailyStats' => $this->dailyStatsTask->run(days: $days),
|
||||
'referrers' => $this->referrersTask->run(days: $days),
|
||||
'devices' => $this->devicesTask->run(days: $days),
|
||||
'navigation' => $this->navigationSectionsTask->run(days: $days),
|
||||
'period' => $days,
|
||||
]);
|
||||
}
|
||||
|
||||
public function section(Request $request): Response
|
||||
{
|
||||
$prefix = $request->query('prefix', '/');
|
||||
$days = (int) $request->query('days', 30);
|
||||
|
||||
$section = $this->sectionDetailTask->run(prefix: $prefix, days: $days);
|
||||
|
||||
return inertia()->render('Dashboard/AnalyticsSectionDetail', [
|
||||
'section' => $section,
|
||||
'period' => $days,
|
||||
]);
|
||||
}
|
||||
|
||||
public function clear(): RedirectResponse
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0');
|
||||
AnalyticsHit::truncate();
|
||||
AnalyticsSession::truncate();
|
||||
AnalyticsDailyStat::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1');
|
||||
|
||||
return redirect()->route('dashboard.analytics.index')
|
||||
->with('success', 'Данные аналитики очищены');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\UI\WEB\Controllers;
|
||||
|
||||
use App\Containers\Analytics\Jobs\RecordVisitJob;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class TrackController extends Controller
|
||||
{
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
$payload = [
|
||||
'visitor_id' => $request->input('visitor_id'),
|
||||
'url' => $request->input('url'),
|
||||
'referrer' => $request->input('referrer'),
|
||||
'title' => $request->input('title'),
|
||||
'screen_resolution' => $request->input('screen_resolution'),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'user_id' => auth('web')->id(),
|
||||
];
|
||||
|
||||
$url = parse_url($request->input('url', ''), PHP_URL_QUERY);
|
||||
if ($url) {
|
||||
parse_str($url, $queryParams);
|
||||
$payload['utm_source'] = $queryParams['utm_source'] ?? null;
|
||||
$payload['utm_medium'] = $queryParams['utm_medium'] ?? null;
|
||||
$payload['utm_campaign'] = $queryParams['utm_campaign'] ?? null;
|
||||
}
|
||||
|
||||
RecordVisitJob::dispatch($payload)->onQueue('analytics');
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Containers\Analytics\UI\WEB\Controllers\TrackController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('/track/hit', [TrackController::class, 'store'])
|
||||
->middleware(['throttle:60,1'])
|
||||
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
// Route is registered in Dashboard/UI/WEB/Routes/web.php
|
||||
@@ -431,6 +431,11 @@ Route::middleware(['access-check', 'dashboard.auth', 'dashboard.permission'])->g
|
||||
Route::put('/{credential}', [IntegrationCredentialsController::class, 'update'])->name('update');
|
||||
Route::delete('/{credential}', [IntegrationCredentialsController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Аналитика
|
||||
Route::get('/dashboard/analytics', \App\Containers\Analytics\UI\WEB\Controllers\AnalyticsController::class)->name('dashboard.analytics.index');
|
||||
Route::get('/dashboard/analytics/section', [\App\Containers\Analytics\UI\WEB\Controllers\AnalyticsController::class, 'section'])->name('dashboard.analytics.section');
|
||||
Route::post('/dashboard/analytics/clear', [\App\Containers\Analytics\UI\WEB\Controllers\AnalyticsController::class, 'clear'])->name('dashboard.analytics.clear');
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ class HandleInertiaRequests extends Middleware
|
||||
|
||||
public function share(Request $request): array
|
||||
{
|
||||
// Отключаем SSR для Dashboard роутов
|
||||
if (str_starts_with($request->path(), 'dashboard')) {
|
||||
config(['inertia.ssr.enabled' => false]);
|
||||
}
|
||||
@@ -49,7 +48,6 @@ class HandleInertiaRequests extends Middleware
|
||||
}
|
||||
return 'empty';
|
||||
},
|
||||
'yandex_metrika_id' => config('services.yandex_metrika.id'),
|
||||
'flash' => function () use ($request) {
|
||||
return [
|
||||
'success' => $request->session()->get('success'),
|
||||
|
||||
@@ -87,15 +87,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
private static function configureFactoryResolution(): void
|
||||
{
|
||||
Factory::guessFactoryNamesUsing(function (string $modelName) {
|
||||
$appNamespace = 'App\\';
|
||||
$className = class_basename($modelName);
|
||||
|
||||
if (str_starts_with($modelName, $appNamespace.'Models\\')) {
|
||||
$modelName = substr($modelName, strlen($appNamespace.'Models\\'));
|
||||
} else {
|
||||
$modelName = substr($modelName, strlen($appNamespace));
|
||||
}
|
||||
|
||||
return 'Database\\Factories\\'.$modelName.'Factory';
|
||||
return 'Database\\Factories\\'.$className.'Factory';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ class SyncDashboardPermissions extends AbstractConsoleCommand
|
||||
'educational_group', 'division',
|
||||
];
|
||||
|
||||
private const EXTRA_PERMISSIONS = [
|
||||
'view_analytics',
|
||||
'view_any_analytic',
|
||||
];
|
||||
|
||||
private const PREFIXES = [
|
||||
'view_any', 'create', 'update', 'delete', 'restore', 'force_delete',
|
||||
];
|
||||
@@ -46,6 +51,13 @@ class SyncDashboardPermissions extends AbstractConsoleCommand
|
||||
['name' => 'view_any_contact_widget', 'guard_name' => 'web'],
|
||||
);
|
||||
|
||||
foreach (self::EXTRA_PERMISSIONS as $name) {
|
||||
Permission::firstOrCreate(
|
||||
['name' => $name, 'guard_name' => 'web'],
|
||||
);
|
||||
$created++;
|
||||
}
|
||||
|
||||
$roleNames = explode(',', $this->option('role'));
|
||||
$allPerms = Permission::where('guard_name', 'web')->get();
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ class HandleInertiaRequests extends Middleware
|
||||
}
|
||||
return 'empty';
|
||||
},
|
||||
'yandex_metrika_id' => config('services.yandex_metrika.id'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user